CD — Terraform Apply + Deploy/Destroy (ECS) #92
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: CD — Terraform Apply + Deploy/Destroy (ECS) | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: "apply: deploy image and scale to 1; destroy: cleanup everything" | |
| required: true | |
| type: choice | |
| options: [apply, destroy] | |
| default: apply | |
| imageTag: | |
| description: "Image tag to deploy (default: latest)" | |
| required: false | |
| type: string | |
| env: | |
| AWS_REGION: us-east-1 | |
| CLUSTER_NAME: ecs-demo-cluster | |
| SERVICE_NAME: ecs-demo-svc | |
| ECR_REPOSITORY: ecs-demo-app | |
| LOG_GROUP: /ecs/ecs-demo | |
| permissions: | |
| id-token: write | |
| contents: read | |
| concurrency: | |
| group: cd-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| cd: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup jq | |
| run: sudo apt-get update && sudo apt-get install -y jq | |
| - name: Configure AWS (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: arn:aws:iam::097635932419:role/github-actions-ecs-role | |
| aws-region: ${{ env.AWS_REGION }} | |
| - name: Package Lambda zips | |
| run: | | |
| set -e | |
| mkdir -p infra | |
| if [ -f wake/lambda_function.py ]; then | |
| (cd wake && zip -r ../infra/wake.zip lambda_function.py >/dev/null) | |
| else | |
| echo "wake/lambda_function.py not found, skipping wake.zip" | |
| fi | |
| if [ -f autosleep/auto_sleep.py ]; then | |
| (cd autosleep && zip -r ../infra/sleep.zip auto_sleep.py >/dev/null) | |
| else | |
| echo "autosleep/auto_sleep.py not found, skipping sleep.zip" | |
| fi | |
| ls -l infra/*.zip || true | |
| - name: Setup Terraform | |
| uses: hashicorp/setup-terraform@v3 | |
| with: | |
| terraform_version: 1.7.5 | |
| - name: Terraform init (infra) | |
| working-directory: infra | |
| run: terraform init -input=false | |
| - name: Terraform import ECR if exists (before apply) | |
| if: ${{ inputs.mode == 'apply' }} | |
| working-directory: infra | |
| env: | |
| AWS_PAGER: "" | |
| run: | | |
| if aws ecr describe-repositories --repository-names "${{ env.ECR_REPOSITORY }}" --region "${{ env.AWS_REGION }}" >/dev/null 2>&1; then | |
| terraform state show aws_ecr_repository.this >/dev/null 2>&1 || terraform import aws_ecr_repository.this "${{ env.ECR_REPOSITORY }}" | |
| else | |
| echo "ECR not found — Terraform will create it." | |
| fi | |
| - name: Terraform apply (infra) | |
| if: ${{ inputs.mode == 'apply' }} | |
| working-directory: infra | |
| run: terraform apply -auto-approve -input=false | |
| - name: Compute ECR URL & Tag | |
| id: ecr | |
| run: | | |
| ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) | |
| ECR_URL="${ACCOUNT_ID}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.ECR_REPOSITORY }}" | |
| TAG="${{ inputs.imageTag }}" | |
| if [ -z "$TAG" ]; then TAG="latest"; fi | |
| echo "ECR_URL=$ECR_URL" >> $GITHUB_OUTPUT | |
| echo "TAG=$TAG" >> $GITHUB_OUTPUT | |
| - name: Assert image tag exists in ECR | |
| if: ${{ inputs.mode == 'apply' }} | |
| env: | |
| AWS_PAGER: "" | |
| run: | | |
| TAG="${{ steps.ecr.outputs.TAG }}" | |
| COUNT=$(aws ecr list-images \ | |
| --repository-name "${{ env.ECR_REPOSITORY }}" \ | |
| --region "${{ env.AWS_REGION }}" \ | |
| --filter tagStatus=TAGGED \ | |
| --query "length(imageIds[?imageTag=='${TAG}'])" \ | |
| --output text) | |
| if [ "$COUNT" -eq 0 ]; then | |
| echo "❌ Image tag not found in ECR: ${{ steps.ecr.outputs.ECR_URL }}:${TAG}" | |
| exit 1 | |
| else | |
| echo "✅ Found image tag: ${{ steps.ecr.outputs.ECR_URL }}:${TAG}" | |
| fi | |
| - name: Scale to 0 and wait (destroy) | |
| if: ${{ inputs.mode == 'destroy' }} | |
| continue-on-error: true | |
| run: | | |
| aws ecs update-service --cluster "${{ env.CLUSTER_NAME }}" --service "${{ env.SERVICE_NAME }}" --desired-count 0 --region "${{ env.AWS_REGION }}" || true | |
| aws ecs wait services-stable --cluster "${{ env.CLUSTER_NAME }}" --services "${{ env.SERVICE_NAME }}" --region "${{ env.AWS_REGION }}" || true | |
| echo "✅ Service scaled to 0." | |
| - name: Delete CloudWatch Log Group (destroy) | |
| if: ${{ inputs.mode == 'destroy' }} | |
| continue-on-error: true | |
| run: | | |
| aws logs delete-log-group --log-group-name "${{ env.LOG_GROUP }}" --region "${{ env.AWS_REGION }}" || true | |
| echo "🧹 CloudWatch log group deleted." | |
| - name: Terraform destroy (full cleanup) | |
| if: ${{ inputs.mode == 'destroy' }} | |
| working-directory: infra | |
| run: terraform destroy -auto-approve -input=false || true | |
| - name: ECR fallback cleanup (destroy) | |
| if: ${{ inputs.mode == 'destroy' }} | |
| continue-on-error: true | |
| env: | |
| AWS_PAGER: "" | |
| run: | | |
| if aws ecr describe-repositories --repository-names "${{ env.ECR_REPOSITORY }}" --region "${{ env.AWS_REGION }}" >/dev/null 2>&1; then | |
| IMAGES=$(aws ecr list-images --repository-name "${{ env.ECR_REPOSITORY }}" --region "${{ env.AWS_REGION }}" --query 'imageIds[*]' --output json || echo "[]") | |
| if [ "$IMAGES" != "[]" ]; then | |
| aws ecr batch-delete-image --repository-name "${{ env.ECR_REPOSITORY }}" --region "${{ env.AWS_REGION }}" --image-ids "$IMAGES" || true | |
| fi | |
| aws ecr delete-repository --repository-name "${{ env.ECR_REPOSITORY }}" --region "${{ env.AWS_REGION }}" --force || true | |
| echo "🗑️ Ensured ECR repo removed." | |
| fi | |
| - name: Verify state is empty (destroy) | |
| if: ${{ inputs.mode == 'destroy' }} | |
| working-directory: infra | |
| run: terraform state list || echo "✅ State is empty" | |
| - name: Get current TaskDefinition ARN | |
| if: ${{ inputs.mode == 'apply' }} | |
| id: svc | |
| run: | | |
| TD=$(aws ecs describe-services --cluster "${{ env.CLUSTER_NAME }}" --services "${{ env.SERVICE_NAME }}" --region "${{ env.AWS_REGION }}" --query "services[0].taskDefinition" --output text) | |
| echo "td=$TD" >> $GITHUB_OUTPUT | |
| - name: Download full TaskDefinition JSON | |
| if: ${{ inputs.mode == 'apply' }} | |
| run: | | |
| aws ecs describe-task-definition --task-definition "${{ steps.svc.outputs.td }}" --region "${{ env.AWS_REGION }}" --query "taskDefinition" > taskdef.json | |
| - name: Update image in TaskDefinition | |
| if: ${{ inputs.mode == 'apply' }} | |
| env: | |
| IMG: ${{ steps.ecr.outputs.ECR_URL }}:${{ steps.ecr.outputs.TAG }} | |
| run: | | |
| jq --arg IMG "$IMG" ' | |
| del(.revision,.status,.taskDefinitionArn,.requiresAttributes,.compatibilities,.registeredBy,.registeredAt,.deregisteredAt) | |
| | .containerDefinitions = (.containerDefinitions | map(if .name=="app" then .image=$IMG else . end)) | |
| ' taskdef.json > register.json | |
| echo "Using image: $IMG" | |
| - name: Register new TaskDefinition | |
| if: ${{ inputs.mode == 'apply' }} | |
| id: register | |
| run: | | |
| NEW_TD=$(aws ecs register-task-definition --region "${{ env.AWS_REGION }}" --cli-input-json file://register.json --query "taskDefinition.taskDefinitionArn" --output text) | |
| echo "new=$NEW_TD" >> $GITHUB_OUTPUT | |
| - name: Update Service & wait | |
| if: ${{ inputs.mode == 'apply' }} | |
| run: | | |
| aws ecs update-service --cluster "${{ env.CLUSTER_NAME }}" --service "${{ env.SERVICE_NAME }}" --task-definition "${{ steps.register.outputs.new }}" --desired-count 1 --region "${{ env.AWS_REGION }}" | |
| aws ecs wait services-stable --cluster "${{ env.CLUSTER_NAME }}" --services "${{ env.SERVICE_NAME }}" --region "${{ env.AWS_REGION }}" | |
| echo "✅ Deployed and service is stable." |