1+ name : Build and Push to ECR
2+
3+ on :
4+ push :
5+ branches : [main, staging]
6+
7+ permissions :
8+ id-token : write
9+ contents : read
10+
11+ jobs :
12+ build-and-push-ecr :
13+ strategy :
14+ fail-fast : false
15+ matrix :
16+ include :
17+ - dockerfile : ./docker/app.Dockerfile
18+ ecr_repo_secret : ECR_APP
19+ service_type : app
20+ - dockerfile : ./docker/db.Dockerfile
21+ ecr_repo_secret : ECR_MIGRATIONS
22+ service_type : core
23+ - dockerfile : ./docker/realtime.Dockerfile
24+ ecr_repo_secret : ECR_REALTIME
25+ service_type : monitoring
26+ runs-on : ubuntu-latest
27+
28+ steps :
29+ - name : Checkout repository
30+ uses : actions/checkout@v4
31+
32+ - name : Configure AWS credentials
33+ uses : aws-actions/configure-aws-credentials@v4
34+ with :
35+ role-to-assume : ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
36+ aws-region : ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
37+
38+ - name : Login to Amazon ECR
39+ id : login-ecr
40+ uses : aws-actions/amazon-ecr-login@v2
41+
42+ - name : Set up Docker Buildx
43+ uses : docker/setup-buildx-action@v3
44+
45+ - name : Generate image tags
46+ id : meta
47+ run : |
48+ ECR_REGISTRY="${{ steps.login-ecr.outputs.registry }}"
49+ ECR_REPO="${{ secrets[matrix.ecr_repo_secret] }}"
50+
51+ # Simple tagging: :latest for main, :staging for staging
52+ if [ "${{ github.ref }}" = "refs/heads/main" ]; then
53+ TAG="latest"
54+ else
55+ TAG="staging"
56+ fi
57+
58+ FULL_IMAGE="${ECR_REGISTRY}/${ECR_REPO}:${TAG}"
59+
60+ echo "tag=$TAG" >> $GITHUB_OUTPUT
61+ echo "full_image=$FULL_IMAGE" >> $GITHUB_OUTPUT
62+
63+ - name : Build and push Docker image
64+ uses : docker/build-push-action@v6
65+ with :
66+ context : .
67+ file : ${{ matrix.dockerfile }}
68+ push : true
69+ tags : ${{ steps.meta.outputs.full_image }}
70+ platforms : linux/amd64
71+ cache-from : type=gha,scope=build-ecr-${{ matrix.service_type }}
72+ cache-to : type=gha,mode=max,scope=build-ecr-${{ matrix.service_type }}
73+ provenance : false
74+ sbom : false
75+
76+ update-ecs-services :
77+ needs : build-and-push-ecr
78+ runs-on : ubuntu-latest
79+ strategy :
80+ fail-fast : false
81+ matrix :
82+ stack_type : [APP, CORE, MONITORING]
83+
84+ steps :
85+ - name : Configure AWS credentials
86+ uses : aws-actions/configure-aws-credentials@v4
87+ with :
88+ role-to-assume : ${{ github.ref == 'refs/heads/main' && secrets.AWS_ROLE_TO_ASSUME || secrets.STAGING_AWS_ROLE_TO_ASSUME }}
89+ aws-region : ${{ github.ref == 'refs/heads/main' && secrets.AWS_REGION || secrets.STAGING_AWS_REGION }}
90+
91+ - name : Login to Amazon ECR
92+ id : login-ecr
93+ uses : aws-actions/amazon-ecr-login@v2
94+
95+ - name : Determine stack and image details
96+ id : stack
97+ run : |
98+ ECR_REGISTRY="${{ steps.login-ecr.outputs.registry }}"
99+
100+ # Determine stack prefix and tag based on environment
101+ if [ "${{ github.ref }}" = "refs/heads/main" ]; then
102+ STACK_PREFIX="PROD"
103+ TAG="latest"
104+ else
105+ STACK_PREFIX="STAGING"
106+ TAG="staging"
107+ fi
108+
109+ # Map stack type to ECR repo and get stack name
110+ case "${{ matrix.stack_type }}" in
111+ APP)
112+ STACK_NAME_VAR="${STACK_PREFIX}_APP_STACK"
113+ ECR_REPO="${{ secrets.ECR_APP }}"
114+ ;;
115+ CORE)
116+ STACK_NAME_VAR="${STACK_PREFIX}_CORE_STACK"
117+ ECR_REPO="${{ secrets.ECR_MIGRATIONS }}"
118+ ;;
119+ MONITORING)
120+ STACK_NAME_VAR="${STACK_PREFIX}_MONITORING_STACK"
121+ ECR_REPO="${{ secrets.ECR_REALTIME }}"
122+ ;;
123+ esac
124+
125+ # Get stack name from secrets
126+ STACK_NAME=$(eval echo \${{ secrets.${STACK_NAME_VAR} }})
127+
128+ # Full image URI with simple tag
129+ IMAGE_URI="${ECR_REGISTRY}/${ECR_REPO}:${TAG}"
130+
131+ echo "name=$STACK_NAME" >> $GITHUB_OUTPUT
132+ echo "image=$IMAGE_URI" >> $GITHUB_OUTPUT
133+
134+ - name : Get ECS services from stack
135+ id : ecs-services
136+ run : |
137+ # Get all ECS services from the stack
138+ SERVICES=$(aws cloudformation describe-stack-resources \
139+ --stack-name "${{ steps.stack.outputs.name }}" \
140+ --query "StackResources[?ResourceType=='AWS::ECS::Service'].PhysicalResourceId" \
141+ --output text 2>/dev/null || echo "")
142+
143+ if [ -z "$SERVICES" ]; then
144+ echo "No ECS services found in stack ${{ steps.stack.outputs.name }}"
145+ echo "services=" >> $GITHUB_OUTPUT
146+ else
147+ echo "Found services: $SERVICES"
148+ echo "services=$SERVICES" >> $GITHUB_OUTPUT
149+ fi
150+
151+ - name : Update ECS services
152+ if : steps.ecs-services.outputs.services != ''
153+ run : |
154+ SERVICES="${{ steps.ecs-services.outputs.services }}"
155+
156+ for SERVICE_ARN in $SERVICES; do
157+ echo "Updating service: $SERVICE_ARN"
158+
159+ # Extract cluster name from service ARN
160+ CLUSTER_NAME=$(echo $SERVICE_ARN | cut -d'/' -f2)
161+ SERVICE_NAME=$(echo $SERVICE_ARN | cut -d'/' -f3)
162+
163+ # Get the current task definition
164+ TASK_DEF_ARN=$(aws ecs describe-services \
165+ --cluster "$CLUSTER_NAME" \
166+ --services "$SERVICE_NAME" \
167+ --query "services[0].taskDefinition" \
168+ --output text)
169+
170+ # Get the task definition details
171+ TASK_DEF=$(aws ecs describe-task-definition \
172+ --task-definition "$TASK_DEF_ARN" \
173+ --query "taskDefinition")
174+
175+ # Update the image in the task definition
176+ # For Ubuntu ECS, container definitions may have multiple containers
177+ NEW_TASK_DEF=$(echo "$TASK_DEF" | jq --arg IMAGE "${{ steps.stack.outputs.image }}" \
178+ '.containerDefinitions |= map(
179+ if .essential == true then
180+ .image = $IMAGE
181+ else . end
182+ ) |
183+ del(.taskDefinitionArn) |
184+ del(.revision) |
185+ del(.status) |
186+ del(.requiresAttributes) |
187+ del(.compatibilities) |
188+ del(.registeredAt) |
189+ del(.registeredBy)')
190+
191+ # Register new task definition
192+ NEW_TASK_ARN=$(aws ecs register-task-definition \
193+ --cli-input-json "$NEW_TASK_DEF" \
194+ --query "taskDefinition.taskDefinitionArn" \
195+ --output text)
196+
197+ echo "Registered new task definition: $NEW_TASK_ARN"
198+
199+ # Update service with new task definition
200+ aws ecs update-service \
201+ --cluster "$CLUSTER_NAME" \
202+ --service "$SERVICE_NAME" \
203+ --task-definition "$NEW_TASK_ARN" \
204+ --force-new-deployment
205+
206+ echo "Service update initiated for $SERVICE_NAME"
207+ done
208+
209+ - name : Wait for service stability
210+ if : steps.ecs-services.outputs.services != ''
211+ run : |
212+ SERVICES="${{ steps.ecs-services.outputs.services }}"
213+
214+ for SERVICE_ARN in $SERVICES; do
215+ CLUSTER_NAME=$(echo $SERVICE_ARN | cut -d'/' -f2)
216+ SERVICE_NAME=$(echo $SERVICE_ARN | cut -d'/' -f3)
217+
218+ echo "Waiting for service $SERVICE_NAME to stabilize..."
219+
220+ # Wait up to 30 minutes for service to stabilize
221+ ATTEMPTS=0
222+ MAX_ATTEMPTS=120
223+ while [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do
224+ DEPLOYMENT_STATUS=$(aws ecs describe-services \
225+ --cluster "$CLUSTER_NAME" \
226+ --services "$SERVICE_NAME" \
227+ --query "services[0].deployments[?status=='PRIMARY'].rolloutState" \
228+ --output text)
229+
230+ if [ "$DEPLOYMENT_STATUS" = "COMPLETED" ]; then
231+ echo "✅ Service $SERVICE_NAME updated successfully!"
232+ break
233+ fi
234+
235+ echo "Deployment status: $DEPLOYMENT_STATUS (attempt $((ATTEMPTS+1))/$MAX_ATTEMPTS)"
236+ sleep 15
237+ ATTEMPTS=$((ATTEMPTS+1))
238+ done
239+
240+ if [ $ATTEMPTS -eq $MAX_ATTEMPTS ]; then
241+ echo "⚠️ Service $SERVICE_NAME did not stabilize within timeout"
242+ fi
243+ done
0 commit comments