diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ae8099a..78dc0a0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.89.0 + rev: v1.94.1 hooks: - id: terraform_fmt - id: terraform_wrapper_module_for_each diff --git a/README.md b/README.md index 7e661b5..f74e660 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# AWS ECR Terraform module +# Amazon ECR Terraform module -Terraform module which creates AWS ECR resources. +Terraform module which creates Amazon ECR resources. ## Usage @@ -187,20 +187,21 @@ Users of Terragrunt can achieve similar results by using modules provided in the Examples codified under the [`examples`](https://github.com/terraform-aws-modules/terraform-aws-ecr/tree/master/examples) are intended to give users references for how to use the module(s) as well as testing/validating changes to the source code of the module. If contributing to the project, please be sure to make any appropriate updates to the relevant examples to allow maintainers to test your changes and to keep the examples up to date for users. Thank you! - [Complete](https://github.com/terraform-aws-modules/terraform-aws-ecr/tree/master/examples/complete) +- [Repository Template](https://github.com/terraform-aws-modules/terraform-aws-ecr/tree/master/examples/repository-template) - + ## Requirements | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 5.37 | +| [aws](#requirement\_aws) | >= 5.61 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 5.37 | +| [aws](#provider\_aws) | >= 5.61 | ## Modules @@ -264,7 +265,7 @@ No modules. | [repository\_name](#output\_repository\_name) | Name of the repository | | [repository\_registry\_id](#output\_repository\_registry\_id) | The registry ID where the repository was created | | [repository\_url](#output\_repository\_url) | The URL of the repository | - + ## License diff --git a/examples/README.md b/examples/README.md index 57eae0b..7dc6a24 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,4 @@ -# Terraform AWS ECR Examples +# Terraform Amazon ECR Examples - [Complete](https://github.com/terraform-aws-modules/terraform-aws-ecr/tree/master/examples/complete) +- [Repository Template](https://github.com/terraform-aws-modules/terraform-aws-ecr/tree/master/examples/repository-template) diff --git a/examples/complete/README.md b/examples/complete/README.md index 297ff8e..2724d28 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -1,4 +1,4 @@ -# Complete AWS ECR Example +# Amazon ECR Complete Example Configuration in this directory creates: @@ -22,19 +22,19 @@ $ terraform apply Note that this example may create resources which will incur monetary charges on your AWS bill. Run `terraform destroy` when you no longer need these resources. - + ## Requirements | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [aws](#requirement\_aws) | >= 5.37 | +| [aws](#requirement\_aws) | >= 5.61 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 5.37 | +| [aws](#provider\_aws) | >= 5.61 | ## Modules @@ -69,6 +69,6 @@ No inputs. | [repository\_name](#output\_repository\_name) | Name of the repository | | [repository\_registry\_id](#output\_repository\_registry\_id) | The registry ID where the repository was created | | [repository\_url](#output\_repository\_url) | The URL of the repository (in the form `aws_account_id.dkr.ecr.region.amazonaws.com/repositoryName`) | - + Apache-2.0 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-ecr/blob/master/LICENSE). diff --git a/examples/complete/versions.tf b/examples/complete/versions.tf index 0b1e951..97e87e8 100644 --- a/examples/complete/versions.tf +++ b/examples/complete/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.37" + version = ">= 5.61" } } } diff --git a/examples/repository-template/README.md b/examples/repository-template/README.md new file mode 100644 index 0000000..2e7a762 --- /dev/null +++ b/examples/repository-template/README.md @@ -0,0 +1,66 @@ +# Amazon ECR Repository Template Example + +## Usage + +To run this example you need to execute: + +```bash +$ terraform init +$ terraform plan +$ terraform apply +``` + +You can validate this example by running the following commands: + +```bash +# Ensure your local CLI is authenticated with ECR +aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin .dkr.ecr.us-east-1.amazonaws.com + +# Dockerhub pull through cache and repo creation +docker pull .dkr.ecr.us-east-1.amazonaws.com/dockerhub/library/nginx:latest + +# Public ECR pull through cache and repo creation +docker pull .dkr.ecr.us-east-1.amazonaws.com/public-ecr/docker/library/nginx:latest +``` + +Note that this example may create resources which will incur monetary charges on your AWS bill. Run `terraform destroy` when you no longer need these resources. + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 5.61 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 5.61 | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [disabled](#module\_disabled) | ../../modules/repository-template | n/a | +| [dockerhub\_pull\_through\_cache\_repository\_template](#module\_dockerhub\_pull\_through\_cache\_repository\_template) | ../../modules/repository-template | n/a | +| [public\_ecr\_pull\_through\_cache\_repository\_template](#module\_public\_ecr\_pull\_through\_cache\_repository\_template) | ../../modules/repository-template | n/a | +| [secrets\_manager\_dockerhub\_credentials](#module\_secrets\_manager\_dockerhub\_credentials) | terraform-aws-modules/secrets-manager/aws | ~> 1.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | + +## Inputs + +No inputs. + +## Outputs + +No outputs. + + +Apache-2.0 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-ecr/blob/master/LICENSE). diff --git a/examples/repository-template/main.tf b/examples/repository-template/main.tf new file mode 100644 index 0000000..12f656b --- /dev/null +++ b/examples/repository-template/main.tf @@ -0,0 +1,113 @@ +provider "aws" { + region = local.region +} + +locals { + region = "us-east-1" + name = "ecr-ex-${basename(path.cwd)}" + + account_id = data.aws_caller_identity.current.account_id + + tags = { + Name = local.name + Example = local.name + Repository = "https://github.com/terraform-aws-modules/terraform-aws-ecr" + } +} + +data "aws_caller_identity" "current" {} + +################################################################################ +# ECR Repository Template +################################################################################ + +module "public_ecr_pull_through_cache_repository_template" { + source = "../../modules/repository-template" + + # Template + description = "Pull through cache repository template for Public ECR artifacts" + prefix = "public-ecr" + resource_tags = local.tags + lifecycle_policy = jsonencode({ + rules = [ + { + rulePriority = 1, + description = "Keep last 30 images", + selection = { + tagStatus = "tagged", + tagPrefixList = ["v"], + countType = "imageCountMoreThan", + countNumber = 30 + }, + action = { + type = "expire" + } + } + ] + }) + + # Pull through cache rule + create_pull_through_cache_rule = true + upstream_registry_url = "public.ecr.aws" + + tags = local.tags +} + +module "dockerhub_pull_through_cache_repository_template" { + source = "../../modules/repository-template" + + # Template + description = "Pull through cache repository template for Dockerhub artifacts" + prefix = "dockerhub" + resource_tags = local.tags + + # Pull through cache rule + create_pull_through_cache_rule = true + upstream_registry_url = "registry-1.docker.io" + credential_arn = module.secrets_manager_dockerhub_credentials.secret_arn + + tags = local.tags +} + +module "disabled" { + source = "../../modules/repository-template" + + create = false +} + +################################################################################ +# Supporting Resources +################################################################################ + +module "secrets_manager_dockerhub_credentials" { + source = "terraform-aws-modules/secrets-manager/aws" + version = "~> 1.0" + + # Secret names must contain 1-512 Unicode characters and be prefixed with ecr-pullthroughcache/ + name_prefix = "ecr-pullthroughcache/dockerhub-credentials" + description = "Dockerhub credentials" + + # For example only + recovery_window_in_days = 0 + secret_string = jsonencode({ + username = "example" + accessToken = "YouShouldNotStoreThisInPlainText" + }) + + # Policy + create_policy = true + block_public_policy = true + policy_statements = { + read = { + sid = "AllowAccountRead" + principals = [{ + type = "AWS" + identifiers = ["arn:aws:iam::${local.account_id}:root"] + }] + actions = ["secretsmanager:GetSecretValue"] + resources = ["*"] + } + } + + tags = local.tags +} diff --git a/examples/repository-template/outputs.tf b/examples/repository-template/outputs.tf new file mode 100644 index 0000000..e69de29 diff --git a/examples/repository-template/variables.tf b/examples/repository-template/variables.tf new file mode 100644 index 0000000..e69de29 diff --git a/examples/repository-template/versions.tf b/examples/repository-template/versions.tf new file mode 100644 index 0000000..97e87e8 --- /dev/null +++ b/examples/repository-template/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.61" + } + } +} diff --git a/main.tf b/main.tf index 1c86358..14dc875 100644 --- a/main.tf +++ b/main.tf @@ -84,7 +84,6 @@ data "aws_iam_policy_document" "repository" { values = var.repository_lambda_read_access_arns } - } } diff --git a/modules/repository-template/README.md b/modules/repository-template/README.md new file mode 100644 index 0000000..9065b9e --- /dev/null +++ b/modules/repository-template/README.md @@ -0,0 +1,171 @@ +# Amazon ECR Repository Template Terraform module + +Terraform module which creates Amazon ECR repository template resources. + +## Usage + +See [`examples`](https://github.com/terraform-aws-modules/terraform-aws-ecr/tree/master/examples) directory for working examples to reference: + +### Pull Through Cache Rule + +#### Public ECR + +```hcl +module "ecr-repository-template" { + source = "terraform-aws-modules/ecr/aws//modules/repository-template" + + # Template + description = "Pull through cache repository template for Karpenter public ECR artifacts" + prefix = "ecr-public" + create_repository_policy = true + + # Pull through cache rule + upstream_registry_url = "public.ecr.aws" + + tags = { + Terraform = "true" + Environment = "dev" + } +} +``` + +#### Private Registry + +```hcl +module "ecr-repository-template" { + source = "terraform-aws-modules/ecr/aws//modules/repository-template" + + # Template + description = "Pull through cache repository template for NGINX Dockerhub artifacts" + prefix = "docker-hub" + + # Pull through cache rule + upstream_registry_url = "registry-1.docker.io" + credential_arn = aws_secretsmanager_secret.ecr_pull_through_cache.arn + + tags = { + Terraform = "true" + Environment = "dev" + } +} +``` + +### Replication + +```hcl +module "ecr" { + source = "terraform-aws-modules/ecr/aws//modules/repository-template" + + # Template + description = "Replication repository template for production ECR artifacts" + prefix = "prod" + create_repository_policy = true + lifecycle_policy = jsonencode({ + rules = [ + { + rulePriority = 1, + description = "Keep last 30 images", + selection = { + tagStatus = "tagged", + tagPrefixList = ["v"], + countType = "imageCountMoreThan", + countNumber = 30 + }, + action = { + type = "expire" + } + } + ] + }) + + tags = { + Terraform = "true" + Environment = "dev" + } +} +``` + +## Examples + +Examples codified under the [`examples`](https://github.com/terraform-aws-modules/terraform-aws-ecr/tree/master/examples) are intended to give users references for how to use the module(s) as well as testing/validating changes to the source code of the module. If contributing to the project, please be sure to make any appropriate updates to the relevant examples to allow maintainers to test your changes and to keep the examples up to date for users. Thank you! + +- [Complete](https://github.com/terraform-aws-modules/terraform-aws-ecr/tree/master/examples/complete) +- [Repository Template](https://github.com/terraform-aws-modules/terraform-aws-ecr/tree/master/examples/repository-template) + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 5.61 | + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 5.61 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_ecr_pull_through_cache_rule.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_pull_through_cache_rule) | resource | +| [aws_ecr_repository_creation_template.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_creation_template) | resource | +| [aws_iam_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.repository](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [applied\_for](#input\_applied\_for) | Which features this template applies to. Must contain one or more of `PULL_THROUGH_CACHE` or `REPLICATION`. Defaults to `PULL_THROUGH_CACHE` | `list(string)` |
[
"PULL_THROUGH_CACHE"
]
| no | +| [create](#input\_create) | Determines whether resources will be created (affects all resources) | `bool` | `true` | no | +| [create\_iam\_role](#input\_create\_iam\_role) | Determines whether the ECR service IAM role should be created | `bool` | `true` | no | +| [create\_pull\_through\_cache\_rule](#input\_create\_pull\_through\_cache\_rule) | Determines whether a pull through cache rule will be created | `bool` | `false` | no | +| [create\_repository\_policy](#input\_create\_repository\_policy) | Determines whether a repository policy will be created | `bool` | `true` | no | +| [credential\_arn](#input\_credential\_arn) | ARN of the Secret which will be used to authenticate against the registry to use for the pull through cache rule | `string` | `null` | no | +| [custom\_role\_arn](#input\_custom\_role\_arn) | A custom IAM role to use for repository creation. Required if using repository tags or KMS encryption | `string` | `null` | no | +| [description](#input\_description) | The description for this template | `string` | `null` | no | +| [encryption\_type](#input\_encryption\_type) | The type of encryption to use for any created repositories. Must be one of: `AES256` or `KMS`. Defaults to `AES256` | `string` | `"AES256"` | no | +| [iam\_role\_description](#input\_iam\_role\_description) | Description of the role | `string` | `null` | no | +| [iam\_role\_name](#input\_iam\_role\_name) | Name to use on IAM role created | `string` | `null` | no | +| [iam\_role\_path](#input\_iam\_role\_path) | IAM role path | `string` | `null` | no | +| [iam\_role\_permissions\_boundary](#input\_iam\_role\_permissions\_boundary) | ARN of the policy that is used to set the permissions boundary for the IAM role | `string` | `null` | no | +| [iam\_role\_tags](#input\_iam\_role\_tags) | A map of additional tags to add to the IAM role created | `map(string)` | `{}` | no | +| [iam\_role\_use\_name\_prefix](#input\_iam\_role\_use\_name\_prefix) | Determines whether the IAM role name (`iam_role_name`) is used as a prefix | `bool` | `true` | no | +| [image\_tag\_mutability](#input\_image\_tag\_mutability) | The tag mutability setting for any created repositories. Must be one of: `MUTABLE` or `IMMUTABLE`. Defaults to `IMMUTABLE` | `string` | `"IMMUTABLE"` | no | +| [kms\_key\_arn](#input\_kms\_key\_arn) | The ARN of the KMS key used to encrypt the repositories created | `string` | `null` | no | +| [lifecycle\_policy](#input\_lifecycle\_policy) | The lifecycle policy document to apply to any created repositories | `string` | `null` | no | +| [prefix](#input\_prefix) | (Required) The repository name prefix to match against. Use `ROOT` to match any prefix that doesn't explicitly match another template | `string` | `""` | no | +| [repository\_lambda\_read\_access\_arns](#input\_repository\_lambda\_read\_access\_arns) | The ARNs of the Lambda service roles that have read access to the repository | `list(string)` | `[]` | no | +| [repository\_policy](#input\_repository\_policy) | The JSON policy to apply to the repository. If not specified, uses the default policy | `string` | `null` | no | +| [repository\_policy\_statements](#input\_repository\_policy\_statements) | A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage | `any` | `{}` | no | +| [repository\_read\_access\_arns](#input\_repository\_read\_access\_arns) | The ARNs of the IAM users/roles that have read access to the repository | `list(string)` | `[]` | no | +| [repository\_read\_write\_access\_arns](#input\_repository\_read\_write\_access\_arns) | The ARNs of the IAM users/roles that have read/write access to the repository | `list(string)` | `[]` | no | +| [resource\_tags](#input\_resource\_tags) | A map of tags to assign to any created repositories | `map(string)` | `{}` | no | +| [tags](#input\_tags) | A map of tags to add to all resources | `map(string)` | `{}` | no | +| [upstream\_registry\_url](#input\_upstream\_registry\_url) | The registry URL of the upstream public registry to use as the source for the pull through cache rule | `string` | `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [iam\_role\_arn](#output\_iam\_role\_arn) | IAM role ARN | +| [iam\_role\_name](#output\_iam\_role\_name) | IAM role name | +| [iam\_role\_unique\_id](#output\_iam\_role\_unique\_id) | Stable and unique string identifying the IAM role | + + +## License + +Apache-2.0 Licensed. See [LICENSE](https://github.com/terraform-aws-modules/terraform-aws-ecr/blob/master/LICENSE). diff --git a/modules/repository-template/main.tf b/modules/repository-template/main.tf new file mode 100644 index 0000000..552b5c9 --- /dev/null +++ b/modules/repository-template/main.tf @@ -0,0 +1,281 @@ +data "aws_caller_identity" "current" { + count = var.create ? 1 : 0 +} + +data "aws_partition" "current" { + count = var.create ? 1 : 0 +} + +################################################################################ +# Repository Template +################################################################################ + +locals { + kms_encrypt = var.encryption_type == "KMS" +} + +resource "aws_ecr_repository_creation_template" "this" { + count = var.create ? 1 : 0 + + applied_for = var.applied_for + custom_role_arn = local.create_iam_role ? aws_iam_role.this[0].arn : var.custom_role_arn + description = var.description + + dynamic "encryption_configuration" { + for_each = var.encryption_type != null ? [1] : [] + + content { + encryption_type = var.encryption_type + kms_key = local.kms_encrypt ? var.kms_key_arn : null + } + } + + image_tag_mutability = var.image_tag_mutability + lifecycle_policy = var.lifecycle_policy + prefix = var.prefix + repository_policy = var.create_repository_policy ? data.aws_iam_policy_document.repository[0].json : var.repository_policy + + resource_tags = var.resource_tags +} + +################################################################################ +# Repository Policy Document +################################################################################ + +data "aws_iam_policy_document" "repository" { + count = var.create && var.create_repository_policy ? 1 : 0 + + statement { + sid = "PrivateReadOnly" + + principals { + type = "AWS" + identifiers = coalescelist( + concat(var.repository_read_access_arns, var.repository_read_write_access_arns), + ["arn:${data.aws_partition.current[0].partition}:iam::${data.aws_caller_identity.current[0].account_id}:root"], + ) + } + + actions = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:DescribeImageScanFindings", + "ecr:DescribeImages", + "ecr:DescribeRepositories", + "ecr:GetDownloadUrlForLayer", + "ecr:GetLifecyclePolicy", + "ecr:GetLifecyclePolicyPreview", + "ecr:GetRepositoryPolicy", + "ecr:ListImages", + "ecr:ListTagsForResource", + ] + } + + dynamic "statement" { + for_each = length(var.repository_lambda_read_access_arns) > 0 ? [1] : [] + + content { + sid = "PrivateLambdaReadOnly" + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + + actions = [ + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer", + ] + + condition { + test = "StringLike" + variable = "aws:sourceArn" + + values = var.repository_lambda_read_access_arns + } + } + } + + dynamic "statement" { + for_each = length(var.repository_read_write_access_arns) > 0 ? [var.repository_read_write_access_arns] : [] + + content { + sid = "ReadWrite" + + principals { + type = "AWS" + identifiers = statement.value + } + + actions = [ + "ecr:PutImage", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + ] + } + } + + dynamic "statement" { + for_each = length(var.repository_read_write_access_arns) > 0 ? [var.repository_read_write_access_arns] : [] + + content { + sid = "ReadWrite" + + principals { + type = "AWS" + identifiers = statement.value + } + + actions = [ + "ecr-public:BatchCheckLayerAvailability", + "ecr-public:CompleteLayerUpload", + "ecr-public:InitiateLayerUpload", + "ecr-public:PutImage", + "ecr-public:UploadLayerPart", + ] + } + } + + dynamic "statement" { + for_each = var.repository_policy_statements + + content { + sid = try(statement.value.sid, null) + actions = try(statement.value.actions, null) + not_actions = try(statement.value.not_actions, null) + effect = try(statement.value.effect, null) + resources = try(statement.value.resources, null) + not_resources = try(statement.value.not_resources, null) + + dynamic "principals" { + for_each = try(statement.value.principals, []) + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = try(statement.value.not_principals, []) + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = try(statement.value.conditions, []) + + content { + test = condition.value.test + values = condition.value.values + variable = condition.value.variable + } + } + } + } +} + +################################################################################ +# Registry Pull Through Cache Rule +################################################################################ + +resource "aws_ecr_pull_through_cache_rule" "this" { + count = var.create && var.create_pull_through_cache_rule ? 1 : 0 + + credential_arn = var.credential_arn + ecr_repository_prefix = var.prefix + upstream_registry_url = var.upstream_registry_url +} + +################################################################################ +# IAM Role +################################################################################ + +locals { + create_iam_role = var.create && var.create_iam_role && (local.kms_encrypt || length(var.resource_tags) > 0) + iam_role_name = try(coalesce(var.iam_role_name, var.prefix), "") + + perm_prefix = var.prefix != "ROOT" ? "${var.prefix}/*" : "*" +} + +data "aws_iam_policy_document" "assume" { + count = local.create_iam_role ? 1 : 0 + + statement { + sid = "ECRServiceAssumeRole" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecr.amazonaws.com"] + } + } +} + +resource "aws_iam_role" "this" { + count = local.create_iam_role ? 1 : 0 + + name = var.iam_role_use_name_prefix ? null : local.iam_role_name + name_prefix = var.iam_role_use_name_prefix ? "${local.iam_role_name}-" : null + path = var.iam_role_path + description = var.iam_role_description + + assume_role_policy = data.aws_iam_policy_document.assume[0].json + permissions_boundary = var.iam_role_permissions_boundary + force_detach_policies = true + + tags = merge(var.tags, var.iam_role_tags) +} + +data "aws_iam_policy_document" "this" { + count = local.create_iam_role ? 1 : 0 + + statement { + sid = "TagResource" + actions = [ + "ecr:CreateRepository", + "ecr:ReplicateImage", + "ecr:TagResource" + ] + resources = [ + "arn:${data.aws_partition.current[0].partition}:ecr::${data.aws_caller_identity.current[0].account_id}:repository/${local.perm_prefix}" + ] + } + + dynamic "statement" { + for_each = local.kms_encrypt ? [1] : [] + + content { + sid = "KMSUsage" + actions = [ + "kms:CreateGrant", + "kms:RetireGrant", + "kms:DescribeKey", + ] + resources = [var.kms_key_arn] + } + } +} + +resource "aws_iam_policy" "this" { + count = local.create_iam_role ? 1 : 0 + + name = var.iam_role_use_name_prefix ? null : local.iam_role_name + name_prefix = var.iam_role_use_name_prefix ? "${local.iam_role_name}-" : null + description = coalesce(var.iam_role_description, "ECR service policy that allows Amazon ECR to make calls to tag resources and use KMS encryption key(s) on your behalf") + policy = data.aws_iam_policy_document.this[0].json + + tags = merge(var.tags, var.iam_role_tags) +} + +resource "aws_iam_role_policy_attachment" "this" { + count = local.create_iam_role ? 1 : 0 + + role = aws_iam_role.this[0].name + policy_arn = aws_iam_policy.this[0].arn +} diff --git a/modules/repository-template/outputs.tf b/modules/repository-template/outputs.tf new file mode 100644 index 0000000..2812734 --- /dev/null +++ b/modules/repository-template/outputs.tf @@ -0,0 +1,18 @@ +################################################################################ +# IAM Role +################################################################################ + +output "iam_role_name" { + description = "IAM role name" + value = try(aws_iam_role.this[0].name, null) +} + +output "iam_role_arn" { + description = "IAM role ARN" + value = try(aws_iam_role.this[0].arn, var.custom_role_arn) +} + +output "iam_role_unique_id" { + description = "Stable and unique string identifying the IAM role" + value = try(aws_iam_role.this[0].unique_id, null) +} diff --git a/modules/repository-template/variables.tf b/modules/repository-template/variables.tf new file mode 100644 index 0000000..ee84672 --- /dev/null +++ b/modules/repository-template/variables.tf @@ -0,0 +1,177 @@ +variable "create" { + description = "Determines whether resources will be created (affects all resources)" + type = bool + default = true +} + +variable "tags" { + description = "A map of tags to add to all resources" + type = map(string) + default = {} +} + +################################################################################ +# Repository Template +################################################################################ + +variable "applied_for" { + description = "Which features this template applies to. Must contain one or more of `PULL_THROUGH_CACHE` or `REPLICATION`. Defaults to `PULL_THROUGH_CACHE`" + type = list(string) + default = ["PULL_THROUGH_CACHE"] +} + +variable "custom_role_arn" { + description = "A custom IAM role to use for repository creation. Required if using repository tags or KMS encryption" + type = string + default = null +} + +variable "description" { + description = "The description for this template" + type = string + default = null +} + +variable "encryption_type" { + description = "The type of encryption to use for any created repositories. Must be one of: `AES256` or `KMS`. Defaults to `AES256`" + type = string + default = "AES256" +} + +variable "kms_key_arn" { + description = "The ARN of the KMS key used to encrypt the repositories created" + type = string + default = null +} + +variable "image_tag_mutability" { + description = "The tag mutability setting for any created repositories. Must be one of: `MUTABLE` or `IMMUTABLE`. Defaults to `IMMUTABLE`" + type = string + default = "IMMUTABLE" +} + +variable "lifecycle_policy" { + description = "The lifecycle policy document to apply to any created repositories" + type = string + default = null +} + +variable "prefix" { + description = "(Required) The repository name prefix to match against. Use `ROOT` to match any prefix that doesn't explicitly match another template" + type = string + default = "" +} + +variable "repository_policy" { + description = "The JSON policy to apply to the repository. If not specified, uses the default policy" + type = string + default = null +} + +variable "resource_tags" { + description = "A map of tags to assign to any created repositories" + type = map(string) + default = {} +} + +################################################################################ +# Registry Pull Through Cache Rule +################################################################################ + +variable "create_pull_through_cache_rule" { + description = "Determines whether a pull through cache rule will be created" + type = bool + default = false +} + +variable "credential_arn" { + description = "ARN of the Secret which will be used to authenticate against the registry to use for the pull through cache rule" + type = string + default = null +} + +variable "upstream_registry_url" { + description = "The registry URL of the upstream public registry to use as the source for the pull through cache rule" + type = string + default = null +} + +################################################################################ +# Repository Policy +################################################################################ + +variable "create_repository_policy" { + description = "Determines whether a repository policy will be created" + type = bool + default = true +} + +variable "repository_read_access_arns" { + description = "The ARNs of the IAM users/roles that have read access to the repository" + type = list(string) + default = [] +} + +variable "repository_lambda_read_access_arns" { + description = "The ARNs of the Lambda service roles that have read access to the repository" + type = list(string) + default = [] +} + +variable "repository_read_write_access_arns" { + description = "The ARNs of the IAM users/roles that have read/write access to the repository" + type = list(string) + default = [] +} + +variable "repository_policy_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = any + default = {} +} + +################################################################################ +# IAM Role +################################################################################ + +variable "create_iam_role" { + description = "Determines whether the ECR service IAM role should be created" + type = bool + default = true +} + +variable "iam_role_name" { + description = "Name to use on IAM role created" + type = string + default = null +} + +variable "iam_role_use_name_prefix" { + description = "Determines whether the IAM role name (`iam_role_name`) is used as a prefix" + type = bool + default = true +} + +variable "iam_role_path" { + description = "IAM role path" + type = string + default = null +} + +variable "iam_role_description" { + description = "Description of the role" + type = string + default = null +} + +variable "iam_role_permissions_boundary" { + description = "ARN of the policy that is used to set the permissions boundary for the IAM role" + type = string + default = null +} + +variable "iam_role_tags" { + description = "A map of additional tags to add to the IAM role created" + type = map(string) + default = {} +} diff --git a/modules/repository-template/versions.tf b/modules/repository-template/versions.tf new file mode 100644 index 0000000..97e87e8 --- /dev/null +++ b/modules/repository-template/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.61" + } + } +} diff --git a/versions.tf b/versions.tf index 0b1e951..97e87e8 100644 --- a/versions.tf +++ b/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.37" + version = ">= 5.61" } } } diff --git a/wrappers/README.md b/wrappers/README.md index 343fd97..d49fe1b 100644 --- a/wrappers/README.md +++ b/wrappers/README.md @@ -98,3 +98,39 @@ inputs = { } } ``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 5.61 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [wrapper](#module\_wrapper) | ../ | n/a | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [defaults](#input\_defaults) | Map of default values which will be used for each item. | `any` | `{}` | no | +| [items](#input\_items) | Maps of items to create a wrapper from. Values are passed through to the module. | `any` | `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [wrapper](#output\_wrapper) | Map of outputs of a wrapper. | + diff --git a/wrappers/repository-template/README.md b/wrappers/repository-template/README.md new file mode 100644 index 0000000..13ab39a --- /dev/null +++ b/wrappers/repository-template/README.md @@ -0,0 +1,136 @@ +# Wrapper for module: `modules/repository-template` + +The configuration in this directory contains an implementation of a single module wrapper pattern, which allows managing several copies of a module in places where using the native Terraform 0.13+ `for_each` feature is not feasible (e.g., with Terragrunt). + +You may want to use a single Terragrunt configuration file to manage multiple resources without duplicating `terragrunt.hcl` files for each copy of the same module. + +This wrapper does not implement any extra functionality. + +## Usage with Terragrunt + +`terragrunt.hcl`: + +```hcl +terraform { + source = "tfr:///terraform-aws-modules/ecr/aws//wrappers/repository-template" + # Alternative source: + # source = "git::git@github.com:terraform-aws-modules/terraform-aws-ecr.git//wrappers/repository-template?ref=master" +} + +inputs = { + defaults = { # Default values + create = true + tags = { + Terraform = "true" + Environment = "dev" + } + } + + items = { + my-item = { + # omitted... can be any argument supported by the module + } + my-second-item = { + # omitted... can be any argument supported by the module + } + # omitted... + } +} +``` + +## Usage with Terraform + +```hcl +module "wrapper" { + source = "terraform-aws-modules/ecr/aws//wrappers/repository-template" + + defaults = { # Default values + create = true + tags = { + Terraform = "true" + Environment = "dev" + } + } + + items = { + my-item = { + # omitted... can be any argument supported by the module + } + my-second-item = { + # omitted... can be any argument supported by the module + } + # omitted... + } +} +``` + +## Example: Manage multiple S3 buckets in one Terragrunt layer + +`eu-west-1/s3-buckets/terragrunt.hcl`: + +```hcl +terraform { + source = "tfr:///terraform-aws-modules/s3-bucket/aws//wrappers" + # Alternative source: + # source = "git::git@github.com:terraform-aws-modules/terraform-aws-s3-bucket.git//wrappers?ref=master" +} + +inputs = { + defaults = { + force_destroy = true + + attach_elb_log_delivery_policy = true + attach_lb_log_delivery_policy = true + attach_deny_insecure_transport_policy = true + attach_require_latest_tls_policy = true + } + + items = { + bucket1 = { + bucket = "my-random-bucket-1" + } + bucket2 = { + bucket = "my-random-bucket-2" + tags = { + Secure = "probably" + } + } + } +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | >= 5.61 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [wrapper](#module\_wrapper) | ../../modules/repository-template | n/a | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [defaults](#input\_defaults) | Map of default values which will be used for each item. | `any` | `{}` | no | +| [items](#input\_items) | Maps of items to create a wrapper from. Values are passed through to the module. | `any` | `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [wrapper](#output\_wrapper) | Map of outputs of a wrapper. | + diff --git a/wrappers/repository-template/main.tf b/wrappers/repository-template/main.tf new file mode 100644 index 0000000..402ef2e --- /dev/null +++ b/wrappers/repository-template/main.tf @@ -0,0 +1,33 @@ +module "wrapper" { + source = "../../modules/repository-template" + + for_each = var.items + + applied_for = try(each.value.applied_for, var.defaults.applied_for, ["PULL_THROUGH_CACHE"]) + create = try(each.value.create, var.defaults.create, true) + create_iam_role = try(each.value.create_iam_role, var.defaults.create_iam_role, true) + create_pull_through_cache_rule = try(each.value.create_pull_through_cache_rule, var.defaults.create_pull_through_cache_rule, false) + create_repository_policy = try(each.value.create_repository_policy, var.defaults.create_repository_policy, true) + credential_arn = try(each.value.credential_arn, var.defaults.credential_arn, null) + custom_role_arn = try(each.value.custom_role_arn, var.defaults.custom_role_arn, null) + description = try(each.value.description, var.defaults.description, null) + encryption_type = try(each.value.encryption_type, var.defaults.encryption_type, "AES256") + iam_role_description = try(each.value.iam_role_description, var.defaults.iam_role_description, null) + iam_role_name = try(each.value.iam_role_name, var.defaults.iam_role_name, null) + iam_role_path = try(each.value.iam_role_path, var.defaults.iam_role_path, null) + iam_role_permissions_boundary = try(each.value.iam_role_permissions_boundary, var.defaults.iam_role_permissions_boundary, null) + iam_role_tags = try(each.value.iam_role_tags, var.defaults.iam_role_tags, {}) + iam_role_use_name_prefix = try(each.value.iam_role_use_name_prefix, var.defaults.iam_role_use_name_prefix, true) + image_tag_mutability = try(each.value.image_tag_mutability, var.defaults.image_tag_mutability, "IMMUTABLE") + kms_key_arn = try(each.value.kms_key_arn, var.defaults.kms_key_arn, null) + lifecycle_policy = try(each.value.lifecycle_policy, var.defaults.lifecycle_policy, null) + prefix = try(each.value.prefix, var.defaults.prefix, "") + repository_lambda_read_access_arns = try(each.value.repository_lambda_read_access_arns, var.defaults.repository_lambda_read_access_arns, []) + repository_policy = try(each.value.repository_policy, var.defaults.repository_policy, null) + repository_policy_statements = try(each.value.repository_policy_statements, var.defaults.repository_policy_statements, {}) + repository_read_access_arns = try(each.value.repository_read_access_arns, var.defaults.repository_read_access_arns, []) + repository_read_write_access_arns = try(each.value.repository_read_write_access_arns, var.defaults.repository_read_write_access_arns, []) + resource_tags = try(each.value.resource_tags, var.defaults.resource_tags, {}) + tags = try(each.value.tags, var.defaults.tags, {}) + upstream_registry_url = try(each.value.upstream_registry_url, var.defaults.upstream_registry_url, null) +} diff --git a/wrappers/repository-template/outputs.tf b/wrappers/repository-template/outputs.tf new file mode 100644 index 0000000..ec6da5f --- /dev/null +++ b/wrappers/repository-template/outputs.tf @@ -0,0 +1,5 @@ +output "wrapper" { + description = "Map of outputs of a wrapper." + value = module.wrapper + # sensitive = false # No sensitive module output found +} diff --git a/wrappers/repository-template/variables.tf b/wrappers/repository-template/variables.tf new file mode 100644 index 0000000..a6ea096 --- /dev/null +++ b/wrappers/repository-template/variables.tf @@ -0,0 +1,11 @@ +variable "defaults" { + description = "Map of default values which will be used for each item." + type = any + default = {} +} + +variable "items" { + description = "Maps of items to create a wrapper from. Values are passed through to the module." + type = any + default = {} +} diff --git a/wrappers/repository-template/versions.tf b/wrappers/repository-template/versions.tf new file mode 100644 index 0000000..97e87e8 --- /dev/null +++ b/wrappers/repository-template/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.61" + } + } +} diff --git a/wrappers/versions.tf b/wrappers/versions.tf index 0b1e951..97e87e8 100644 --- a/wrappers/versions.tf +++ b/wrappers/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.37" + version = ">= 5.61" } } }