From 43e6c646449882c44ad5cacff0181a209a2e63d5 Mon Sep 17 00:00:00 2001 From: Vladimir Djurovic Date: Fri, 12 Dec 2025 18:15:02 +0100 Subject: [PATCH 1/5] feat: additional ECR configuration --- easy-ecr/README.md | 3 ++ easy-ecr/main.tf | 2 +- easy-ecr/scan_config.tf | 17 +++++++++- easy-ecr/tests/scan_config.tftest.hcl | 48 +++++++++++++++++++++++++++ easy-ecr/variables.tf | 11 ++++++ 5 files changed, 79 insertions(+), 2 deletions(-) diff --git a/easy-ecr/README.md b/easy-ecr/README.md index 7ed30b0..aeb1b7c 100644 --- a/easy-ecr/README.md +++ b/easy-ecr/README.md @@ -37,6 +37,8 @@ This Terraform module provides production-ready ECR repository for storing conta |**Description:** || | [aws_ecr_registry_policy.registry_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_registry_policy) | resource | |**Description:** || +| [aws_ecr_registry_scanning_configuration.registry_scan_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_registry_scanning_configuration) | resource | +|**Description:** || | [aws_ecr_repository.ecr_private_repo](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository) | resource | |**Description:** || | [aws_ecr_repository_policy.repo_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_policy) | resource | @@ -70,6 +72,7 @@ This Terraform module provides production-ready ECR repository for storing conta | [pullthrough\_cache\_rules](#input\_pullthrough\_cache\_rules) | List of custom pullthrough cache rules to apply to repository |
list(object({
ecr_repository_prefix = optional(string, "ROOT")
upstream_repository_prefix = optional(string, "ROOT")
credential_arn = optional(string, null)
custom_role_arn = optional(string, null)
upstream_registry_url = string
}))
| `[]` | no | | [quay\_pullthrough\_cache\_rule](#input\_quay\_pullthrough\_cache\_rule) | Pullthrough cache rule for Quay public registry. Override default values to customize |
object({
enabled = optional(bool, false)
ecr_repository_prefix = optional(string, "ROOT")
upstream_repository_prefix = optional(string, "ROOT")
})
| `{}` | no | | [registry\_policy\_path](#input\_registry\_policy\_path) | Path to JSON policy file (optional). If specified, policy will be applied to registry | `string` | `null` | no | +| [registry\_scan\_configuration](#input\_registry\_scan\_configuration) | n/a |
object({
type = optional(string, "BASIC")
rules = optional(list(object({
frequency = optional(string, "SCAN_ON_PUSH")
filter = optional(string, "*")
})), [])
})
| `{}` | no | | [repo\_policy\_path](#input\_repo\_policy\_path) | Path to JSON policy file (optional). If specified, policy will be applied to repository | `string` | `null` | no | | [repository\_name](#input\_repository\_name) | Name of the repository | `string` | n/a | yes | | [scan\_images\_on\_push](#input\_scan\_images\_on\_push) | Whether images are scanned after being pushed to the repository (true) or not scanned (false). Default is true. | `bool` | `true` | no | diff --git a/easy-ecr/main.tf b/easy-ecr/main.tf index f7c1494..b34a071 100644 --- a/easy-ecr/main.tf +++ b/easy-ecr/main.tf @@ -6,7 +6,7 @@ locals { resolved_region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region should_create_kms_key = (!var.use_default_ecnryption_key && var.encryption_key_arn == null) ? true : false - image_tag_mutability = var.image_tag_mutable ? (length(var.mutability_exclusion_filters) > 0 ? "MUTABLE_WITH_EXCLUSION" : "MUTABLE") : (length(var.mutability_exclusion_filters) > 0 ? "IMMUTABLE_WITH_EXCLUSION" : "IMUTABLE") + image_tag_mutability = var.image_tag_mutable ? (length(var.mutability_exclusion_filters) > 0 ? "MUTABLE_WITH_EXCLUSION" : "MUTABLE") : (length(var.mutability_exclusion_filters) > 0 ? "IMMUTABLE_WITH_EXCLUSION" : "IMMUTABLE") } data "aws_region" "current_region" {} diff --git a/easy-ecr/scan_config.tf b/easy-ecr/scan_config.tf index 63c3270..699c7ac 100644 --- a/easy-ecr/scan_config.tf +++ b/easy-ecr/scan_config.tf @@ -5,4 +5,19 @@ resource "aws_ecr_account_setting" "account_scan_config" { name = var.default_account_scan_config.name value = var.default_account_scan_config.value -} \ No newline at end of file +} + +resource "aws_ecr_registry_scanning_configuration" "registry_scan_config" { + region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region + scan_type = var.registry_scan_configuration.type + dynamic "rule" { + for_each = var.registry_scan_configuration.rules + content { + scan_frequency = rule.value.frequency + repository_filter { + filter = rule.value.filter + filter_type = "WILDCARD" + } + } + } +} diff --git a/easy-ecr/tests/scan_config.tftest.hcl b/easy-ecr/tests/scan_config.tftest.hcl index c2a942a..fb47657 100644 --- a/easy-ecr/tests/scan_config.tftest.hcl +++ b/easy-ecr/tests/scan_config.tftest.hcl @@ -44,3 +44,51 @@ run "default_scan_config_is_overriden_when_specified" { error_message = "Scan configuration resource was not created with correct value" } } + +run "scan_configuration_is_basic_by_default" { + command = apply + + variables { + repository_name = "test-repo" + } + + assert { + condition = aws_ecr_registry_scanning_configuration.registry_scan_config.scan_type == "BASIC" + error_message = "Scanning configuration is not BASIC" + } + assert { + condition = length(aws_ecr_registry_scanning_configuration.registry_scan_config.rule) == 0 + error_message = "Rules list is not empty" + } +} + +run "scan_configuration_overrides_defaults" { + command = apply + + variables { + repository_name = "test-repo" + registry_scan_configuration = { + type = "ENHANCED" + rules = [ + { + frequency = "CONTINUOUS_SCAN" + filter = "my-filter" + } + ] + } + } + + assert { + condition = aws_ecr_registry_scanning_configuration.registry_scan_config.scan_type == "ENHANCED" + error_message = "Scanning configuration is not ENHANCED" + } + assert { + condition = length(aws_ecr_registry_scanning_configuration.registry_scan_config.rule) == 1 + error_message = "Rules list is empty" + } + + assert { + condition = length([for r in aws_ecr_registry_scanning_configuration.registry_scan_config.rule : r if r.scan_frequency == "CONTINUOUS_SCAN"]) == 1 + error_message = "Invalid scan configuration" + } +} diff --git a/easy-ecr/variables.tf b/easy-ecr/variables.tf index 18ab90e..eaa9050 100644 --- a/easy-ecr/variables.tf +++ b/easy-ecr/variables.tf @@ -213,6 +213,17 @@ variable "default_account_scan_config" { description = "Default ECR basic scan type configuration." } +variable "registry_scan_configuration" { + type = object({ + type = optional(string, "BASIC") + rules = optional(list(object({ + frequency = optional(string, "SCAN_ON_PUSH") + filter = optional(string, "*") + })), []) + }) + default = {} +} + variable "tags" { type = map(string) default = {} From c1027022879fde352b3a62d716b7040711a33b51 Mon Sep 17 00:00:00 2001 From: Vladimir Djurovic Date: Fri, 12 Dec 2025 18:37:53 +0100 Subject: [PATCH 2/5] add replication config --- easy-ecr/README.md | 5 ++++- easy-ecr/replication.tf | 29 +++++++++++++++++++++++++++++ easy-ecr/variables.tf | 12 +++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 easy-ecr/replication.tf diff --git a/easy-ecr/README.md b/easy-ecr/README.md index aeb1b7c..8d346f9 100644 --- a/easy-ecr/README.md +++ b/easy-ecr/README.md @@ -39,6 +39,8 @@ This Terraform module provides production-ready ECR repository for storing conta |**Description:** || | [aws_ecr_registry_scanning_configuration.registry_scan_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_registry_scanning_configuration) | resource | |**Description:** || +| [aws_ecr_replication_configuration.replication_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_replication_configuration) | resource | +|**Description:** || | [aws_ecr_repository.ecr_private_repo](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository) | resource | |**Description:** || | [aws_ecr_repository_policy.repo_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_policy) | resource | @@ -72,7 +74,8 @@ This Terraform module provides production-ready ECR repository for storing conta | [pullthrough\_cache\_rules](#input\_pullthrough\_cache\_rules) | List of custom pullthrough cache rules to apply to repository |
list(object({
ecr_repository_prefix = optional(string, "ROOT")
upstream_repository_prefix = optional(string, "ROOT")
credential_arn = optional(string, null)
custom_role_arn = optional(string, null)
upstream_registry_url = string
}))
| `[]` | no | | [quay\_pullthrough\_cache\_rule](#input\_quay\_pullthrough\_cache\_rule) | Pullthrough cache rule for Quay public registry. Override default values to customize |
object({
enabled = optional(bool, false)
ecr_repository_prefix = optional(string, "ROOT")
upstream_repository_prefix = optional(string, "ROOT")
})
| `{}` | no | | [registry\_policy\_path](#input\_registry\_policy\_path) | Path to JSON policy file (optional). If specified, policy will be applied to registry | `string` | `null` | no | -| [registry\_scan\_configuration](#input\_registry\_scan\_configuration) | n/a |
object({
type = optional(string, "BASIC")
rules = optional(list(object({
frequency = optional(string, "SCAN_ON_PUSH")
filter = optional(string, "*")
})), [])
})
| `{}` | no | +| [registry\_scan\_configuration](#input\_registry\_scan\_configuration) | Registry scanning configuration |
object({
type = optional(string, "BASIC")
rules = optional(list(object({
frequency = optional(string, "SCAN_ON_PUSH")
filter = optional(string, "*")
})), [])
})
| `{}` | no | +| [replication\_config](#input\_replication\_config) | Registry replication configuration |
list(object({
region = string
filter = optional(string, null)
}))
| `[]` | no | | [repo\_policy\_path](#input\_repo\_policy\_path) | Path to JSON policy file (optional). If specified, policy will be applied to repository | `string` | `null` | no | | [repository\_name](#input\_repository\_name) | Name of the repository | `string` | n/a | yes | | [scan\_images\_on\_push](#input\_scan\_images\_on\_push) | Whether images are scanned after being pushed to the repository (true) or not scanned (false). Default is true. | `bool` | `true` | no | diff --git a/easy-ecr/replication.tf b/easy-ecr/replication.tf new file mode 100644 index 0000000..44442bb --- /dev/null +++ b/easy-ecr/replication.tf @@ -0,0 +1,29 @@ +# Copyright 2025 Bitshift +# SPDX-License-Identifier: MPL-2.0 + + + +data "aws_caller_identity" "current" {} + +resource "aws_ecr_replication_configuration" "replication_config" { + count = length(var.replication_config) > 0 ? 1 : 0 + region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region + replication_configuration { + rule { + dynamic "destination" { + for_each = var.replication_config + content { + region = destination.value.region + registry_id = data.aws_caller_identity.current.account_id + } + } + dynamic "repository_filter" { + for_each = [for r in var.replication_config : r if r.filter != null] + content { + filter = repository_filter.value.filter + filter_type = "PREFIX_MATCH" + } + } + } + } +} \ No newline at end of file diff --git a/easy-ecr/variables.tf b/easy-ecr/variables.tf index eaa9050..98916d5 100644 --- a/easy-ecr/variables.tf +++ b/easy-ecr/variables.tf @@ -221,7 +221,17 @@ variable "registry_scan_configuration" { filter = optional(string, "*") })), []) }) - default = {} + default = {} + description = "Registry scanning configuration" +} + +variable "replication_config" { + type = list(object({ + region = string + filter = optional(string, null) + })) + default = [] + description = "Registry replication configuration" } variable "tags" { From cdd2362308cd62e2abf592a9e5a8f3dda69f8327 Mon Sep 17 00:00:00 2001 From: Vladimir Djurovic Date: Fri, 12 Dec 2025 23:51:37 +0100 Subject: [PATCH 3/5] add test for replication config --- easy-ecr/README.md | 2 +- easy-ecr/replication.tf | 2 + easy-ecr/tests/replication_config.tftest.hcl | 58 ++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 easy-ecr/tests/replication_config.tftest.hcl diff --git a/easy-ecr/README.md b/easy-ecr/README.md index 8d346f9..0d8e916 100644 --- a/easy-ecr/README.md +++ b/easy-ecr/README.md @@ -40,7 +40,7 @@ This Terraform module provides production-ready ECR repository for storing conta | [aws_ecr_registry_scanning_configuration.registry_scan_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_registry_scanning_configuration) | resource | |**Description:** || | [aws_ecr_replication_configuration.replication_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_replication_configuration) | resource | -|**Description:** || +|**Description:** Defines registry replication configuration. Current implementation allows only replication withing the same AWS account. It is possible to define rul;e filters for replication. || | [aws_ecr_repository.ecr_private_repo](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository) | resource | |**Description:** || | [aws_ecr_repository_policy.repo_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_policy) | resource | diff --git a/easy-ecr/replication.tf b/easy-ecr/replication.tf index 44442bb..3839979 100644 --- a/easy-ecr/replication.tf +++ b/easy-ecr/replication.tf @@ -5,6 +5,8 @@ data "aws_caller_identity" "current" {} +# Defines registry replication configuration. Current implementation allows only replication +# withing the same AWS account. It is possible to define rul;e filters for replication. resource "aws_ecr_replication_configuration" "replication_config" { count = length(var.replication_config) > 0 ? 1 : 0 region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region diff --git a/easy-ecr/tests/replication_config.tftest.hcl b/easy-ecr/tests/replication_config.tftest.hcl new file mode 100644 index 0000000..bcbaecf --- /dev/null +++ b/easy-ecr/tests/replication_config.tftest.hcl @@ -0,0 +1,58 @@ +# Copyright 2025 Bitshift +# SPDX-License-Identifier: MPL-2.0 + +mock_provider "aws" { + override_data { + target = data.aws_caller_identity.current + values = { + account_id = "123456789012" + } + } +} + +run "no_replication_config_by_default" { + command = apply + + variables { + repository_name = "test-repo" + } + + assert { + condition = length(aws_ecr_replication_configuration.replication_config) == 0 + error_message = "Replication configuration is not empty" + } +} + +run "per_region_config_should_be_created" { + command = plan + + variables { + repository_name = "test-repo" + replication_config = [ + { + region = "us-east-1" + }, + { + region = "us-east-2" + filter = "my-filter" + } + ] + } + + assert { + condition = length(aws_ecr_replication_configuration.replication_config) == 1 + error_message = "Replication configuration is empty" + } + assert { + condition = aws_ecr_replication_configuration.replication_config[0].replication_configuration[0].rule[0].destination[0].region == "us-east-1" + error_message = "Invalid replication configuration" + } + assert { + condition = aws_ecr_replication_configuration.replication_config[0].replication_configuration[0].rule[0].destination[1].region == "us-east-2" + error_message = "Invalid replication configuration" + } + assert { + condition = aws_ecr_replication_configuration.replication_config[0].replication_configuration[0].rule[0].repository_filter[0].filter == "my-filter" + error_message = "Invalid replication configuration (filter)" + } +} \ No newline at end of file From 38d547905a6fce2d2b84c459071c3c7d32e8fe99 Mon Sep 17 00:00:00 2001 From: Vladimir Djurovic Date: Mon, 15 Dec 2025 15:24:38 +0100 Subject: [PATCH 4/5] added IAM roles --- easy-ecr/README.md | 12 +++- easy-ecr/outputs.tf | 13 ++++ easy-ecr/replication.tf | 2 +- easy-ecr/roles.tf | 121 ++++++++++++++++++++++++++++++++ easy-ecr/tests/roles.tftest.hcl | 49 +++++++++++++ easy-ecr/variables.tf | 12 ++++ 6 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 easy-ecr/outputs.tf create mode 100644 easy-ecr/roles.tf create mode 100644 easy-ecr/tests/roles.tftest.hcl diff --git a/easy-ecr/README.md b/easy-ecr/README.md index 0d8e916..109e68b 100644 --- a/easy-ecr/README.md +++ b/easy-ecr/README.md @@ -40,7 +40,7 @@ This Terraform module provides production-ready ECR repository for storing conta | [aws_ecr_registry_scanning_configuration.registry_scan_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_registry_scanning_configuration) | resource | |**Description:** || | [aws_ecr_replication_configuration.replication_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_replication_configuration) | resource | -|**Description:** Defines registry replication configuration. Current implementation allows only replication withing the same AWS account. It is possible to define rul;e filters for replication. || +|**Description:** Defines registry replication configuration. Current implementation allows only replication withing the same AWS account. It is possible to define rule filters for replication. || | [aws_ecr_repository.ecr_private_repo](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository) | resource | |**Description:** || | [aws_ecr_repository_policy.repo_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository_policy) | resource | @@ -49,6 +49,14 @@ This Terraform module provides production-ready ECR repository for storing conta |**Description:** || | [aws_ecrpublic_repository_policy.public_repo_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecrpublic_repository_policy) | resource | |**Description:** || +| [aws_iam_role.repo_push_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +|**Description:** Role which allows read/write access to repository || +| [aws_iam_role.repo_read_only_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +|**Description:** Role which allows read-only access to repository || +| [aws_iam_role_policy.push_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +|**Description:** IAM policy for role allowing read/write (push) access to repository || +| [aws_iam_role_policy.read_only_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +|**Description:** IAM policy for role allowing read-only (pull) access to repository || | [aws_kms_key.domain_encryption_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | |**Description:** || @@ -71,7 +79,9 @@ This Terraform module provides production-ready ECR repository for storing conta | [mutability\_exclusion\_filters](#input\_mutability\_exclusion\_filters) | List of tag prefixes to exclude from image tag mutability. Setting this will result in IMMUTABLE\_WITH\_EXLUSIONSo MUTABLE\_WITH\_EXCLUSION behavior. Only applicable for private repositories. | `list(string)` | `[]` | no | | [public\_catalog\_data](#input\_public\_catalog\_data) | Catalog data for public repositories (optional) |
object({
about = optional(string, ""),
description = optional(string, ""),
architectures = optional(list(string), []),
operating_systems = optional(list(string), []),
usage = optional(string, ""),
logo_image_path = optional(string, null)
})
| `{}` | no | | [public\_repo\_policy\_path](#input\_public\_repo\_policy\_path) | Path to JSON policy file (optional). If specified, policy will be applied to public repository. | `string` | `null` | no | +| [pull\_only\_principals](#input\_pull\_only\_principals) | List of principal ARNs who are allowed to assume role allowing pull access to repository | `list(string)` | `[]` | no | | [pullthrough\_cache\_rules](#input\_pullthrough\_cache\_rules) | List of custom pullthrough cache rules to apply to repository |
list(object({
ecr_repository_prefix = optional(string, "ROOT")
upstream_repository_prefix = optional(string, "ROOT")
credential_arn = optional(string, null)
custom_role_arn = optional(string, null)
upstream_registry_url = string
}))
| `[]` | no | +| [push\_principals](#input\_push\_principals) | List of principal ARNs who are allowed to assume role allowing read/write (push) access to repository | `list(string)` | `[]` | no | | [quay\_pullthrough\_cache\_rule](#input\_quay\_pullthrough\_cache\_rule) | Pullthrough cache rule for Quay public registry. Override default values to customize |
object({
enabled = optional(bool, false)
ecr_repository_prefix = optional(string, "ROOT")
upstream_repository_prefix = optional(string, "ROOT")
})
| `{}` | no | | [registry\_policy\_path](#input\_registry\_policy\_path) | Path to JSON policy file (optional). If specified, policy will be applied to registry | `string` | `null` | no | | [registry\_scan\_configuration](#input\_registry\_scan\_configuration) | Registry scanning configuration |
object({
type = optional(string, "BASIC")
rules = optional(list(object({
frequency = optional(string, "SCAN_ON_PUSH")
filter = optional(string, "*")
})), [])
})
| `{}` | no | diff --git a/easy-ecr/outputs.tf b/easy-ecr/outputs.tf new file mode 100644 index 0000000..cf407bd --- /dev/null +++ b/easy-ecr/outputs.tf @@ -0,0 +1,13 @@ +# Copyright 2025 Bitshift +# SPDX-License-Identifier: MPL-2.0 + +output "private_repository_url" { + value = length(aws_ecr_repository.ecr_private_repo) > 0 ? aws_ecr_repository.ecr_private_repo[0].repository_url : "" + description = "URL of the private repository" +} + +output "private_repository_arn" { + value = length(aws_ecr_repository.ecr_private_repo) > 0 ? aws_ecr_repository.ecr_private_repo[0].arn : "" + description = "ARN of the private repository" +} + diff --git a/easy-ecr/replication.tf b/easy-ecr/replication.tf index 3839979..903b6fc 100644 --- a/easy-ecr/replication.tf +++ b/easy-ecr/replication.tf @@ -6,7 +6,7 @@ data "aws_caller_identity" "current" {} # Defines registry replication configuration. Current implementation allows only replication -# withing the same AWS account. It is possible to define rul;e filters for replication. +# withing the same AWS account. It is possible to define rule filters for replication. resource "aws_ecr_replication_configuration" "replication_config" { count = length(var.replication_config) > 0 ? 1 : 0 region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region diff --git a/easy-ecr/roles.tf b/easy-ecr/roles.tf new file mode 100644 index 0000000..8742378 --- /dev/null +++ b/easy-ecr/roles.tf @@ -0,0 +1,121 @@ +# Copyright 2025 Bitshift +# SPDX-License-Identifier: MPL-2.0 + + + +################################################################################### +# +# Roles allowing read-only (pull) access to repository. +# +################################################################################### + +# IAM policy defining read-only access to repository +data "aws_iam_policy_document" "pull_only_policy_document" { + count = length(var.pull_only_principals) > 0 ? 1 : 0 + statement { + effect = "Allow" + actions = ["ecr:GetAuthorizationToken"] + resources = ["*"] + } + statement { + effect = "Allow" + actions = [ + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchImportUpstreamImage" + ] + resources = [aws_ecr_repository.ecr_private_repo[0].arn, "${aws_ecr_repository.ecr_private_repo[0].arn}/*"] + } +} + +# policy to allow assuming read-only role +data "aws_iam_policy_document" "assume_pull_role_document" { + count = length(var.pull_only_principals) > 0 ? 1 : 0 + statement { + effect = "Allow" + principals { + type = "AWS" + identifiers = var.pull_only_principals + } + actions = ["sts:AssumeRole"] + } +} + +# Role which allows read-only access to repository +resource "aws_iam_role" "repo_read_only_role" { + count = length(var.pull_only_principals) > 0 ? 1 : 0 + name = "ECRPullAccess-${var.repository_name}" + assume_role_policy = data.aws_iam_policy_document.assume_pull_role_document[0].json + tags = var.tags +} + +# IAM policy for role allowing read-only (pull) access to repository +resource "aws_iam_role_policy" "read_only_role_policy" { + count = length(var.pull_only_principals) > 0 ? 1 : 0 + policy = data.aws_iam_policy_document.pull_only_policy_document[0].json + role = aws_iam_role.repo_read_only_role[0].name +} + +################################################################################### +# +# Roles allowing read/write (push/pull) access to repository. +# +################################################################################### + +data "aws_iam_policy_document" "push_policy_document" { + count = length(var.push_principals) > 0 ? 1 : 0 + statement { + effect = "Allow" + actions = ["ecr:GetAuthorizationToken"] + resources = ["*"] + } + statement { + effect = "Allow" + actions = [ + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:GetRepositoryPolicy", + "ecr:DescribeRepositories", + "ecr:ListImages", + "ecr:DescribeImages", + "ecr:BatchGetImage", + "ecr:GetLifecyclePolicy", + "ecr:GetLifecyclePolicyPreview", + "ecr:ListTagsForResource", + "ecr:DescribeImageScanFindings", + "ecr:InitiateLayerUpload", + "ecr:UploadLayerPart", + "ecr:CompleteLayerUpload", + "ecr:PutImage" + ] + resources = [aws_ecr_repository.ecr_private_repo[0].arn, "${aws_ecr_repository.ecr_private_repo[0].arn}/*"] + } +} + +data "aws_iam_policy_document" "assume_push_role_document" { + count = length(var.push_principals) > 0 ? 1 : 0 + statement { + effect = "Allow" + principals { + type = "AWS" + identifiers = var.push_principals + } + actions = ["sts:AssumeRole"] + } +} + +# Role which allows read/write access to repository +resource "aws_iam_role" "repo_push_role" { + count = length(var.push_principals) > 0 ? 1 : 0 + name = "ECRPushAccess-${var.repository_name}" + assume_role_policy = data.aws_iam_policy_document.assume_push_role_document[0].json + tags = var.tags +} + +# IAM policy for role allowing read/write (push) access to repository +resource "aws_iam_role_policy" "push_role_policy" { + count = length(var.push_principals) > 0 ? 1 : 0 + policy = data.aws_iam_policy_document.push_policy_document[0].json + role = aws_iam_role.repo_push_role[0].name +} + diff --git a/easy-ecr/tests/roles.tftest.hcl b/easy-ecr/tests/roles.tftest.hcl new file mode 100644 index 0000000..5303e58 --- /dev/null +++ b/easy-ecr/tests/roles.tftest.hcl @@ -0,0 +1,49 @@ +# Copyright 2025 Bitshift +# SPDX-License-Identifier: MPL-2.0 + +mock_provider "aws" { + override_data { + target = data.aws_iam_policy_document.assume_pull_role_document + values = { + json = "{}" + } + } + override_data { + target = data.aws_iam_policy_document.pull_only_policy_document + values = { + json = "{}" + } + } +} + +run "roles_should_not_be_created_when_no_principals" { + command = apply + + variables { + repository_name = "test-repo" + } + + assert { + condition = length(aws_iam_role.repo_read_only_role) == 0 + error_message = "Read only role is created when no principals specified" + } + + assert { + condition = length(aws_iam_role.repo_push_role) == 0 + error_message = "Push is created when no principals specified" + } +} + +run "read_only_role_is_created_when_principals_are_specified" { + command = apply + + variables { + repository_name = "test-repo" + pull_only_principals = ["arn:aws:iam::/user/user1"] + } + + assert { + condition = length(aws_iam_role.repo_read_only_role) == 1 + error_message = "Read only role is notcreated when principals are specified" + } +} \ No newline at end of file diff --git a/easy-ecr/variables.tf b/easy-ecr/variables.tf index 98916d5..4c55929 100644 --- a/easy-ecr/variables.tf +++ b/easy-ecr/variables.tf @@ -234,6 +234,18 @@ variable "replication_config" { description = "Registry replication configuration" } +variable "pull_only_principals" { + type = list(string) + default = [] + description = "List of principal ARNs who are allowed to assume role allowing pull access to repository" +} + +variable "push_principals" { + type = list(string) + default = [] + description = "List of principal ARNs who are allowed to assume role allowing read/write (push) access to repository" +} + variable "tags" { type = map(string) default = {} From ae8b0cc17da8bd4501ad5cafbea6765def73fd89 Mon Sep 17 00:00:00 2001 From: Vladimir Djurovic Date: Tue, 16 Dec 2025 12:00:12 +0100 Subject: [PATCH 5/5] update permissions --- easy-ecr/README.md | 5 ++++- easy-ecr/outputs.tf | 6 +++--- easy-ecr/roles.tf | 7 ++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/easy-ecr/README.md b/easy-ecr/README.md index 109e68b..f9f2d79 100644 --- a/easy-ecr/README.md +++ b/easy-ecr/README.md @@ -96,7 +96,10 @@ This Terraform module provides production-ready ECR repository for storing conta ## Outputs -No outputs. +| Name | Description | +|------|-------------| +| [private\_repository\_arn](#output\_private\_repository\_arn) | ARN of the private repository | +| [private\_repository\_url](#output\_private\_repository\_url) | URL of the private repository | ## Examples diff --git a/easy-ecr/outputs.tf b/easy-ecr/outputs.tf index cf407bd..83ea141 100644 --- a/easy-ecr/outputs.tf +++ b/easy-ecr/outputs.tf @@ -2,12 +2,12 @@ # SPDX-License-Identifier: MPL-2.0 output "private_repository_url" { - value = length(aws_ecr_repository.ecr_private_repo) > 0 ? aws_ecr_repository.ecr_private_repo[0].repository_url : "" + value = length(aws_ecr_repository.ecr_private_repo) > 0 ? aws_ecr_repository.ecr_private_repo[0].repository_url : "" description = "URL of the private repository" } output "private_repository_arn" { - value = length(aws_ecr_repository.ecr_private_repo) > 0 ? aws_ecr_repository.ecr_private_repo[0].arn : "" - description = "ARN of the private repository" + value = length(aws_ecr_repository.ecr_private_repo) > 0 ? aws_ecr_repository.ecr_private_repo[0].arn : "" + description = "ARN of the private repository" } diff --git a/easy-ecr/roles.tf b/easy-ecr/roles.tf index 8742378..a8568dd 100644 --- a/easy-ecr/roles.tf +++ b/easy-ecr/roles.tf @@ -24,7 +24,7 @@ data "aws_iam_policy_document" "pull_only_policy_document" { "ecr:GetDownloadUrlForLayer", "ecr:BatchImportUpstreamImage" ] - resources = [aws_ecr_repository.ecr_private_repo[0].arn, "${aws_ecr_repository.ecr_private_repo[0].arn}/*"] + resources = ["arn:aws:ecr:${data.aws_region.current_region.region}:${data.aws_caller_identity.current.account_id}:repository/*"] } } @@ -86,9 +86,10 @@ data "aws_iam_policy_document" "push_policy_document" { "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload", - "ecr:PutImage" + "ecr:PutImage", + "ecr:BatchImportUpstreamImage" ] - resources = [aws_ecr_repository.ecr_private_repo[0].arn, "${aws_ecr_repository.ecr_private_repo[0].arn}/*"] + resources = ["arn:aws:ecr:${data.aws_region.current_region.region}:${data.aws_caller_identity.current.account_id}:repository/*"] } }