fix(deploy): coturn cert ownership + missing kms:Decrypt in Lambda po… #54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy backend | |
| # Image-based release: build the production server image in CI, publish an | |
| # immutable tag to ECR, then have EC2 pull that exact tag and restart the | |
| # container — no source checkout build / mediasoup recompile on the instance. | |
| # | |
| # Activates only when the ECR_REPOSITORY repo variable is set (see README | |
| # "Deployment"); until then both jobs skip cleanly so merges to main stay green. | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'server/**' | |
| - 'shared/**' | |
| - 'package.json' | |
| - 'package-lock.json' | |
| - '.dockerignore' | |
| - 'docker-compose.prod.yml' | |
| - '.github/workflows/deploy-backend.yml' | |
| workflow_dispatch: | |
| # Serialize production releases. Without this, two quick merges could build and | |
| # deploy concurrently and a slower older run could restart EC2 *after* a newer | |
| # one — silently rolling production back to the older SHA. Queue instead of | |
| # cancelling (cancel-in-progress: false) so an in-flight deploy is never | |
| # interrupted mid-restart; the newer run waits and deploys last. | |
| concurrency: | |
| group: production-deploy | |
| cancel-in-progress: false | |
| permissions: | |
| id-token: write # OIDC federation to assume the AWS deploy role | |
| contents: read | |
| env: | |
| AWS_REGION: ${{ vars.AWS_REGION }} | |
| ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }} | |
| jobs: | |
| build-and-push: | |
| name: Build & push image to ECR | |
| if: ${{ vars.ECR_REPOSITORY != '' }} | |
| runs-on: ubuntu-latest | |
| outputs: | |
| image: ${{ steps.meta.outputs.image }} | |
| registry: ${{ steps.login-ecr.outputs.registry }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Configure AWS credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Log in to Amazon ECR | |
| id: login-ecr | |
| uses: aws-actions/amazon-ecr-login@v2 | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Resolve immutable image tag | |
| id: meta | |
| run: | | |
| echo "image=${{ steps.login-ecr.outputs.registry }}/${ECR_REPOSITORY}:${GITHUB_SHA}" >> "$GITHUB_OUTPUT" | |
| # Build the runtime target explicitly — the Dockerfile's last stage is `smoke` | |
| # (which runs a mediasoup worker), so an unqualified build would ship that. | |
| - name: Build and push image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ./server/Dockerfile | |
| target: runtime | |
| push: true | |
| tags: | | |
| ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }} | |
| ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| deploy: | |
| name: Deploy image to EC2 | |
| needs: build-and-push | |
| if: ${{ vars.ECR_REPOSITORY != '' }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Pull image tag on EC2 and restart | |
| uses: appleboy/ssh-action@v1.2.5 | |
| env: | |
| SERVER_IMAGE: ${{ needs.build-and-push.outputs.image }} | |
| ECR_REGISTRY: ${{ needs.build-and-push.outputs.registry }} | |
| AWS_REGION: ${{ vars.AWS_REGION }} | |
| DEPLOY_SHA: ${{ github.sha }} | |
| with: | |
| host: ${{ secrets.EC2_HOST }} | |
| username: ubuntu | |
| key: ${{ secrets.EC2_SSH_KEY }} | |
| command_timeout: 10m | |
| envs: SERVER_IMAGE,ECR_REGISTRY,AWS_REGION,DEPLOY_SHA | |
| script: | | |
| set -euo pipefail | |
| # Authenticate the Docker daemon to ECR. The instance needs an IAM role | |
| # with ECR pull permissions and the AWS CLI installed. | |
| aws ecr get-login-password --region "$AWS_REGION" \ | |
| | docker login --username AWS --password-stdin "$ECR_REGISTRY" | |
| cd ~/ameet | |
| # Check out the EXACT commit that produced SERVER_IMAGE (detached HEAD) | |
| # rather than moving `main`, so the compose file + env contract used to | |
| # deploy always match the image tag being released — even if a newer | |
| # commit has already landed on main. | |
| git fetch --quiet origin main | |
| git checkout --quiet --force --detach "$DEPLOY_SHA" | |
| export SERVER_IMAGE | |
| # Pull the exact published tag and (re)start. `set -e` makes the deploy | |
| # fail fast if the pull or container start fails. | |
| docker compose -f docker-compose.prod.yml pull | |
| docker compose -f docker-compose.prod.yml up -d | |
| docker image prune -f | |
| - name: Health check | |
| run: | | |
| HEALTH_URL="${{ vars.HEALTH_URL || 'https://api.ameet.raja-dev.me/api/health' }}" | |
| for i in $(seq 1 12); do | |
| if curl -fsS "$HEALTH_URL" | grep -q '"ok":true'; then | |
| echo "Health check passed (attempt $i)" | |
| exit 0 | |
| fi | |
| echo "Attempt $i: API not healthy yet, retrying in 5s..." | |
| sleep 5 | |
| done | |
| echo "Health check failed: API did not return ok:true after 60s" | |
| exit 1 |