diff --git a/examples/container-image/main.tf b/examples/container-image/main.tf index 75a36ffc..f1a23d0d 100644 --- a/examples/container-image/main.tf +++ b/examples/container-image/main.tf @@ -128,7 +128,8 @@ module "docker_build_from_ecr" { dir_sha = local.dir_sha } - cache_from = ["${module.ecr.repository_url}:latest"] + cache_from = ["${module.ecr.repository_url}:latest"] + use_cache_from_previous_image = true } module "ecr" { diff --git a/modules/docker-build/README.md b/modules/docker-build/README.md index 0bfb506c..207dd2f4 100644 --- a/modules/docker-build/README.md +++ b/modules/docker-build/README.md @@ -60,6 +60,7 @@ module "docker_image" { | [terraform](#requirement\_terraform) | >= 1.0 | | [aws](#requirement\_aws) | >= 4.22 | | [docker](#requirement\_docker) | >= 3.0 | +| [external](#requirement\_external) | >= 2.3 | | [null](#requirement\_null) | >= 2.0 | ## Providers @@ -68,6 +69,7 @@ module "docker_image" { |------|---------| | [aws](#provider\_aws) | >= 4.22 | | [docker](#provider\_docker) | >= 3.0 | +| [external](#provider\_external) | >= 2.3 | | [null](#provider\_null) | >= 2.0 | ## Modules @@ -85,6 +87,7 @@ No modules. | [null_resource.sam_metadata_docker_registry_image](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | | [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +| [external_external.latest_ecr_image](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) | data source | ## Inputs @@ -109,6 +112,7 @@ No modules. | [scan\_on\_push](#input\_scan\_on\_push) | Indicates whether images are scanned after being pushed to the repository | `bool` | `false` | no | | [source\_path](#input\_source\_path) | Path to folder containing application code | `string` | `null` | no | | [triggers](#input\_triggers) | A map of arbitrary strings that, when changed, will force the docker\_image resource to be replaced. This can be used to rebuild an image when contents of source code folders change | `map(string)` | `{}` | no | +| [use\_cache\_from\_previous\_image](#input\_use\_cache\_from\_previous\_image) | If true, use the most recently pushed image in ECR as Docker cache source (cache\_from). Requires an existing ECR repo. | `bool` | `false` | no | | [use\_image\_tag](#input\_use\_image\_tag) | Controls whether to use image tag in ECR repository URI or not. Disable this to deploy latest image using ID (sha256:...) | `bool` | `true` | no | ## Outputs diff --git a/modules/docker-build/main.tf b/modules/docker-build/main.tf index 1bf27a1f..18bb6cf2 100644 --- a/modules/docker-build/main.tf +++ b/modules/docker-build/main.tf @@ -7,6 +7,15 @@ locals { ecr_repo = var.create_ecr_repo ? aws_ecr_repository.this[0].id : var.ecr_repo image_tag = var.use_image_tag ? coalesce(var.image_tag, formatdate("YYYYMMDDhhmmss", timestamp())) : null ecr_image_name = var.use_image_tag ? format("%v/%v:%v", local.ecr_address, local.ecr_repo, local.image_tag) : format("%v/%v", local.ecr_address, local.ecr_repo) + + previous_image_from_ecr = try(data.external.latest_ecr_image[0].result.image_uri, "") + + previous_image_list = ( + var.use_cache_from_previous_image && local.previous_image_from_ecr != "" + ) ? [local.previous_image_from_ecr] : [] + + cache_from_effective = concat(var.cache_from, local.previous_image_list) + } resource "docker_image" "this" { @@ -17,7 +26,7 @@ resource "docker_image" "this" { dockerfile = var.docker_file_path build_args = var.build_args platform = var.platform - cache_from = var.cache_from + cache_from = local.cache_from_effective } force_remove = var.force_remove @@ -33,6 +42,17 @@ resource "docker_registry_image" "this" { triggers = length(var.triggers) == 0 ? { image_id = docker_image.this.image_id } : var.triggers } +data "external" "latest_ecr_image" { + count = var.use_cache_from_previous_image ? 1 : 0 + + program = ["bash", "${path.module}/scripts/get-latest-ecr-image.sh"] + + query = { + repository = var.ecr_repo + region = data.aws_region.current.name + } +} + resource "aws_ecr_repository" "this" { count = var.create_ecr_repo ? 1 : 0 diff --git a/modules/docker-build/scripts/get-latest-ecr-image.sh b/modules/docker-build/scripts/get-latest-ecr-image.sh new file mode 100644 index 00000000..00abf01c --- /dev/null +++ b/modules/docker-build/scripts/get-latest-ecr-image.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +read -r INPUT +REPO=$(echo "$INPUT" | jq -r '.repository // empty') +REGION=$(echo "$INPUT" | jq -r '.region // empty') + +if [[ -z "$REPO" || -z "$REGION" ]]; then + echo '{"image_uri": ""}' + exit 0 +fi + +# Check if repo exists +if ! aws ecr describe-repositories --repository-names "$REPO" --region "$REGION" >/dev/null 2>&1; then + echo 'no{"image_uri": ""}' + exit 0 +fi + +# Get latest image tag +IMAGE=$(aws ecr describe-images \ + --repository-name "$REPO" \ + --region "$REGION" \ + --query 'reverse(sort_by(imageDetails, &imagePushedAt))[?imageTags]|[0].imageTags[0]' \ + --output text 2>/dev/null || echo "") + +if [ -z "$IMAGE" ] || [ "$IMAGE" == "None" ]; then + echo '{"image_uri": ""}' + exit 0 +fi + +# Get full image URI +URI=$(aws ecr describe-repositories \ + --repository-names "$REPO" \ + --region "$REGION" \ + --query 'repositories[0].repositoryUri' \ + --output text) + +echo "{\"image_uri\": \"${URI}:${IMAGE}\"}" diff --git a/modules/docker-build/variables.tf b/modules/docker-build/variables.tf index e153fa7f..9543a0e5 100644 --- a/modules/docker-build/variables.tf +++ b/modules/docker-build/variables.tf @@ -118,3 +118,9 @@ variable "cache_from" { type = list(string) default = [] } + +variable "use_cache_from_previous_image" { + description = "If true, use the most recently pushed image in ECR as Docker cache source (cache_from). Requires an existing ECR repo." + type = bool + default = false +} diff --git a/modules/docker-build/versions.tf b/modules/docker-build/versions.tf index 93aadf1a..719c4f82 100644 --- a/modules/docker-build/versions.tf +++ b/modules/docker-build/versions.tf @@ -14,5 +14,9 @@ terraform { source = "hashicorp/null" version = ">= 2.0" } + external = { + source = "hashicorp/external" + version = ">= 2.3" + } } } diff --git a/wrappers/docker-build/main.tf b/wrappers/docker-build/main.tf index 7d7614cf..6e2c6e09 100644 --- a/wrappers/docker-build/main.tf +++ b/wrappers/docker-build/main.tf @@ -3,24 +3,25 @@ module "wrapper" { for_each = var.items - build_args = try(each.value.build_args, var.defaults.build_args, {}) - cache_from = try(each.value.cache_from, var.defaults.cache_from, []) - create_ecr_repo = try(each.value.create_ecr_repo, var.defaults.create_ecr_repo, false) - create_sam_metadata = try(each.value.create_sam_metadata, var.defaults.create_sam_metadata, false) - docker_file_path = try(each.value.docker_file_path, var.defaults.docker_file_path, "Dockerfile") - ecr_address = try(each.value.ecr_address, var.defaults.ecr_address, null) - ecr_force_delete = try(each.value.ecr_force_delete, var.defaults.ecr_force_delete, true) - ecr_repo = try(each.value.ecr_repo, var.defaults.ecr_repo, null) - ecr_repo_lifecycle_policy = try(each.value.ecr_repo_lifecycle_policy, var.defaults.ecr_repo_lifecycle_policy, null) - ecr_repo_tags = try(each.value.ecr_repo_tags, var.defaults.ecr_repo_tags, {}) - force_remove = try(each.value.force_remove, var.defaults.force_remove, false) - image_tag = try(each.value.image_tag, var.defaults.image_tag, null) - image_tag_mutability = try(each.value.image_tag_mutability, var.defaults.image_tag_mutability, "MUTABLE") - keep_locally = try(each.value.keep_locally, var.defaults.keep_locally, false) - keep_remotely = try(each.value.keep_remotely, var.defaults.keep_remotely, false) - platform = try(each.value.platform, var.defaults.platform, null) - scan_on_push = try(each.value.scan_on_push, var.defaults.scan_on_push, false) - source_path = try(each.value.source_path, var.defaults.source_path, null) - triggers = try(each.value.triggers, var.defaults.triggers, {}) - use_image_tag = try(each.value.use_image_tag, var.defaults.use_image_tag, true) + build_args = try(each.value.build_args, var.defaults.build_args, {}) + cache_from = try(each.value.cache_from, var.defaults.cache_from, []) + create_ecr_repo = try(each.value.create_ecr_repo, var.defaults.create_ecr_repo, false) + create_sam_metadata = try(each.value.create_sam_metadata, var.defaults.create_sam_metadata, false) + docker_file_path = try(each.value.docker_file_path, var.defaults.docker_file_path, "Dockerfile") + ecr_address = try(each.value.ecr_address, var.defaults.ecr_address, null) + ecr_force_delete = try(each.value.ecr_force_delete, var.defaults.ecr_force_delete, true) + ecr_repo = try(each.value.ecr_repo, var.defaults.ecr_repo, null) + ecr_repo_lifecycle_policy = try(each.value.ecr_repo_lifecycle_policy, var.defaults.ecr_repo_lifecycle_policy, null) + ecr_repo_tags = try(each.value.ecr_repo_tags, var.defaults.ecr_repo_tags, {}) + force_remove = try(each.value.force_remove, var.defaults.force_remove, false) + image_tag = try(each.value.image_tag, var.defaults.image_tag, null) + image_tag_mutability = try(each.value.image_tag_mutability, var.defaults.image_tag_mutability, "MUTABLE") + keep_locally = try(each.value.keep_locally, var.defaults.keep_locally, false) + keep_remotely = try(each.value.keep_remotely, var.defaults.keep_remotely, false) + platform = try(each.value.platform, var.defaults.platform, null) + scan_on_push = try(each.value.scan_on_push, var.defaults.scan_on_push, false) + source_path = try(each.value.source_path, var.defaults.source_path, null) + triggers = try(each.value.triggers, var.defaults.triggers, {}) + use_cache_from_previous_image = try(each.value.use_cache_from_previous_image, var.defaults.use_cache_from_previous_image, false) + use_image_tag = try(each.value.use_image_tag, var.defaults.use_image_tag, true) } diff --git a/wrappers/docker-build/versions.tf b/wrappers/docker-build/versions.tf index 93aadf1a..719c4f82 100644 --- a/wrappers/docker-build/versions.tf +++ b/wrappers/docker-build/versions.tf @@ -14,5 +14,9 @@ terraform { source = "hashicorp/null" version = ">= 2.0" } + external = { + source = "hashicorp/external" + version = ">= 2.3" + } } }