Skip to content
Open
5 changes: 1 addition & 4 deletions .github/workflows/continuous-deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ jobs:
uses: ./.github/workflows/deploy-backend.yml
with:
apigee_environment: internal-dev
build_recordprocessor_image: false
diff_base_sha: ${{ github.event.before }}
diff_head_sha: ${{ github.sha }}
run_diff_check: true
Expand Down Expand Up @@ -86,9 +85,7 @@ jobs:
uses: ./.github/workflows/deploy-backend.yml
with:
apigee_environment: ${{ matrix.sub_environment_name }}
recordprocessor_image_version: ${{ needs.deploy-internal-dev-backend.outputs.recordprocessor_image_version }}
diff_base_sha: ${{ github.event.before }}
diff_head_sha: ${{ github.sha }}
lambda_image_overrides: ${{ needs.deploy-internal-dev-backend.outputs.image_uris_json }}
run_diff_check: false
create_mns_subscription: true
environment: dev
Expand Down
122 changes: 97 additions & 25 deletions .github/workflows/deploy-backend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ on:
required: false
type: boolean
default: false
recordprocessor_image_version:
build_ack_backend_image:
required: false
type: boolean
default: false
lambda_image_overrides:
description: >
JSON map of lambda_name -> immutable image selector for reuse mode.
Supports tags, sha256 digests, or full image URIs. Empty object means
no explicit override.
required: false
type: string
default: ""
default: "{}"
diff_base_sha:
required: false
type: string
Expand All @@ -37,9 +45,9 @@ on:
required: true
type: string
outputs:
recordprocessor_image_version:
description: Selected immutable image selector used for recordprocessor deployment
value: ${{ jobs.deploy-recordprocessor-image.outputs.image_uri }}
image_uris_json:
description: JSON map of lambda_name -> immutable image URI used for deployment
value: ${{ jobs.terraform-plan.outputs.image_uris_json }}
workflow_dispatch:
inputs:
apigee_environment:
Expand Down Expand Up @@ -68,11 +76,18 @@ on:
required: true
type: boolean
default: true
recordprocessor_image_version:
description: Existing immutable recordprocessor image selector (tag, digest, or image URI) for reuse mode
build_ack_backend_image:
description: Build and publish a new ack backend image
required: true
type: boolean
default: true
lambda_image_overrides:
description: >
JSON map of lambda_name -> immutable image selector for reuse mode.
e.g. {"recordprocessor":"internal-dev-git-abc123","ack-backend":"123456789012.dkr.ecr.eu-west-2.amazonaws.com/imms-ackbackend-repo@sha256:..."}
required: false
type: string
default: ""
default: "{}"
diff_base_sha:
description: Base commit SHA for diff checks
required: false
Expand Down Expand Up @@ -100,25 +115,38 @@ env: # Sonarcloud - do not allow direct usage of untrusted data
run-name: Deploy Backend - ${{ inputs.environment }} ${{ inputs.sub_environment }}

