Last active
November 3, 2022 15:54
-
-
Save statico/82499d00dbaa19556b38476411b900f4 to your computer and use it in GitHub Desktop.
Push-to-deploy architecture using AWS Fargate and GitHub Actions
This file contains 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
# See: | |
# - https://github.com/aws-actions/amazon-ecs-deploy-task-definition | |
name: build-and-deploy | |
on: | |
push: | |
branches: main | |
# The idea here is to update whatever is already in place on AWS. If we need to | |
# modify the ECS task definition, we should do it on ECS. Let's save | |
# declarative state for something like TerraForm when we get to that point. | |
env: | |
AWS_REGION: us-east-2 | |
ECR_REGISTRY: 000000000.dkr.ecr.us-east-2.amazonaws.com | |
ECR_REPOSITORY: app | |
ECS_CLUSTER: staging | |
ECS_SERVICE: app | |
ECS_TASK: app-staging | |
ECS_CONTAINER_NAME: app-container | |
IMAGE_TAG: ${{ github.sha }} | |
jobs: | |
build: | |
runs-on: ubuntu-latest | |
steps: | |
- name: Configure AWS credentials | |
uses: aws-actions/configure-aws-credentials@v1 | |
with: | |
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
aws-region: ${{ env.AWS_REGION }} | |
- name: Login to Amazon ECR | |
id: login-ecr | |
uses: aws-actions/amazon-ecr-login@v1 | |
- uses: actions/checkout@v2 | |
- name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v1.6.0 | |
- name: Build Docker image | |
uses: docker/build-push-action@v2.7.0 | |
with: | |
context: . | |
load: true | |
build-args: | | |
BUILD_ID=${{ github.sha }} | |
tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} | |
cache-from: type=gha,scope=v5 | |
cache-to: type=gha,mode=max,scope=v5 | |
- name: Push image to ECR | |
run: docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG | |
- name: Download task definition | |
if: github.ref == 'refs/heads/main' | |
run: | | |
aws ecs describe-task-definition --task-definition $ECS_TASK --query taskDefinition > task-definition.json | |
- name: Fill in the new image ID in the Amazon ECS task definition | |
if: github.ref == 'refs/heads/main' | |
id: task-def | |
uses: aws-actions/amazon-ecs-render-task-definition@v1 | |
with: | |
task-definition: task-definition.json | |
container-name: ${{ env.ECS_CONTAINER_NAME }} | |
image: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} | |
- name: Deploy to Amazon ECS | |
if: github.ref == 'refs/heads/main' | |
uses: aws-actions/amazon-ecs-deploy-task-definition@v1 | |
with: | |
task-definition: ${{ steps.task-def.outputs.task-definition }} | |
cluster: ${{ env.ECS_CLUSTER }} | |
service: ${{ env.ECS_SERVICE }} | |
wait-for-service-stability: false | |
- name: Report Status | |
if: github.ref == 'refs/heads/main' | |
uses: ravsamhq/notify-slack-action@1.3.1 | |
with: | |
status: ${{ job.status }} | |
notify_when: "success,failure,warnings" | |
notification_title: null | |
footer: null | |
env: | |
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} |
This file contains 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
# ------------------------------ | |
# * Dependencies | |
FROM node:16.13.0-alpine3.14 AS deps | |
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. | |
RUN apk add --no-cache libc6-compat | |
WORKDIR /app | |
COPY package.json yarn.lock ./ | |
RUN yarn install --frozen-lockfile | |
# ------------------------------ | |
# * Builder | |
FROM node:16.13.0-alpine3.14 AS builder | |
WORKDIR /app | |
COPY --from=deps /app/node_modules ./node_modules | |
COPY . . | |
ARG BUILD_ID=unknown | |
ENV NEXT_PUBLIC_BUILD_ID=$BUILD_ID | |
ENV NEXT_TELEMETRY_DISABLED=1 | |
RUN yarn lint && yarn prettier:check | |
RUN NEXT_PUBLIC_BUILD_TIME="$(date -R)" NODE_OPTIONS="--max-old-space-size=8192" yarn build && yarn install --production --ignore-scripts --prefer-offline | |
# ------------------------------ | |
# * Production | |
FROM node:16.13.0-alpine3.14 AS runner | |
RUN apk add --no-cache vips-tools | |
WORKDIR /app | |
RUN addgroup -g 1001 -S nodejs | |
RUN adduser -S nextjs -u 1001 | |
COPY --from=builder /app/knexfile.ts ./ | |
COPY --from=builder /app/runtimeConfig.js ./ | |
COPY --from=builder /app/next.config.js ./ | |
COPY --from=builder /app/migrations ./migrations | |
COPY --from=builder /app/email-templates ./email-templates | |
COPY --from=builder /app/lib ./lib | |
COPY --from=builder /app/public ./public | |
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next | |
COPY --from=builder /app/node_modules ./node_modules | |
COPY --from=builder /app/package.json ./package.json | |
USER nextjs | |
EXPOSE 3000 | |
ENV PORT=3000 | |
ARG BUILD_ID=unknown | |
ENV NEXT_PUBLIC_BUILD_ID=$BUILD_ID | |
ENV NODE_ENV=production | |
ENV NEXT_TELEMETRY_DISABLED=1 | |
CMD ["yarn", "start"] |
This file contains 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: pr | |
on: | |
pull_request: | |
branches: main | |
env: | |
ECR_REGISTRY: 000000000.dkr.ecr.us-east-2.amazonaws.com | |
ECR_REPOSITORY: app | |
IMAGE_TAG: ${{ github.sha }} | |
jobs: | |
build: | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/checkout@v2 | |
- name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v1.6.0 | |
- name: Build Docker image | |
uses: docker/build-push-action@v2.7.0 | |
with: | |
context: . | |
load: true | |
build-args: | | |
BUILD_ID=${{ github.sha }} | |
tags: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} | |
cache-from: type=gha,scope=v5 | |
cache-to: type=gha,mode=max,scope=v5 |
This file contains 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
#!/usr/bin/env node | |
const { randomUUID } = require("crypto") | |
const { writeFileSync } = require("fs") | |
const { exec, tempdir } = require("shelljs") | |
const aws = async (cmd) => { | |
const output = exec(`aws --output json ${cmd}`, { silent: true, exit: true }) | |
try { | |
return JSON.parse(output.stdout) | |
} catch (err) { | |
console.error(output.stdout + output.stderr) | |
process.exit(1) | |
} | |
} | |
const getStagingImage = async () => { | |
const service = await aws( | |
`ecs describe-services --cluster staging --service app` | |
) | |
const app = service.services.find((s) => s.serviceName === "app") | |
const arn = app.taskDefinition | |
console.log(`Staging task definition: ${arn}`) | |
const taskDef = await aws( | |
`ecs describe-task-definition --task-definition ${arn}` | |
) | |
const containers = taskDef.taskDefinition.containerDefinitions | |
const container = containers.find((c) => c.name === "app-container") | |
const image = container.image | |
console.log(`Image: ${image}\n`) | |
const hash = image.match(/(\w+)$/)[0] | |
exec(`git fetch`) | |
exec(`git log -n 1 ${hash}`) | |
return image | |
} | |
const promoteImageToProd = async (image) => { | |
const { taskDefinitionArns } = await aws(`ecs list-task-definitions`) | |
const latest = taskDefinitionArns | |
.filter((x) => /\/app-prod:\d+$/.test(x)) | |
.sort((a, b) => { | |
const ax = Number(a.match(/:(\d+)$/)[1]) | |
const bx = Number(b.match(/:(\d+)$/)[1]) | |
return bx - ax | |
})[0] | |
const taskDef = await aws( | |
`ecs describe-task-definition --task-definition ${latest}` | |
) | |
const containers = taskDef.taskDefinition.containerDefinitions | |
const container = containers.find((c) => c.name === "app-container") | |
if (container.image === image) { | |
console.log("Image is already up to date. Making sure prod is updated.") | |
const name = | |
taskDef.taskDefinition.family + ":" + taskDef.taskDefinition.revision | |
await aws( | |
`ecs update-service --cluster prod --service app --task-definition ${name}` | |
) | |
return | |
} | |
container.image = image | |
delete taskDef.taskDefinition.compatibilities | |
delete taskDef.taskDefinition.registeredAt | |
delete taskDef.taskDefinition.registeredBy | |
delete taskDef.taskDefinition.requiresAttributes | |
delete taskDef.taskDefinition.revision | |
delete taskDef.taskDefinition.status | |
delete taskDef.taskDefinition.taskDefinitionArn | |
const path = tempdir() + "/" + randomUUID() | |
writeFileSync(path, JSON.stringify(taskDef.taskDefinition), "utf8") | |
const result = await aws( | |
`ecs register-task-definition --cli-input-json file://${path}` | |
) | |
const name = | |
result.taskDefinition.family + ":" + result.taskDefinition.revision | |
await aws( | |
`ecs update-service --cluster prod --service app --task-definition ${name}` | |
) | |
console.log(`Updated prod cluster to ${name}`) | |
} | |
const main = async () => { | |
const image = await getStagingImage() | |
await promoteImageToProd(image) | |
} | |
main().then(() => ({})) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment