Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion easy-ecr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ 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_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 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 |
Expand All @@ -45,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:** ||

Expand All @@ -67,9 +79,13 @@ This Terraform module provides production-ready ECR repository for storing conta
| <a name="input_mutability_exclusion_filters"></a> [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 |
| <a name="input_public_catalog_data"></a> [public\_catalog\_data](#input\_public\_catalog\_data) | Catalog data for public repositories (optional) | <pre>object({<br/> about = optional(string, ""),<br/> description = optional(string, ""),<br/> architectures = optional(list(string), []),<br/> operating_systems = optional(list(string), []),<br/> usage = optional(string, ""),<br/> logo_image_path = optional(string, null)<br/> })</pre> | `{}` | no |
| <a name="input_public_repo_policy_path"></a> [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 |
| <a name="input_pull_only_principals"></a> [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 |
| <a name="input_pullthrough_cache_rules"></a> [pullthrough\_cache\_rules](#input\_pullthrough\_cache\_rules) | List of custom pullthrough cache rules to apply to repository | <pre>list(object({<br/> ecr_repository_prefix = optional(string, "ROOT")<br/> upstream_repository_prefix = optional(string, "ROOT")<br/> credential_arn = optional(string, null)<br/> custom_role_arn = optional(string, null)<br/> upstream_registry_url = string<br/> }))</pre> | `[]` | no |
| <a name="input_push_principals"></a> [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 |
| <a name="input_quay_pullthrough_cache_rule"></a> [quay\_pullthrough\_cache\_rule](#input\_quay\_pullthrough\_cache\_rule) | Pullthrough cache rule for Quay public registry. Override default values to customize | <pre>object({<br/> enabled = optional(bool, false)<br/> ecr_repository_prefix = optional(string, "ROOT")<br/> upstream_repository_prefix = optional(string, "ROOT")<br/> })</pre> | `{}` | no |
| <a name="input_registry_policy_path"></a> [registry\_policy\_path](#input\_registry\_policy\_path) | Path to JSON policy file (optional). If specified, policy will be applied to registry | `string` | `null` | no |
| <a name="input_registry_scan_configuration"></a> [registry\_scan\_configuration](#input\_registry\_scan\_configuration) | Registry scanning configuration | <pre>object({<br/> type = optional(string, "BASIC")<br/> rules = optional(list(object({<br/> frequency = optional(string, "SCAN_ON_PUSH")<br/> filter = optional(string, "*")<br/> })), [])<br/> })</pre> | `{}` | no |
| <a name="input_replication_config"></a> [replication\_config](#input\_replication\_config) | Registry replication configuration | <pre>list(object({<br/> region = string<br/> filter = optional(string, null)<br/> }))</pre> | `[]` | no |
| <a name="input_repo_policy_path"></a> [repo\_policy\_path](#input\_repo\_policy\_path) | Path to JSON policy file (optional). If specified, policy will be applied to repository | `string` | `null` | no |
| <a name="input_repository_name"></a> [repository\_name](#input\_repository\_name) | Name of the repository | `string` | n/a | yes |
| <a name="input_scan_images_on_push"></a> [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 |
Expand All @@ -80,7 +96,10 @@ This Terraform module provides production-ready ECR repository for storing conta

## Outputs

No outputs.
| Name | Description |
|------|-------------|
| <a name="output_private_repository_arn"></a> [private\_repository\_arn](#output\_private\_repository\_arn) | ARN of the private repository |
| <a name="output_private_repository_url"></a> [private\_repository\_url](#output\_private\_repository\_url) | URL of the private repository |

## Examples

Expand Down
2 changes: 1 addition & 1 deletion easy-ecr/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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" {}
Expand Down
13 changes: 13 additions & 0 deletions easy-ecr/outputs.tf
Original file line number Diff line number Diff line change
@@ -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"
}

31 changes: 31 additions & 0 deletions easy-ecr/replication.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2025 Bitshift
# SPDX-License-Identifier: MPL-2.0



data "aws_caller_identity" "current" {}

# Defines registry replication configuration. Current implementation allows only 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
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"
}
}
}
}
}
122 changes: 122 additions & 0 deletions easy-ecr/roles.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# 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 = ["arn:aws:ecr:${data.aws_region.current_region.region}:${data.aws_caller_identity.current.account_id}:repository/*"]
}
}

# 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",
"ecr:BatchImportUpstreamImage"
]
resources = ["arn:aws:ecr:${data.aws_region.current_region.region}:${data.aws_caller_identity.current.account_id}:repository/*"]
}
}

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
}

17 changes: 16 additions & 1 deletion easy-ecr/scan_config.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

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"
}
}
}
}
58 changes: 58 additions & 0 deletions easy-ecr/tests/replication_config.tftest.hcl
Original file line number Diff line number Diff line change
@@ -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)"
}
}
49 changes: 49 additions & 0 deletions easy-ecr/tests/roles.tftest.hcl
Original file line number Diff line number Diff line change
@@ -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"
}
}
Loading
Loading