jobs:
deploy-recordprocessor-image:
name: Deploy recordprocessor image
deploy-lambda-images:
name: Deploy ${{ matrix.lambda_name }} image
strategy:
fail-fast: false
matrix:
include:
- lambda_name: recordprocessor
ecr_repository: imms-recordprocessor-repo
dockerfile_path: lambdas/recordprocessor/Dockerfile
lambda_paths: |
lambdas/recordprocessor/
- lambda_name: ack-backend
ecr_repository: imms-ackbackend-repo
dockerfile_path: lambdas/ack_backend/Dockerfile
lambda_paths: |
lambdas/ack_backend/
uses: ./.github/workflows/deploy-lambda-artifact.yml
with:
lambda_name: recordprocessor
lambda_name: ${{ matrix.lambda_name }}
environment: ${{ inputs.environment }}
sub_environment: ${{ inputs.sub_environment }}
build_image: ${{ inputs.build_recordprocessor_image }}
image_version: ${{ inputs.recordprocessor_image_version }}
build_image: ${{ matrix.lambda_name == 'recordprocessor' && inputs.build_recordprocessor_image || matrix.lambda_name == 'ack-backend' && inputs.build_ack_backend_image || false }}
image_version: ${{ fromJson(inputs.lambda_image_overrides)[matrix.lambda_name] || '' }}
run_diff_check: ${{ inputs.run_diff_check }}
diff_base_sha: ${{ inputs.diff_base_sha }}
diff_head_sha: ${{ inputs.diff_head_sha }}
lambda_paths: |
lambdas/recordprocessor/
lambda_paths: ${{ matrix.lambda_paths }}
shared_paths: |
lambdas/shared/src/common/
docker_context_path: lambdas
dockerfile_path: lambdas/recordprocessor/Dockerfile
ecr_repository: imms-recordprocessor-repo
dockerfile_path: ${{ matrix.dockerfile_path }}
ecr_repository: ${{ matrix.ecr_repository }}
image_tag_prefix: ${{ inputs.sub_environment }}-
allow_implicit_tag_prefix_reuse: ${{ inputs.sub_environment == 'internal-dev' || startsWith(inputs.sub_environment, 'pr-') }}

Expand All @@ -127,13 +155,11 @@ jobs:
id-token: write
contents: read
needs:
- deploy-recordprocessor-image
if: ${{ !cancelled() && needs.deploy-recordprocessor-image.result == 'success' }}
- deploy-lambda-images
if: ${{ !cancelled() }}
outputs:
recordprocessor_image_uri: ${{ needs.deploy-recordprocessor-image.outputs.image_uri }}
image_uris_json: ${{ steps.lambda-images.outputs.image_uris_json }}
runs-on: ubuntu-latest
env:
TF_VAR_recordprocessor_image_uri: ${{ needs.deploy-recordprocessor-image.outputs.image_uri }}
environment:
name: ${{ inputs.environment }}
steps:
Expand All @@ -151,6 +177,38 @@ jobs:
with:
terraform_version: "1.12.2"

