diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml deleted file mode 100644 index b0e05b2..0000000 --- a/.github/workflows/build.yaml +++ /dev/null @@ -1,62 +0,0 @@ -on: - push: - tags: - - 'v*.*.*' -jobs: - check-master: - runs-on: ubuntu-latest - outputs: - proceed: ${{ steps.check.outputs.proceed }} - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - id: check - run: | - echo "proceed=$(git branch -r --contains $GITHUB_SHA | grep -q 'origin/master' && echo true || echo false)" >> $GITHUB_OUTPUT - echo "proceed=false" >> $GITHUB_OUTPUT - api: - runs-on: ubuntu-latest - needs: check-master - environment: dockerhub-push - steps: - - uses: actions/checkout@v3 - - uses: docker/login-action@v2 - with: - username: hurtki - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - uses: docker/setup-buildx-action@v2 - - name: Docker build - run: | - VERSION="${GITHUB_REF_NAME#v}" - docker buildx build --push -t hurtki/github-banners-api:$VERSION ./api/ - renderer: - runs-on: ubuntu-latest - needs: check-master - environment: dockerhub-push - steps: - - uses: actions/checkout@v3 - - uses: docker/login-action@v2 - with: - username: hurtki - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - uses: docker/setup-buildx-action@v2 - - name: Docker build - run: | - VERSION="${GITHUB_REF_NAME#v}" - docker buildx build --push -t hurtki/github-banners-renderer:$VERSION ./renderer/ - storage: - runs-on: ubuntu-latest - needs: check-master - environment: dockerhub-push - steps: - - uses: actions/checkout@v3 - - uses: docker/login-action@v2 - with: - username: hurtki - password: ${{ secrets.DOCKER_HUB_TOKEN }} - - uses: docker/setup-buildx-action@v2 - - name: Docker build - run: | - VERSION="${GITHUB_REF_NAME#v}" - docker buildx build --push -t hurtki/github-banners-storage:$VERSION ./storage/ diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..8e21593 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,114 @@ +on: + push: + tags: + - 'v*.*.*' +jobs: + check-master: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - run: git branch -r --contains $GITHUB_SHA | grep -q 'origin/master' + api: + runs-on: ubuntu-latest + needs: check-master + environment: dockerhub-push + steps: + - uses: actions/checkout@v3 + - uses: docker/login-action@v2 + with: + username: hurtki + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - uses: docker/setup-buildx-action@v2 + - name: Docker build + run: | + VERSION="${GITHUB_REF_NAME#v}" + docker buildx build --push -t hurtki/github-banners-api:$VERSION ./api/ + renderer: + runs-on: ubuntu-latest + needs: check-master + environment: dockerhub-push + steps: + - uses: actions/checkout@v3 + - uses: docker/login-action@v2 + with: + username: hurtki + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - uses: docker/setup-buildx-action@v2 + - name: Docker build + run: | + VERSION="${GITHUB_REF_NAME#v}" + docker buildx build --push -t hurtki/github-banners-renderer:$VERSION ./renderer/ + storage: + runs-on: ubuntu-latest + needs: check-master + environment: dockerhub-push + steps: + - uses: actions/checkout@v3 + - uses: docker/login-action@v2 + with: + username: hurtki + password: ${{ secrets.DOCKER_HUB_TOKEN }} + - uses: docker/setup-buildx-action@v2 + - name: Docker build + run: | + VERSION="${GITHUB_REF_NAME#v}" + docker buildx build --push -t hurtki/github-banners-storage:$VERSION ./storage/ + deploy: + runs-on: ubuntu-latest + needs: [api, renderer, storage] + environment: ssh-deploy + steps: + - uses: actions/checkout@v3 + - name: Get version + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV + - name: Deploy via SSH + uses: appleboy/ssh-action@v0.1.7 + env: + VERSION: ${{ env.VERSION }} + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_KEY }} + port: ${{ secrets.SSH_PORT }} + script: | + cd /deploy/github-banners/ + export VER=$VERSION + docker-compose -f docker-compose.prod.yaml pull + docker-compose -f docker-compose.prod.yaml up -d + health-check: + runs-on: ubuntu-latest + needs: deploy + environment: health-check + steps: + - name: http preview endpoint check + run: | + curl -o /dev/null -s -w "%{http_code}" "https://$DOMAIN/banners/preview?username=hurtki&type=dark" | grep -q "^200$" + rollback: + needs: health-check + if: needs.health-check.result == 'failure' + runs-on: ubuntu-latest + environment: ssh-deploy + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Get previous version + run: | + PREV_TAG=$(git tag --sort=-creatordate | sed -n \'2p\' | sed \'s/^v//\') + echo "PREV_VERSION=${PREV_TAG}" >> $GITHUB_ENV + - name: Rollback via ssh + uses: appleboy/ssh-action@v0.1.7 + env: + VERSION: ${{ env.PREV_VERSION }} + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_KEY }} + port: ${{ secrets.SSH_PORT }} + script: | + cd /deploy/github-banners/ + export VER=$VERSION + docker-compose -f docker-compose.prod.yaml pull + docker-compose -f docker-compose.prod.yaml up -d diff --git a/.gitignore b/.gitignore index 7d1a074..566b429 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ go.work.sum api/renderer_impl.go api/storage_impl.go +nginx/default.svg diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml new file mode 100644 index 0000000..ecfa75d --- /dev/null +++ b/docker-compose.prod.yaml @@ -0,0 +1,110 @@ +services: + # api service + api: + image: hurtki/github-banners-api:${VER:?VER is required} + platform: linux/amd64 + container_name: api + restart: always + env_file: ./api/.env + environment: + SERVICES_SECRET_KEY: "$SERVICES_SECRET_KEY" + STORAGE_BASE_URL: "$STORAGE_BASE_URL" + networks: + - banners-net + - api-net + - kafka-net + depends_on: + kafka: + condition: service_healthy + api-psgr: + condition: service_started + # api service's Postgres database + api-psgr: + image: postgres:15 + env_file: ./api/.env + container_name: api-psgr + networks: + - api-net + volumes: + - pgdata:/var/lib/postgresql/data + # one instance of rendere service + renderer: + image: hurtki/github-banners-renderer:${VER:?VER is required} + platform: linux/amd64 + env_file: ./renderer/.env + container_name: renderer + environment: + SERVICES_SECRET_KEY: "${SERVICES_SECRET_KEY}" + STORAGE_BASE_URL: "$STORAGE_BASE_URL" + networks: + - banners-net + - kafka-net + depends_on: + kafka: + condition: service_healthy + # storage service + storage: + image: hurtki/github-banners-storage:${VER:?VER is required} + platform: linux/amd64 + container_name: storage + volumes: + - banners-storage:/var/www/banners + networks: + - banners-net + # nginx api gateway + nginx: + build: + context: ./nginx/ + dockerfile: ./nginx/Dockerfile.prod + container_name: nginx + volumes: + - banners-storage:/var/www/banners:ro + # key + cert + - /etc/nginx/ssl:/etc/nginx/ssl:ro + ports: + - 80:80 + networks: + - banners-net + depends_on: + - api + kafka: + image: apache/kafka:4.2.0 + container_name: kafka + environment: + KAFKA_NODE_ID: 1 + KAFKA_PROCESS_ROLES: broker,controller + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_NUM_PARTITIONS: 3 + KAFKA_LOG4J_ROOT_LOGLEVEL: WARN + KAFKA_LOG_DIRS: /var/lib/kafka/data + networks: + - kafka-net + healthcheck: + test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list || exit 1"] + interval: 5s + timeout: 5s + retries: 10 + volumes: + - kafka-data:/var/lib/kafka/data +volumes: + pgdata: + name: postgres_db_banners_api_ms + kafka-data: + name: kafka-data + banners-storage: + name: banners-storage +networks: + api-net: + driver: bridge + banners-net: + driver: bridge + kafka-net: + driver: bridge diff --git a/docker-compose.yaml b/docker-compose.yaml index 70b8e7f..53ca6ea 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -50,7 +50,9 @@ services: - banners-net # nginx api gateway nginx: - build: ./nginx/ + build: + context: ./nginx/ + dockerfile: Dockerfile.dev container_name: nginx volumes: - banners-storage:/var/www/banners:ro diff --git a/nginx/Dockerfile b/nginx/Dockerfile.dev similarity index 59% rename from nginx/Dockerfile rename to nginx/Dockerfile.dev index 61b25a5..21e5584 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile.dev @@ -2,6 +2,6 @@ FROM nginx:latest COPY ./default.svg /var/www/banners/default.svg -COPY ./nginx.conf /etc/nginx/conf.d/default.conf +COPY ./nginx.dev.conf /etc/nginx/conf.d/default.conf EXPOSE 80 diff --git a/nginx/Dockerfile.prod b/nginx/Dockerfile.prod new file mode 100644 index 0000000..6bc6370 --- /dev/null +++ b/nginx/Dockerfile.prod @@ -0,0 +1,8 @@ +FROM nginx:latest + +COPY ./default.svg /var/www/banners/default.svg + +COPY ./nginx.prod.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + diff --git a/nginx/default.svg b/nginx/default.svg.example similarity index 100% rename from nginx/default.svg rename to nginx/default.svg.example diff --git a/nginx/nginx.conf b/nginx/nginx.dev.conf similarity index 100% rename from nginx/nginx.conf rename to nginx/nginx.dev.conf diff --git a/nginx/nginx.prod.conf b/nginx/nginx.prod.conf new file mode 100644 index 0000000..14a2bed --- /dev/null +++ b/nginx/nginx.prod.conf @@ -0,0 +1,33 @@ +# --- HTTPS thorugh Cloudflare Origin Certificate --- +server { + listen 443 ssl; + server_name _; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + + root /var/www/; + + # --- API proxy --- + location = /banners { + proxy_pass http://api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + location ^~ /banners/preview { + proxy_pass http://api; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + # --- Static banners serving --- + location ^~ /banners/ { + try_files $uri.svg /banners/default; + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + add_header Last-Modified ""; + etag off; + add_header Pragma "no-cache" always; + add_header Expires "0" always; + add_header Surrogate-Control "no-store" always; + } +}