Skip to content

Commit ed45803

Browse files
committed
Switch to codedeploy
1 parent c6a0710 commit ed45803

26 files changed

+3661
-2234
lines changed
Lines changed: 388 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,388 @@
1+
name: CodeDeploy - Production
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
paths:
8+
- 'gefapi/**'
9+
- 'migrations/**'
10+
- 'docker-compose.yml'
11+
- 'docker-compose.prod.yml'
12+
- 'Dockerfile'
13+
- 'entrypoint.sh'
14+
- 'appspec.yml'
15+
- 'scripts/codedeploy/**'
16+
- '.github/workflows/codedeploy_production.yml'
17+
workflow_dispatch:
18+
inputs:
19+
force_deploy:
20+
description: 'Force deployment even if no changes detected'
21+
required: false
22+
default: 'false'
23+
type: choice
24+
options:
25+
- 'true'
26+
- 'false'
27+
28+
env:
29+
AWS_REGION: us-east-1
30+
APPLICATION_NAME: trendsearth-api
31+
DEPLOYMENT_GROUP: trendsearth-api-production
32+
ECR_REPOSITORY: trendsearth-api
33+
34+
concurrency:
35+
group: production-deployment
36+
cancel-in-progress: false
37+
38+
# Required for OIDC authentication with AWS
39+
permissions:
40+
id-token: write
41+
contents: read
42+
43+
jobs:
44+
build-and-push:
45+
name: Build and Push Docker Image
46+
runs-on: ubuntu-latest
47+
timeout-minutes: 30
48+
environment: production
49+
outputs:
50+
image-uri: ${{ steps.build-push.outputs.image-uri }}
51+
image-tag: ${{ steps.deployment-info.outputs.image-tag }}
52+
53+
steps:
54+
- name: Checkout code
55+
uses: actions/checkout@v4
56+
57+
- name: Get deployment info
58+
id: deployment-info
59+
run: |
60+
echo "commit-sha=${{ github.sha }}" >> $GITHUB_OUTPUT
61+
echo "short-sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
62+
echo "timestamp=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_OUTPUT
63+
echo "image-tag=production-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
64+
echo "branch=${{ github.ref_name }}" >> $GITHUB_OUTPUT
65+
66+
- name: Configure AWS credentials via OIDC
67+
uses: aws-actions/configure-aws-credentials@v4
68+
with:
69+
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
70+
role-session-name: GitHubActionsProductionBuild
71+
aws-region: ${{ env.AWS_REGION }}
72+
73+
- name: Login to Amazon ECR
74+
id: login-ecr
75+
uses: aws-actions/amazon-ecr-login@v2
76+
77+
- name: Set up Docker Buildx
78+
uses: docker/setup-buildx-action@v3
79+
80+
- name: Ensure ECR repository exists
81+
id: ecr-repo
82+
env:
83+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
84+
run: |
85+
REPO="${{ env.ECR_REPOSITORY }}"
86+
echo "repo-name=$REPO" >> $GITHUB_OUTPUT
87+
88+
if ! aws ecr describe-repositories --repository-names "$REPO" 2>/dev/null; then
89+
echo "Creating ECR repository: $REPO"
90+
aws ecr create-repository \
91+
--repository-name "$REPO" \
92+
--image-scanning-configuration scanOnPush=true \
93+
--image-tag-mutability MUTABLE
94+
else
95+
echo "ECR repository exists: $REPO"
96+
fi
97+
98+
- name: Build and push Docker image to ECR
99+
id: build-push
100+
env:
101+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
102+
IMAGE_TAG: ${{ steps.deployment-info.outputs.image-tag }}
103+
run: |
104+
IMAGE_URI="$ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG"
105+
106+
docker buildx build \
107+
--platform linux/amd64 \
108+
--cache-from=type=gha,scope=trendsearth-api-production \
109+
--cache-to=type=gha,mode=max,scope=trendsearth-api-production \
110+
--tag "$IMAGE_URI" \
111+
--tag "$ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:production-latest" \
112+
--tag "$ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:latest" \
113+
--push \
114+
.
115+
116+
echo "image-uri=$IMAGE_URI" >> $GITHUB_OUTPUT
117+
echo "✅ Docker image pushed to ECR: $IMAGE_URI"
118+
119+
deploy:
120+
name: Deploy to Production via CodeDeploy
121+
runs-on: ubuntu-latest
122+
timeout-minutes: 20
123+
environment: production
124+
needs: build-and-push
125+
126+
steps:
127+
- name: Checkout code
128+
uses: actions/checkout@v4
129+
130+
- name: Configure AWS credentials via OIDC
131+
uses: aws-actions/configure-aws-credentials@v4
132+
with:
133+
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
134+
role-session-name: GitHubActionsProductionDeploy
135+
aws-region: ${{ env.AWS_REGION }}
136+
137+
- name: Login to Amazon ECR
138+
id: login-ecr
139+
uses: aws-actions/amazon-ecr-login@v2
140+
141+
- name: Get deployment info
142+
id: deployment-info
143+
run: |
144+
echo "commit-sha=${{ github.sha }}" >> $GITHUB_OUTPUT
145+
echo "short-sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
146+
echo "timestamp=$(date +%Y%m%d-%H%M%S)" >> $GITHUB_OUTPUT
147+
148+
# Get AWS account ID for S3 bucket name
149+
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
150+
echo "s3-bucket=trendsearth-api-deployments-${ACCOUNT_ID}" >> $GITHUB_OUTPUT
151+
152+
- name: Get image references
153+
id: images
154+
env:
155+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
156+
run: |
157+
echo "api-image=${{ needs.build-and-push.outputs.image-uri }}" >> $GITHUB_OUTPUT
158+
echo "ecr-registry=$ECR_REGISTRY" >> $GITHUB_OUTPUT
159+
echo "✅ API image: ${{ needs.build-and-push.outputs.image-uri }}"
160+
161+
- name: Create environment file for deployment
162+
env:
163+
ECR_REGISTRY: ${{ steps.images.outputs.ecr-registry }}
164+
API_IMAGE: ${{ steps.images.outputs.api-image }}
165+
run: |
166+
# Generate prod.env with secrets from GitHub
167+
# This file is included in the deployment package and copied to the server
168+
# Uses prod.env to match docker-compose.prod.yml env_file reference
169+
cat > prod.env << EOF
170+
# Generated by GitHub Actions on $(date -u +"%Y-%m-%d %H:%M:%S UTC")
171+
# Commit: ${{ github.sha }}
172+
173+
# ECR Images (pre-built in CI)
174+
ECR_REGISTRY=$ECR_REGISTRY
175+
API_IMAGE=$API_IMAGE
176+
177+
# Environment
178+
ENVIRONMENT=production
179+
DEBUG=False
180+
TESTING=false
181+
182+
# Flask/API Configuration
183+
SECRET_KEY=${{ secrets.SECRET_KEY }}
184+
JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}
185+
186+
# Database Configuration
187+
DATABASE_URL=${{ secrets.PRODUCTION_DATABASE_URL }}
188+
189+
# Redis Configuration
190+
REDIS_URL=${{ secrets.PRODUCTION_REDIS_URL }}
191+
192+
# Rate Limiting
193+
RATE_LIMITING_ENABLED=${{ secrets.RATE_LIMITING_ENABLED || 'true' }}
194+
RATE_LIMIT_STORAGE_URI=${{ secrets.PRODUCTION_RATE_LIMIT_STORAGE_URI }}
195+
196+
# Google Earth Engine
197+
GEE_SERVICE_ACCOUNT_JSON=${{ secrets.GEE_SERVICE_ACCOUNT_JSON }}
198+
199+
# Rollbar Error Tracking
200+
ROLLBAR_SCRIPT_TOKEN=${{ secrets.ROLLBAR_SCRIPT_TOKEN }}
201+
ROLLBAR_ENV=production
202+
203+
# API URLs
204+
API_URL=${{ secrets.PRODUCTION_API_URL || 'https://api.trends.earth' }}
205+
206+
# S3 Configuration
207+
S3_BUCKET_NAME=${{ secrets.PRODUCTION_S3_BUCKET_NAME }}
208+
AWS_ACCESS_KEY_ID=${{ secrets.PRODUCTION_AWS_ACCESS_KEY_ID }}
209+
AWS_SECRET_ACCESS_KEY=${{ secrets.PRODUCTION_AWS_SECRET_ACCESS_KEY }}
210+
211+
# SMTP Configuration
212+
SMTP_HOST=${{ secrets.SMTP_HOST }}
213+
SMTP_PORT=${{ secrets.SMTP_PORT }}
214+
SMTP_USER=${{ secrets.SMTP_USER }}
215+
SMTP_PASSWORD=${{ secrets.SMTP_PASSWORD }}
216+
SMTP_FROM=${{ secrets.SMTP_FROM }}
217+
218+
# Deployment info
219+
GIT_REVISION=${{ github.sha }}
220+
GIT_BRANCH=${{ github.ref_name }}
221+
DEPLOYMENT_ENVIRONMENT=production
222+
EOF
223+
224+
echo "✅ Created prod.env with $(wc -l < prod.env) lines"
225+
226+
- name: Update appspec.yml for production
227+
run: |
228+
# Modify appspec.yml to use production-specific destination
229+
# This prevents race conditions when staging and production deploy simultaneously
230+
sed -i 's|destination: /opt/trendsearth-api|destination: /opt/trendsearth-api-production|g' appspec.yml
231+
echo "Updated appspec.yml destination to /opt/trendsearth-api-production"
232+
cat appspec.yml
233+
234+
- name: Create deployment package
235+
id: package
236+
run: |
237+
# Create a deployment archive
238+
REVISION_FILE="production-${{ steps.deployment-info.outputs.timestamp }}-${{ steps.deployment-info.outputs.short-sha }}.zip"
239+
echo "revision-file=$REVISION_FILE" >> $GITHUB_OUTPUT
240+
241+
# Include only files needed for deployment (not source code - that's in the Docker image)
242+
zip -r "$REVISION_FILE" \
243+
appspec.yml \
244+
prod.env \
245+
docker-compose.yml \
246+
docker-compose.prod.yml \
247+
scripts/codedeploy/ \
248+
entrypoint.sh \
249+
-x "*.pyc" \
250+
-x "__pycache__/*" \
251+
-x ".git/*" \
252+
-x ".venv/*" \
253+
-x "*.log"
254+
255+
echo "Created deployment package: $REVISION_FILE ($(du -h $REVISION_FILE | cut -f1))"
256+
257+
- name: Upload deployment package to S3
258+
id: upload
259+
run: |
260+
BUCKET="${{ steps.deployment-info.outputs.s3-bucket }}"
261+
KEY="production/${{ steps.package.outputs.revision-file }}"
262+
263+
aws s3 cp "${{ steps.package.outputs.revision-file }}" "s3://$BUCKET/$KEY"
264+
265+
echo "s3-key=$KEY" >> $GITHUB_OUTPUT
266+
echo "Uploaded to s3://$BUCKET/$KEY"
267+
268+
- name: Stop any in-progress deployments
269+
run: |
270+
echo "🔍 Checking for in-progress deployments..."
271+
272+
# Get list of active deployments
273+
ACTIVE_DEPLOYMENTS=$(aws deploy list-deployments \
274+
--application-name ${{ env.APPLICATION_NAME }} \
275+
--deployment-group-name ${{ env.DEPLOYMENT_GROUP }} \
276+
--include-only-statuses "InProgress" "Queued" "Created" \
277+
--query 'deployments' \
278+
--output text 2>/dev/null || echo "")
279+
280+
if [ -n "$ACTIVE_DEPLOYMENTS" ] && [ "$ACTIVE_DEPLOYMENTS" != "None" ]; then
281+
for DEPLOYMENT_ID in $ACTIVE_DEPLOYMENTS; do
282+
echo "⏹️ Stopping deployment: $DEPLOYMENT_ID"
283+
aws deploy stop-deployment \
284+
--deployment-id "$DEPLOYMENT_ID" \
285+
--auto-rollback-enabled 2>/dev/null || true
286+
done
287+
288+
# Wait for deployments to stop
289+
echo "⏳ Waiting for deployments to stop..."
290+
sleep 15
291+
echo "✅ Previous deployments stopped"
292+
else
293+
echo "✅ No active deployments found"
294+
fi
295+
296+
- name: Create CodeDeploy deployment
297+
id: deploy
298+
run: |
299+
BUCKET="${{ steps.deployment-info.outputs.s3-bucket }}"
300+
KEY="${{ steps.upload.outputs.s3-key }}"
301+
302+
DEPLOYMENT_ID=$(aws deploy create-deployment \
303+
--application-name ${{ env.APPLICATION_NAME }} \
304+
--deployment-group-name ${{ env.DEPLOYMENT_GROUP }} \
305+
--s3-location bucket=$BUCKET,key=$KEY,bundleType=zip \
306+
--deployment-config-name CodeDeployDefault.OneAtATime \
307+
--description "Production deployment from ${{ github.ref_name }}" \
308+
--file-exists-behavior OVERWRITE \
309+
--query 'deploymentId' \
310+
--output text)
311+
312+
echo "deployment-id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT
313+
echo "🚀 Created deployment: $DEPLOYMENT_ID"
314+
315+
- name: Wait for deployment to complete
316+
run: |
317+
DEPLOYMENT_ID="${{ steps.deploy.outputs.deployment-id }}"
318+
echo "⏳ Waiting for deployment $DEPLOYMENT_ID to complete..."
319+
320+
# Wait for deployment (max 30 minutes)
321+
aws deploy wait deployment-successful \
322+
--deployment-id "$DEPLOYMENT_ID" \
323+
--cli-read-timeout 1800 || {
324+
# Get deployment info if failed
325+
STATUS=$(aws deploy get-deployment --deployment-id "$DEPLOYMENT_ID" \
326+
--query 'deploymentInfo.status' --output text)
327+
328+
echo "❌ Deployment failed with status: $STATUS"
329+
330+
# Get deployment events for debugging
331+
echo "📋 Deployment events:"
332+
aws deploy list-deployment-targets --deployment-id "$DEPLOYMENT_ID" \
333+
--query 'targetIds' --output text | while read TARGET_ID; do
334+
echo "Target: $TARGET_ID"
335+
aws deploy get-deployment-target \
336+
--deployment-id "$DEPLOYMENT_ID" \
337+
--target-id "$TARGET_ID" \
338+
--query 'deploymentTarget.instanceTarget.lifecycleEvents[*].{Event:lifecycleEventName,Status:status,Message:diagnostics.message}' \
339+
--output table
340+
done
341+
342+
exit 1
343+
}
344+
345+
echo "✅ Deployment completed successfully!"
346+
347+
- name: Verify deployment
348+
run: |
349+
echo "🔍 Verifying deployment..."
350+
351+
# Get deployment status
352+
DEPLOYMENT_ID="${{ steps.deploy.outputs.deployment-id }}"
353+
aws deploy get-deployment \
354+
--deployment-id "$DEPLOYMENT_ID" \
355+
--query 'deploymentInfo.{Status:status,CreateTime:createTime,CompleteTime:completeTime}' \
356+
--output table
357+
358+
- name: Deployment notification
359+
if: always()
360+
run: |
361+
if [ "${{ job.status }}" = "success" ]; then
362+
echo "✅ Production deployment successful!"
363+
echo "🌐 Site: https://api.trends.earth"
364+
echo "📦 Commit: ${{ github.sha }}"
365+
echo "🚀 Deployment ID: ${{ steps.deploy.outputs.deployment-id }}"
366+
else
367+
echo "❌ Production deployment failed!"
368+
echo "📦 Commit: ${{ github.sha }}"
369+
echo "🚀 Deployment ID: ${{ steps.deploy.outputs.deployment-id }}"
370+
exit 1
371+
fi
372+
373+
- name: Notify Rollbar of deployment
374+
if: success()
375+
run: |
376+
# Notify Rollbar of successful deployment (if token is configured)
377+
if [ -n "${{ secrets.ROLLBAR_ACCESS_TOKEN }}" ]; then
378+
curl -X POST 'https://api.rollbar.com/api/1/deploy/' \
379+
-F access_token="${{ secrets.ROLLBAR_ACCESS_TOKEN }}" \
380+
-F environment="production" \
381+
-F revision="${{ github.sha }}" \
382+
-F local_username="${{ github.actor }}" \
383+
-F comment="Deployed via CodeDeploy from ${{ github.ref_name }}" \
384+
-F status=succeeded
385+
echo "📊 Rollbar deployment notification sent"
386+
else
387+
echo "⚠️ ROLLBAR_ACCESS_TOKEN not configured, skipping deployment notification"
388+
fi

0 commit comments

Comments
 (0)