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
47 changes: 47 additions & 0 deletions .github/workflows/easy-ecr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
name: easy-ecr - verify and build

on:
push:
branches:
- main
paths:
- 'easy-ecr/**'
pull_request:
branches:
- main
paths:
- 'easy-ecr/**'

jobs:
test-and-verify:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.14.0
- name: Setup tflint
uses: terraform-linters/setup-tflint@v6
with:
tflint_version: 'v0.60.0'
- name: Install Checkov
run: |
pip install checkov
- name: Run verification
run: |
make codeartifact-repo-verify
release:
needs: test-and-verify
uses: ./.github/workflows/common-release.yaml
with:
tag-prefix: 'easy-ecr-'
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,10 @@ codeartifact-repo-format: init
cd ./codeartifact-repo && ../scripts/format.sh

codeartifact-repo-verify: init
cd ./codeartifact-repo && ../scripts/verify.sh
cd ./codeartifact-repo && ../scripts/verify.sh

easy-ecr-format: init
cd ./easy-ecr && ../scripts/format.sh

easy-ecr-verify: init
cd ./easy-ecr && ../scripts/verify.sh
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ Included modules:

* [tf-bootstrap-aws](./tf-bootstrap-aws/README.md) - Cloudformation template to easily bootstrap Terraform S3 backend
* [codeartifact-repo](./codeartifact-repo/README.md) - Terraform module to setup and configure AWS CodeArtifact domain and repository
* [easy-ecr](./easy-ecr/README.md) - Terraform module to setup and configure AWS ECR repository

95 changes: 95 additions & 0 deletions easy-ecr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<!-- BEGIN_TF_DOCS -->
# Easy ECR

This Terraform module provides production-ready ECR repository for storing container images.