- name: Download lambda deployment manifests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
pattern: deploy-manifest-*-${{ inputs.environment }}-${{ inputs.sub_environment }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/lambda-manifests
merge-multiple: true

- name: Assemble lambda image map and export Terraform vars
id: lambda-images
run: |
set -euo pipefail

manifest_dir="${RUNNER_TEMP}/lambda-manifests"
shopt -s nullglob
manifest_files=("${manifest_dir}"/*.json)

if [ "${#manifest_files[@]}" -eq 0 ]; then
echo "No lambda deployment manifests found."
exit 1
fi

image_uris_json="$(
jq -cs 'map(select(.lambda_name != null and .image_uri != null) | {(.lambda_name): .image_uri}) | add' \
"${manifest_files[@]}"
)"

echo "image_uris_json=${image_uris_json}" >> "$GITHUB_OUTPUT"
jq -er '
to_entries[]
| "TF_VAR_\(.key | gsub("-"; "_"))_image_uri=\(.value)"
' <<< "${image_uris_json}" >> "$GITHUB_ENV"

- name: Terraform Init
working-directory: infrastructure/instance
run: make init
Expand All @@ -173,10 +231,8 @@ jobs:
id-token: write
contents: read
needs: terraform-plan
if: ${{ !cancelled() && needs.terraform-plan.result == 'success' }}
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
env:
TF_VAR_recordprocessor_image_uri: ${{ needs.terraform-plan.outputs.recordprocessor_image_uri }}
environment:
name: ${{ inputs.environment }}
steps:
Expand All @@ -193,6 +249,22 @@ jobs:
with:
terraform_version: "1.12.2"

- name: Restore lambda image Terraform vars
env:
IMAGE_URIS_JSON: ${{ needs.terraform-plan.outputs.image_uris_json }}
run: |
set -euo pipefail

if [ -z "${IMAGE_URIS_JSON}" ] || [ "${IMAGE_URIS_JSON}" = "null" ]; then
echo "terraform-plan did not emit image_uris_json."
exit 1
fi

jq -er '
to_entries[]
| "TF_VAR_\(.key | gsub("-"; "_"))_image_uri=\(.value)"
' <<< "${IMAGE_URIS_JSON}" >> "$GITHUB_ENV"

- name: Retrieve Terraform Plan
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr-deploy-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
with:
apigee_environment: internal-dev
build_recordprocessor_image: ${{ github.event.action == 'opened' || github.event.action == 'reopened' }}
build_ack_backend_image: ${{ github.event.action == 'opened' || github.event.action == 'reopened' }}
diff_base_sha: ${{ github.event.before }}
diff_head_sha: ${{ github.event.pull_request.head.sha }}
run_diff_check: ${{ github.event.action == 'synchronize' }}
Expand Down
17 changes: 10 additions & 7 deletions .github/workflows/pr-teardown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ jobs:
make init apigee_environment=$APIGEE_ENVIRONMENT environment=$BACKEND_ENVIRONMENT sub_environment=$BACKEND_SUB_ENVIRONMENT
make workspace apigee_environment=$APIGEE_ENVIRONMENT environment=$BACKEND_ENVIRONMENT sub_environment=$BACKEND_SUB_ENVIRONMENT
echo "ID_SYNC_QUEUE_ARN=$(make -s output name=id_sync_queue_arn)" >> $GITHUB_ENV
recordprocessor_image_uri="$(make -s output name=recordprocessor_image_uri 2>/dev/null || true)"
if [ -z "${recordprocessor_image_uri}" ]; then
# Destroy still evaluates variable validation, so provide a non-empty fallback when output is unavailable.
recordprocessor_image_uri="placeholder.dkr.ecr.eu-west-2.amazonaws.com/imms-recordprocessor-repo@sha256:0000000000000000000000000000000000000000000000000000000000000000"
fi
echo "TF_VAR_recordprocessor_image_uri=${recordprocessor_image_uri}" >> $GITHUB_ENV
# Destroy still evaluates variable validation, so provide a non-empty fallback when output is unavailable.
resolve_or_placeholder() {
local uri="$(make -s output "name=$1" 2>/dev/null || true)"
echo "${uri:-placeholder.dkr.ecr.eu-west-2.amazonaws.com/$2@sha256:0000000000000000000000000000000000000000000000000000000000000000}"
}
echo "TF_VAR_recordprocessor_image_uri=$(resolve_or_placeholder recordprocessor_image_uri imms-recordprocessor-repo)" >> $GITHUB_ENV
echo "TF_VAR_ack_backend_image_uri=$(resolve_or_placeholder ack_backend_image_uri imms-ackbackend-repo)" >> $GITHUB_ENV

- name: Install poetry
run: pip install poetry==2.1.4
Expand Down Expand Up @@ -128,4 +129,6 @@ jobs:
--output json
}

cleanup_repo_by_prefix "imms-recordprocessor-repo"
for repository_name in imms-recordprocessor-repo imms-ackbackend-repo; do
cleanup_repo_by_prefix "${repository_name}"
done
53 changes: 53 additions & 0 deletions infrastructure/account/ackbackend_ecr_repo.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
resource "aws_ecr_repository" "ackbackend_repository" {
image_scanning_configuration {
scan_on_push = true
}
image_tag_mutability = "IMMUTABLE"
name = "imms-ackbackend-repo"
}

resource "aws_ecr_lifecycle_policy" "ackbackend_repository_lifecycle_policy" {
repository = aws_ecr_repository.ackbackend_repository.name

policy = jsonencode({
rules = [{
rulePriority = 1
description = "Keep last 10 images"
selection = {
tagStatus = "any"
countType = "imageCountMoreThan"
countNumber = 10
}
action = { type = "expire" }
}]
})
}

resource "aws_ecr_repository_policy" "ackbackend_repository_lambda_image_retrieval_policy" {
repository = aws_ecr_repository.ackbackend_repository.name

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "LambdaECRImageRetrievalPolicy"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
Action = [
"ecr:BatchGetImage",
"ecr:DeleteRepositoryPolicy",
"ecr:GetDownloadUrlForLayer",
"ecr:GetRepositoryPolicy",
"ecr:SetRepositoryPolicy"
]
Condition = {
StringLike = {
"aws:sourceArn" = "arn:aws:lambda:${var.aws_region}:${var.imms_account_id}:function:imms-*-ack-lambda"
}
}
}
]
})
}
80 changes: 2 additions & 78 deletions infrastructure/instance/ack_lambda.tf
Original file line number Diff line number Diff line change
@@ -1,81 +1,5 @@
# Define the directory containing the Docker image and calculate its SHA-256 hash for triggering redeployments
locals {
ack_lambda_dir = abspath("${path.root}/../../lambdas/ack_backend")
ack_lambda_files = fileset(local.ack_lambda_dir, "**")
ack_lambda_dir_sha = sha1(join("", [for f in local.ack_lambda_files : filesha1("${local.ack_lambda_dir}/${f}")]))
ack_lambda_name = "${local.short_prefix}-ack-lambda"
}


resource "aws_ecr_repository" "ack_lambda_repository" {
image_scanning_configuration {
scan_on_push = true
}
name = "${local.short_prefix}-ack-repo"
force_delete = local.is_temp
}

# Module for building and pushing Docker image to ECR
module "ack_processor_docker_image" {
source = "terraform-aws-modules/lambda/aws//modules/docker-build"
version = "8.7.0"
docker_file_path = "./ack_backend/Dockerfile"
create_ecr_repo = false
ecr_repo = aws_ecr_repository.ack_lambda_repository.name
ecr_repo_lifecycle_policy = jsonencode({
"rules" : [
{
"rulePriority" : 1,
"description" : "Keep only the last 2 images",
"selection" : {
"tagStatus" : "any",
"countType" : "imageCountMoreThan",
"countNumber" : 2
},
"action" : {
"type" : "expire"
}
}
]
})

platform = "linux/amd64"
use_image_tag = false
source_path = abspath("${path.root}/../../lambdas")
triggers = {
dir_sha = local.ack_lambda_dir_sha
shared_dir_sha = local.shared_dir_sha
}
}

# Define the lambdaECRImageRetreival policy
resource "aws_ecr_repository_policy" "ack_lambda_ECRImageRetreival_policy" {
repository = aws_ecr_repository.ack_lambda_repository.name

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
"Sid" : "LambdaECRImageRetrievalPolicy",
"Effect" : "Allow",
"Principal" : {
"Service" : "lambda.amazonaws.com"
},
"Action" : [
"ecr:BatchGetImage",
"ecr:DeleteRepositoryPolicy",
"ecr:GetDownloadUrlForLayer",
"ecr:GetRepositoryPolicy",
"ecr:SetRepositoryPolicy"
],
"Condition" : {
"StringLike" : {
"aws:sourceArn" : "arn:aws:lambda:${var.aws_region}:${var.immunisation_account_id}:function:${local.ack_lambda_name}"
}
}
}
]
})
ack_lambda_name = "${local.short_prefix}-ack-lambda"
}

# IAM Role for Lambda
Expand Down Expand Up @@ -201,7 +125,7 @@ resource "aws_lambda_function" "ack_processor_lambda" {
function_name = local.ack_lambda_name
role = aws_iam_role.ack_lambda_exec_role.arn
package_type = "Image"
image_uri = module.ack_processor_docker_image.image_uri
image_uri = var.ack_backend_image_uri
architectures = ["x86_64"]
timeout = 900
memory_size = 2048
Expand Down
Loading
Loading