diff --git a/examples/aws/containers/ecr-repos/main.tf b/examples/aws/containers/ecr-repos/main.tf new file mode 100644 index 0000000..51c3cb6 --- /dev/null +++ b/examples/aws/containers/ecr-repos/main.tf @@ -0,0 +1,20 @@ +module "ecr_repos" { + source = "../../../../modules/aws/containers/ecr-repos" + + repositories = { + example = {} + another-example = { + image_tag_mutability = "IMMUTABLE" + external_account_ids_with_read_access = ["586861619874"] + external_account_ids_with_write_access = ["586861619874"] + external_account_ids_with_lambda_access = ["586861619874"] + tags = { + Hello = "World" + } + } + } + + tags_all = { + Hi = "There" + } +} diff --git a/examples/aws/containers/ecr-repos/outputs.tf b/examples/aws/containers/ecr-repos/outputs.tf new file mode 100644 index 0000000..56a8ff4 --- /dev/null +++ b/examples/aws/containers/ecr-repos/outputs.tf @@ -0,0 +1,19 @@ +output "ecr_repo_arns" { + description = "A map of repository name to its ECR ARN." + value = module.ecr_repos.ecr_repo_arns +} + +output "ecr_repo_urls" { + description = "A map of repository name to its URL." + value = module.ecr_repos.ecr_repo_urls +} + +output "ecr_read_policy_actions" { + description = "A list of IAM policy actions necessary for ECR read access." + value = module.ecr_repos.ecr_read_policy_actions +} + +output "ecr_write_policy_actions" { + description = "A list of IAM policy actions necessary for ECR write access." + value = module.ecr_repos.ecr_write_policy_actions +} diff --git a/modules/aws/containers/ecr-repos/main.tf b/modules/aws/containers/ecr-repos/main.tf new file mode 100644 index 0000000..1480f75 --- /dev/null +++ b/modules/aws/containers/ecr-repos/main.tf @@ -0,0 +1,103 @@ +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} +data "aws_partition" "current" {} + +locals { + # Construct the configuration of ECR repositories that combine the raw user input with the configured defaults. + repositories_with_defaults = { + for repo_name, user_config in var.repositories : + repo_name => { + external_account_ids_with_read_access = lookup(user_config, "external_account_ids_with_read_access", var.default_external_account_ids_with_read_access) + external_account_ids_with_write_access = lookup(user_config, "external_account_ids_with_write_access", var.default_external_account_ids_with_write_access) + external_account_ids_with_lambda_access = lookup(user_config, "external_account_ids_with_lambda_access", var.default_external_account_ids_with_lambda_access) + enable_automatic_image_scanning = lookup(user_config, "enable_automatic_image_scanning", var.default_automatic_image_scanning) + encryption_config = lookup(user_config, "encryption_config", var.default_encryption_config) + image_tag_mutability = lookup(user_config, "image_tag_mutability", var.default_image_tag_mutability) + lifecycle_policy_rules = lookup(user_config, "lifecycle_policy_rules", var.default_lifecycle_policy_rules) + tags = merge( + lookup(user_config, "tags", {}), + var.tags_all, + ) + } + } + + repositories_with_lifecycle_rules = { + for repo_name, repo in local.repositories_with_defaults : + repo_name => repo if length(repo.lifecycle_policy_rules) > 0 + } + repositories_with_external_access = { + for repo_name, repo in local.repositories_with_defaults : + repo_name => repo + if( + length(repo.external_account_ids_with_read_access) > 0 + || length(repo.external_account_ids_with_write_access) > 0 + || length(repo.external_account_ids_with_lambda_access) > 0 + ) + } + + # The list of IAM policy actions for write access + iam_write_access_policies = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:GetRepositoryPolicy", + "ecr:DescribeRepositories", + "ecr:ListImages", + "ecr:DescribeImages", + "ecr:BatchGetImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "ecr:PutImage", + ] + + # The list of IAM policy actions for read access + iam_read_access_policies = [ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "ecr:BatchCheckLayerAvailability", + "ecr:ListImages", + ] +} + +resource "aws_ecr_repository" "repos" { + for_each = local.repositories_with_defaults + + name = each.key + image_tag_mutability = each.value.image_tag_mutability + tags = each.value.tags + + image_scanning_configuration { + scan_on_push = each.value.enable_automatic_image_scanning + } + + dynamic "encryption_configuration" { + for_each = each.value.encryption_config != null ? ["once"] : [] + content { + encryption_type = each.value.encryption_config.encryption_type + kms_key = each.value.encryption_config.kms_key + } + } +} + +resource "aws_ecr_lifecycle_policy" "this" { + for_each = local.repositories_with_lifecycle_rules + repository = aws_ecr_repository.repos[each.key].name + policy = jsonencode(each.value.lifecycle_policy_rules) +} + +resource "aws_ecr_replication_configuration" "this" { + count = length(var.replication_regions) > 0 ? 1 : 0 + replication_configuration { + rule { + + dynamic "destination" { + for_each = var.replication_regions + content { + region = destination.value + registry_id = data.aws_caller_identity.current.account_id + } + } + } + } +} diff --git a/modules/aws/containers/ecr-repos/outputs.tf b/modules/aws/containers/ecr-repos/outputs.tf new file mode 100644 index 0000000..25598d7 --- /dev/null +++ b/modules/aws/containers/ecr-repos/outputs.tf @@ -0,0 +1,19 @@ +output "ecr_repo_arns" { + description = "A map of repository name to its ECR ARN." + value = { for repo_name, repo in aws_ecr_repository.repos : repo_name => repo.arn } +} + +output "ecr_repo_urls" { + description = "A map of repository name to its URL." + value = { for repo_name, repo in aws_ecr_repository.repos : repo_name => repo.repository_url } +} + +output "ecr_read_policy_actions" { + description = "A list of IAM policy actions necessary for ECR read access." + value = local.iam_read_access_policies +} + +output "ecr_write_policy_actions" { + description = "A list of IAM policy actions necessary for ECR write access." + value = local.iam_write_access_policies +} diff --git a/modules/aws/containers/ecr-repos/policy.tf b/modules/aws/containers/ecr-repos/policy.tf new file mode 100644 index 0000000..0d5efb8 --- /dev/null +++ b/modules/aws/containers/ecr-repos/policy.tf @@ -0,0 +1,65 @@ +resource "aws_ecr_repository_policy" "external_account_access" { + for_each = local.repositories_with_external_access + repository = aws_ecr_repository.repos[each.key].name + policy = data.aws_iam_policy_document.external_account_access[each.key].json +} + +data "aws_iam_policy_document" "external_account_access" { + for_each = local.repositories_with_external_access + + dynamic "statement" { + for_each = length(each.value.external_account_ids_with_read_access) > 0 ? ["noop"] : [] + + content { + effect = "Allow" + + principals { + type = "AWS" + identifiers = formatlist("arn:${data.aws_partition.current.partition}:iam::%s:root", each.value.external_account_ids_with_read_access) + } + + actions = local.iam_read_access_policies + } + } + + dynamic "statement" { + for_each = length(each.value.external_account_ids_with_write_access) > 0 ? ["noop"] : [] + + content { + effect = "Allow" + + principals { + type = "AWS" + identifiers = formatlist("arn:${data.aws_partition.current.partition}:iam::%s:root", each.value.external_account_ids_with_write_access) + } + + actions = local.iam_write_access_policies + } + } + + dynamic "statement" { + for_each = each.value.external_account_ids_with_lambda_access + + content { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + + condition { + test = "StringLike" + variable = "aws:sourceARN" + + # Allow lambda function access in any of the regions that the ECR repo is created for each account. + values = [ + for region in concat([data.aws_region.current.name], var.replication_regions) : + "arn:${data.aws_partition.current.partition}:lambda:${region}:${statement.value}:function:*" + ] + } + + actions = local.iam_read_access_policies + } + } +} diff --git a/modules/aws/containers/ecr-repos/variables.tf b/modules/aws/containers/ecr-repos/variables.tf new file mode 100644 index 0000000..0011b95 --- /dev/null +++ b/modules/aws/containers/ecr-repos/variables.tf @@ -0,0 +1,64 @@ +variable "repositories" { + description = "A map of repo names to configurations for that repository." + type = any +} + +variable "default_external_account_ids_with_read_access" { + description = "The default list of AWS account IDs for external AWS accounts that should be able to pull images from these ECR repos. Can be overridden on a per repo basis by the external_account_ids_with_read_access property in the repositories map." + type = list(string) + default = [] +} + +variable "default_external_account_ids_with_write_access" { + description = "The default list of AWS account IDs for external AWS accounts that should be able to pull and push images to these ECR repos. Can be overridden on a per repo basis by the external_account_ids_with_write_access property in the repositories map." + type = list(string) + default = [] +} + +variable "default_external_account_ids_with_lambda_access" { + description = "The default list of AWS account IDs for external AWS accounts that should be able to create Lambda functions based on container images in these ECR repos. Can be overridden on a per repo basis by the external_account_ids_with_lambda_access property in the repositories map." + type = list(string) + default = [] +} + +variable "default_automatic_image_scanning" { + description = "Whether or not to enable image scanning on all the repos. Can be overridden on a per repo basis by the enable_automatic_image_scanning property in the repositories map." + type = bool + default = true +} + +variable "default_encryption_config" { + description = "The default encryption configuration to apply to the created ECR repository. When null, the images in the ECR repo will not be encrypted at rest. Can be overridden on a per repo basis by the encryption_config property in the repositories map." + type = object({ + encryption_type = string + kms_key = string + }) + default = { + encryption_type = "AES256" + kms_key = null + } +} + +variable "default_image_tag_mutability" { + description = "The tag mutability setting for all the repos. Must be one of: MUTABLE or IMMUTABLE. Can be overridden on a per repo basis by the image_tag_mutability property in the repositories map." + type = string + default = "MUTABLE" +} + +variable "tags_all" { + description = "A map of tags (where the key and value correspond to tag keys and values) that should be assigned to all ECR repositories." + type = map(string) + default = {} +} + +variable "default_lifecycle_policy_rules" { + description = "Add lifecycle policy to ECR repo." + type = any + default = [] +} + +variable "replication_regions" { + description = "List of regions (e.g., us-east-1) to replicate the ECR repository to." + type = list(string) + default = [] +} diff --git a/modules/aws/containers/ecr-repos/versions.tf b/modules/aws/containers/ecr-repos/versions.tf new file mode 100644 index 0000000..9416453 --- /dev/null +++ b/modules/aws/containers/ecr-repos/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">=1.3" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">=4.0" + } + } +}