## Contents
* [Requirements](#requirements)
* [Providers](#providers)
* [Resources](#resources)
* [Inputs](#inputs)
* [Outputs](#outputs)
* [Examples](#examples)

## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.14.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 6.21.0 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | 6.25.0 |

## Resources
| Name | Type |
|------|------|
| [aws_ecr_account_setting.account_scan_config](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_account_setting) | resource |
|**Description:** ||
| [aws_ecr_lifecycle_policy.repo_lifecycle_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_lifecycle_policy) | resource |
|**Description:** ||
| [aws_ecr_pull_through_cache_rule.custom_pullthrough_cache_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_pull_through_cache_rule) | resource |
|**Description:** ||
| [aws_ecr_pull_through_cache_rule.default_pullthrough_cache_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_pull_through_cache_rule) | resource |
|**Description:** ||
| [aws_ecr_registry_policy.registry_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_registry_policy) | 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 |
|**Description:** ||
| [aws_ecrpublic_repository.ecr_public_repo](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecrpublic_repository) | resource |
|**Description:** ||
| [aws_ecrpublic_repository_policy.public_repo_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecrpublic_repository_policy) | resource |
|**Description:** ||
| [aws_kms_key.domain_encryption_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource |
|**Description:** ||

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_aws_public_pullthrough_cache_rule"></a> [aws\_public\_pullthrough\_cache\_rule](#input\_aws\_public\_pullthrough\_cache\_rule) | Pullthrough cache rule for AWS 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_default_account_scan_config"></a> [default\_account\_scan\_config](#input\_default\_account\_scan\_config) | Default ECR basic scan type configuration. | <pre>object({<br/> name = string<br/> value = string<br/> })</pre> | <pre>{<br/> "name": "BASIC_SCAN_TYPE_VERSION",<br/> "value": "AWS_NATIVE"<br/>}</pre> | no |
| <a name="input_docker_hub_pullthrough_cache_rule"></a> [docker\_hub\_pullthrough\_cache\_rule](#input\_docker\_hub\_pullthrough\_cache\_rule) | Pullthrough cache rule for Docker Hub 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/> credential_arn = optional(string, null)<br/> })</pre> | `{}` | no |
| <a name="input_domain_encryption_key_policy_path"></a> [domain\_encryption\_key\_policy\_path](#input\_domain\_encryption\_key\_policy\_path) | Local path to policy file to be applied to created KMS key. If not specified, no custom policy is applied. | `string` | `null` | no |
| <a name="input_ecr_region"></a> [ecr\_region](#input\_ecr\_region) | Region in which repositories will be managed. If not specified, defaults to region configured for provider | `string` | `null` | no |
| <a name="input_encryption_key_arn"></a> [encryption\_key\_arn](#input\_encryption\_key\_arn) | ARN of KMS key used for repository encryption. If not specified, and use\_default\_ecnryption\_key is false, creates new KMS key | `string` | `null` | no |
| <a name="input_force_delete"></a> [force\_delete](#input\_force\_delete) | If 'true', deletes repository even if it has contents | `bool` | `false` | no |
| <a name="input_github_cr_pullthrough_cache_rule"></a> [github\_cr\_pullthrough\_cache\_rule](#input\_github\_cr\_pullthrough\_cache\_rule) | Pullthrough cache rule for Github Container 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/> credential_arn = optional(string, null)<br/> })</pre> | `{}` | no |
| <a name="input_gitlab_cr_pullthrough_cache_rule"></a> [gitlab\_cr\_pullthrough\_cache\_rule](#input\_gitlab\_cr\_pullthrough\_cache\_rule) | Pullthrough cache rule for Gitlab Container 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/> credential_arn = optional(string, null)<br/> })</pre> | `{}` | no |
| <a name="input_image_lifecycle_policy_path"></a> [image\_lifecycle\_policy\_path](#input\_image\_lifecycle\_policy\_path) | Path to JSON file providing lifecycle policy for the repository | `string` | `null` | no |
| <a name="input_image_tag_mutable"></a> [image\_tag\_mutable](#input\_image\_tag\_mutable) | Whether image tags are mutable. Only applicable for private repositories. | `bool` | `true` | no |
| <a name="input_k8s_pullthrough_cache_rule"></a> [k8s\_pullthrough\_cache\_rule](#input\_k8s\_pullthrough\_cache\_rule) | Pullthrough cache rule for Kubernetes 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_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_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_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_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 |
| <a name="input_tags"></a> [tags](#input\_tags) | Tags to be applied to resources | `map(string)` | `{}` | no |
| <a name="input_use_default_ecnryption_key"></a> [use\_default\_ecnryption\_key](#input\_use\_default\_ecnryption\_key) | Whether to use default ECR encryption key (defaults to true) | `bool` | `true` | no |
| <a name="input_use_default_image_lifecycle_policy"></a> [use\_default\_image\_lifecycle\_policy](#input\_use\_default\_image\_lifecycle\_policy) | Whether to use default image lifecycle or not. Defaults to true. | `bool` | `true` | no |
| <a name="input_visibility"></a> [visibility](#input\_visibility) | Visibility of the repository. Allowed values are 'PRIVATE' (default) and 'PUBLIC'. | `string` | `"PRIVATE"` | no |

## Outputs

No outputs.

## Examples

Examples configuration for using the module:

```hcl

module "easy_ecr" {
source = ""https://github.com/bitshifted/cloud-tools//easy-ecr?ref=easy-ecr-<current version>"
}
```
<!-- END_TF_DOCS -->
48 changes: 48 additions & 0 deletions easy-ecr/cache.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Copyright 2025 Bitshift
# SPDX-License-Identifier: MPL-2.0



locals {
default_cache_rules = {
aws_public = merge(var.aws_public_pullthrough_cache_rule, {
upstream_registry_url = "public.ecr.aws"
})
k8s_public = merge(var.k8s_pullthrough_cache_rule, {
upstream_registry_url = "registry.k8s.io"
})
quay = merge(var.quay_pullthrough_cache_rule, {
upstream_registry_url = "quay.io"
})
docker_hub = merge(var.docker_hub_pullthrough_cache_rule, {
upstream_registry_url = "registry-1.docker.io"
})
github = merge(var.github_cr_pullthrough_cache_rule, {
upstream_registry_url = "ghcr.io"
})
gitlab = merge(var.gitlab_cr_pullthrough_cache_rule, {
upstream_registry_url = "registry.gitlab.com"
})
}

}

resource "aws_ecr_pull_through_cache_rule" "default_pullthrough_cache_rule" {
for_each = { for k, v in local.default_cache_rules : k => v if v.enabled == true }
region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region
ecr_repository_prefix = each.value.ecr_repository_prefix
credential_arn = can(each.value.credential_arn) ? each.value.credential_arn : null
custom_role_arn = can(each.value.custom_role_arn) ? each.value.custom_role_arn : null
upstream_registry_url = each.value.upstream_registry_url
upstream_repository_prefix = each.value.upstream_repository_prefix
}

resource "aws_ecr_pull_through_cache_rule" "custom_pullthrough_cache_rule" {
for_each = { for k, v in var.pullthrough_cache_rules : k => v }
region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region
ecr_repository_prefix = each.value.ecr_repository_prefix
credential_arn = can(each.value.credential_arn) ? each.value.credential_arn : null
custom_role_arn = can(each.value.custom_role_arn) ? each.value.custom_role_arn : null
upstream_registry_url = each.value.upstream_registry_url
upstream_repository_prefix = each.value.upstream_repository_prefix
}
30 changes: 30 additions & 0 deletions easy-ecr/default-lifecycle-policy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"rules": [
{
"rulePriority": 10,
"description": "Untagged images policy",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 30
},
"action": {
"type": "expire"
}
},
{
"rulePriority": 20,
"description": "Any image policy",
"selection": {
"tagStatus": "tagged",
"tagPatternList": ["*"],
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {
"type": "expire"
}
}
]
}
10 changes: 10 additions & 0 deletions easy-ecr/docs/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
## Examples

Examples configuration for using the module:

```hcl

module "easy_ecr" {
source = ""https://github.com/bitshifted/cloud-tools//easy-ecr?ref=easy-ecr-<current version>"
}
```
3 changes: 3 additions & 0 deletions easy-ecr/docs/intro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Easy ECR

This Terraform module provides production-ready ECR repository for storing container images.
65 changes: 65 additions & 0 deletions easy-ecr/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Copyright 2025 Bitshift
# SPDX-License-Identifier: MPL-2.0



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")
}

data "aws_region" "current_region" {}

resource "aws_ecr_repository" "ecr_private_repo" {
count = var.visibility == "PRIVATE" ? 1 : 0
name = var.repository_name
region = local.resolved_region
force_delete = var.force_delete
image_tag_mutability = local.image_tag_mutability

#checkov:skip=CKV_AWS_136: Using AWS-managed AES256 is acceptable if user enables it
encryption_configuration {
encryption_type = var.use_default_ecnryption_key ? "AES256" : "KMS"
kms_key = !var.use_default_ecnryption_key ? var.encryption_key_arn != null ? var.encryption_key_arn : aws_kms_key.domain_encryption_key[0].arn : null
}

dynamic "image_tag_mutability_exclusion_filter" {
for_each = var.mutability_exclusion_filters
content {
filter = image_tag_mutability_exclusion_filter.value
filter_type = "WILDCARD"
}
}

image_scanning_configuration {
scan_on_push = var.scan_images_on_push
}

tags = var.tags
}

resource "aws_ecrpublic_repository" "ecr_public_repo" {
count = var.visibility == "PUBLIC" ? 1 : 0
repository_name = var.repository_name
region = local.resolved_region

catalog_data {
about_text = var.public_catalog_data.about
architectures = var.public_catalog_data.architectures
description = var.public_catalog_data.description
logo_image_blob = var.public_catalog_data.logo_image_path != null ? filebase64(var.public_catalog_data.logo_image_path) : null
operating_systems = var.public_catalog_data.operating_systems
usage_text = var.public_catalog_data.usage
}

tags = var.tags
}

resource "aws_kms_key" "domain_encryption_key" {
count = local.should_create_kms_key ? 1 : 0
description = "KMS key for ECR repository domain ${var.repository_name}"
enable_key_rotation = true
policy = var.domain_encryption_key_policy_path != null ? file(var.domain_encryption_key_policy_path) : null
tags = var.tags
}
34 changes: 34 additions & 0 deletions easy-ecr/policies.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Copyright 2025 Bitshift
# SPDX-License-Identifier: MPL-2.0

locals {
use_any_lifecycle_policy = var.visibility == "PRIVATE" && (var.use_default_image_lifecycle_policy || var.image_lifecycle_policy_path != null)
apply_default_lifecycle_policy = var.use_default_image_lifecycle_policy && var.image_lifecycle_policy_path == null
}

resource "aws_ecr_registry_policy" "registry_policy" {
count = var.registry_policy_path != null ? 1 : 0
region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region
policy = file(var.registry_policy_path)
}

resource "aws_ecr_repository_policy" "repo_policy" {
count = var.repo_policy_path != null ? 1 : 0
region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region
repository = aws_ecr_repository.ecr_private_repo[0].name
policy = file(var.repo_policy_path)
}

resource "aws_ecr_lifecycle_policy" "repo_lifecycle_policy" {
count = local.use_any_lifecycle_policy ? 1 : 0
region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region
repository = aws_ecr_repository.ecr_private_repo[0].name
policy = local.apply_default_lifecycle_policy ? file("${path.module}/default-lifecycle-policy.json") : file(var.image_lifecycle_policy_path)
}

resource "aws_ecrpublic_repository_policy" "public_repo_policy" {
count = var.public_repo_policy_path != null ? 1 : 0
region = var.ecr_region != null ? var.ecr_region : data.aws_region.current_region.region
repository_name = aws_ecrpublic_repository.ecr_public_repo[0].id
policy = file(var.public_repo_policy_path)
}
Loading
Loading