Skip to content

Commit 0008eb1

Browse files
mishraompCopilot
andauthored
feat: ECR caching for PROD environment (#108)
* feat: ECR caching for PROD environment * copilot suggestion Co-authored-by: Copilot <[email protected]> * fix: immutability * fix: idempotency * fix: ECR and tagging * fix: ecr * fix lifecycle policy * fix: grouping in if clause --------- Co-authored-by: Copilot <[email protected]>
1 parent 4ae5dde commit 0008eb1

File tree

5 files changed

+116
-16
lines changed

5 files changed

+116
-16
lines changed

.github/workflows/.deployer.yml

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,57 +66,69 @@ jobs:
6666
steps:
6767
- name: Checkout
6868
uses: actions/checkout@v4
69+
6970
- name: Configure AWS Credentials
7071
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4
7172
with:
72-
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
73-
role-session-name: ${{ inputs.environment_name }}-deployment
74-
aws-region: ${{ env.AWS_REGION }}
73+
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
74+
role-session-name: ${{ inputs.environment_name }}-deployment
75+
aws-region: ${{ env.AWS_REGION }}
76+
7577
- uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3
7678
with:
7779
terraform_version: ${{ env.TF_VERSION }}
80+
7881
- name: Setup Terragrunt
7982
uses: autero1/action-terragrunt@aefb0a43c4f5503a91fefb307745c4d51c26ed0e # v3
8083
with:
8184
terragrunt-version: ${{ env.TG_VERSION }}
85+
86+
- name: Get ECR Registry
87+
if: inputs.app_env == 'prod'
88+
id: ecr-check
89+
run: |
90+
ECR_REGISTRY=$(aws sts get-caller-identity --query Account --output text).dkr.ecr.ca-central-1.amazonaws.com
91+
echo "ecr-registry=$ECR_REGISTRY" >> $GITHUB_OUTPUT
92+
8293
- name: Terragrunt ${{inputs.command}}
8394
working-directory: terraform/${{ inputs.working_directory }}/${{ inputs.environment_name }}
8495
env:
8596
target_env: ${{ inputs.environment_name }}
8697
aws_license_plate: ${{ secrets.AWS_LICENSE_PLATE }}
87-
flyway_image: ghcr.io/${{github.repository}}/migrations:${{inputs.tag}}
88-
api_image: ghcr.io/${{github.repository}}/backend:${{inputs.tag}}
98+
flyway_image: ${{ (inputs.app_env == 'prod' && inputs.working_directory == 'api' && format('{0}/quickstart-aws-containers-migrations-prod:{1}', steps.ecr-check.outputs.ecr-registry, inputs.tag)) || (format('ghcr.io/{0}/migrations:{1}', github.repository, inputs.tag)) }}
99+
api_image: ${{ (inputs.app_env == 'prod' && inputs.working_directory == 'api' && format('{0}/quickstart-aws-containers-backend-prod:{1}', steps.ecr-check.outputs.ecr-registry, inputs.tag)) || (format('ghcr.io/{0}/backend:{1}', github.repository, inputs.tag)) }}
89100
app_env: ${{inputs.app_env}}
90101
stack_prefix: ${{ inputs.stack_prefix }}
91102
run: |
92103
# Run terraform
93104
terragrunt run-all ${{inputs.command}} --terragrunt-non-interactive
105+
94106
- name: Terragrunt API Outputs
95107
if: ( inputs.working_directory == 'api' && inputs.command == 'apply' )
96108
working-directory: terraform/${{ inputs.working_directory }}/${{ inputs.environment_name }}
97109
id: tg-outputs
98110
env:
99111
target_env: ${{ inputs.environment_name }}
100112
aws_license_plate: ${{ secrets.AWS_LICENSE_PLATE }}
101-
flyway_image: ghcr.io/${{github.repository}}/migrations:${{inputs.tag}}
102-
api_image: ghcr.io/${{github.repository}}/backend:${{inputs.tag}}
113+
flyway_image: ${{ inputs.app_env == 'prod' && format('{0}/quickstart-aws-containers-migrations-prod:{1}', steps.ecr-check.outputs.ecr-registry, inputs.tag) || format('ghcr.io/{0}/migrations:{1}', github.repository, inputs.tag) }}
114+
api_image: ${{ inputs.app_env == 'prod' && format('{0}/quickstart-aws-containers-backend-prod:{1}', steps.ecr-check.outputs.ecr-registry, inputs.tag) || format('ghcr.io/{0}/backend:{1}', github.repository, inputs.tag) }}
103115
app_env: ${{inputs.app_env}}
104116
stack_prefix: ${{ inputs.stack_prefix }}
105117
run: |
106118
terragrunt output -json > outputs.json
107119
#print the output
108120
cat outputs.json
109-
110-
echo "API_GW_URL=$(jq -r .apigw_url.value outputs.json)" >> $GITHUB_OUTPUT
121+
echo "API_GW_URL=$(jq -r .apigw_url.value outputs.json)" >> $GITHUB_OUTPUT
122+
111123
- name: Terragrunt Frontend Outputs
112124
if: ( inputs.working_directory == 'frontend' && inputs.command == 'apply' )
113125
working-directory: terraform/${{ inputs.working_directory }}/${{ inputs.environment_name }}
114126
id: tg-outputs-frontend
115127
env:
116128
target_env: ${{ inputs.environment_name }}
117129
aws_license_plate: ${{ secrets.AWS_LICENSE_PLATE }}
118-
flyway_image: ghcr.io/${{github.repository}}/migrations:${{inputs.tag}}
119-
api_image: ghcr.io/${{github.repository}}/backend:${{inputs.tag}}
130+
flyway_image: ${{ inputs.app_env == 'prod' && format('{0}/quickstart-aws-containers-migrations-prod:{1}', steps.ecr-check.outputs.ecr-registry, inputs.tag) || format('ghcr.io/{0}/migrations:{1}', github.repository, inputs.tag) }}
131+
api_image: ${{ inputs.app_env == 'prod' && format('{0}/quickstart-aws-containers-backend-prod:{1}', steps.ecr-check.outputs.ecr-registry, inputs.tag) || format('ghcr.io/{0}/backend:{1}', github.repository, inputs.tag) }}
120132
app_env: ${{inputs.app_env}}
121133
stack_prefix: ${{ inputs.stack_prefix }}
122134
run: |

.github/workflows/release.yml

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ jobs:
7070
echo "tags<<EOF" >> $GITHUB_OUTPUT
7171
echo "$tags" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' >> $GITHUB_OUTPUT
7272
echo "EOF" >> $GITHUB_OUTPUT
73+
7374
retag-images:
7475
name: Retag Images
7576
needs: [vars]
@@ -86,23 +87,80 @@ jobs:
8687
target: ${{inputs.containers_tag || 'test'}} # this is the tag of the containers to deploy, defaults to test
8788
tags: |
8889
${{ needs.vars.outputs.tags }}
90+
91+
push-to-ecr:
92+
name: Push Images to ECR
93+
needs: [vars, retag-images]
94+
runs-on: ubuntu-24.04
95+
strategy:
96+
matrix:
97+
package: [backend, migrations]
98+
steps:
99+
- name: Configure AWS Credentials
100+
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4
101+
with:
102+
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
103+
role-session-name: gha-ecr-push
104+
aws-region: ca-central-1
105+
106+
- name: Login to Amazon ECR
107+
id: login-ecr
108+
uses: aws-actions/amazon-ecr-login@v2
109+
110+
- name: Login to GitHub Container Registry
111+
uses: docker/login-action@v3
112+
with:
113+
registry: ghcr.io
114+
username: ${{ github.actor }}
115+
password: ${{ secrets.GITHUB_TOKEN }}
116+
117+
- name: Pull, tag and push image to ECR
118+
env:
119+
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
120+
GHCR_IMAGE: ghcr.io/${{ github.repository }}/${{ matrix.package }}:${{ needs.vars.outputs.tag }}
121+
run: |
122+
# Create ECR repository if it doesn't exist
123+
aws ecr create-repository \
124+
--repository-name quickstart-aws-containers-${{ matrix.package }}-prod \
125+
--image-tag-mutability IMMUTABLE \
126+
--image-scanning-configuration scanOnPush=true \
127+
|| true
128+
129+
# Apply lifecycle policy separately
130+
aws ecr put-lifecycle-policy \
131+
--repository-name quickstart-aws-containers-${{ matrix.package }}-prod \
132+
--lifecycle-policy '{"rules":[{"rulePriority":1,"description":"Keep only 5 tagged images","selection":{"tagStatus":"tagged","countType":"imageCountMoreThan","countNumber":5},"action":{"type":"expire"}},{"rulePriority":2,"description":"Remove untagged images","selection":{"tagStatus":"untagged","countType":"imageCountMoreThan","countNumber":0},"action":{"type":"expire"}}]}' \
133+
|| true
134+
135+
# Pull image from GHCR
136+
docker pull $GHCR_IMAGE
137+
138+
# Tag for ECR
139+
ECR_IMAGE=$ECR_REGISTRY/quickstart-aws-containers-${{ matrix.package }}-prod:${{ needs.vars.outputs.tag }}
140+
docker tag $GHCR_IMAGE $ECR_IMAGE
141+
142+
# Push to ECR
143+
docker push $ECR_IMAGE
144+
89145
resume-resources:
90146
name: Resume Resources # This job resumes resources for the merged PR which is needed if the resources were paused.
91147
needs: [vars]
92148
uses: ./.github/workflows/resume-resources.yml
93149
with:
94150
app_env: prod
95151
secrets: inherit
152+
96153
deploy:
97154
name: Deploy Stack
98-
needs: [vars, resume-resources, retag-images]
155+
needs: [vars, resume-resources, retag-images, push-to-ecr]
99156
uses: ./.github/workflows/.deploy_stack.yml
100157
secrets: inherit
101158
with:
102159
environment_name: dev # since we only have one namespace dev, update this to PROD
103160
command: apply
104161
tag: ${{ needs.vars.outputs.tag}} # this is the tag of the containers to deploy
105162
app_env: prod
163+
106164
release:
107165
name: Github Release
108166
runs-on: ubuntu-24.04
@@ -120,6 +178,7 @@ jobs:
120178
tag_name: ${{ needs.vars.outputs.tag }}
121179
name: ${{ needs.vars.outputs.tag }}
122180
body: ${{ needs.vars.outputs.clean_changelog }}
181+
123182
pause-resources:
124183
name: Pause Resources # This job pauses resources for the merged PR which is needed if the resources were not paused, this is to save money, remove it after cloning.
125184
needs: [release]

infrastructure/api/ecs.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ resource "aws_ecs_task_definition" "flyway_task" {
4848
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
4949
task_role_arn = aws_iam_role.app_container_role.arn
5050
container_definitions = jsonencode([
51-
{
51+
{
5252
name = "${var.app_name}-flyway"
5353
image = "${var.flyway_image}"
5454
essential = true
@@ -171,7 +171,7 @@ resource "aws_ecs_task_definition" "node_api_task" {
171171
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
172172
task_role_arn = aws_iam_role.app_container_role.arn
173173
container_definitions = jsonencode([
174-
{
174+
{
175175
name = "${local.container_name}"
176176
image = "${var.api_image}"
177177
essential = true

infrastructure/api/iam.tf

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
data "aws_caller_identity" "current" {}
32
# ECS task execution role data
43
data "aws_iam_policy_document" "ecs_task_execution_role" {
@@ -103,4 +102,29 @@ EOF
103102
resource "aws_iam_role_policy_attachment" "rdsAttach" {
104103
role = aws_iam_role.app_container_role.name
105104
policy_arn = data.aws_iam_policy.appRDS.arn
105+
}
106+
107+
# ECR permissions for production environment
108+
resource "aws_iam_role_policy" "ecs_task_execution_ecr" {
109+
count = var.app_env == "prod" ? 1 : 0
110+
name = "${var.app_name}-ecs_task_execution_ecr"
111+
role = aws_iam_role.ecs_task_execution_role.id
112+
113+
policy = jsonencode({
114+
Version = "2012-10-17"
115+
Statement = [
116+
{
117+
Effect = "Allow"
118+
Action = [
119+
"ecr:BatchCheckLayerAvailability",
120+
"ecr:GetDownloadUrlForLayer",
121+
"ecr:BatchGetImage",
122+
"ecr:GetAuthorizationToken"
123+
]
124+
Resource = [
125+
"arn:aws:ecr:*:*:*"
126+
]
127+
}
128+
]
129+
})
106130
}

infrastructure/api/vars.tf

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ variable "repository_names" {
129129
}
130130
variable "image_tag_mutability" {
131131
description = "Tag mutability setting for the repository. Must be one of: MUTABLE or IMMUTABLE."
132-
default = "MUTABLE"
132+
default = "IMMUTABLE"
133133
}
134134

135135
variable "image_scanning_enabled" {
@@ -149,6 +149,11 @@ variable "write_principals" {
149149
type = list(any)
150150
default = []
151151
}
152+
variable "ecr_image_retention_count" {
153+
description = "Number of images to retain in ECR repository"
154+
type = number
155+
default = 5
156+
}
152157

153158
variable "tags" {
154159
description = "A set of of one or more tags to provide some metadata for the provisioned resources."

0 commit comments

Comments
 (0)