A container registry without security controls is a liability. Images accumulate. Old ones with known vulnerabilities sit alongside current ones with no clear way to tell them apart. Someone deploys something that passed scanning six months ago and is now full of CVEs. Without lifecycle policies, the registry grows without bound and the storage bill quietly grows with it.
Amazon ECR handles private container image storage with built-in scanning, immutable tags, lifecycle policies, and cross-region replication. In this article I will walk through a production ECR configuration with Terraform, automated scanning workflows, and the pull-through cache that eliminates DockerHub rate limits for base images.
Repository Setup with Scanning and Immutable Tags
resource "aws_ecr_repository" "app" {
name = "production/app"
image_tag_mutability = "IMMUTABLE"
image_scanning_configuration {
scan_on_push = true
}
encryption_configuration {
encryption_type = "KMS"
kms_key = aws_kms_key.ecr.arn
}
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
resource "aws_ecr_registry_scanning_configuration" "main" {
scan_type = "ENHANCED"
rule {
scan_frequency = "CONTINUOUS_SCAN"
repository_filter {
filter = "production/*"
filter_type = "WILDCARD"
}
}
}
resource "aws_ecr_registry_policy" "main" {
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowCrossAccountPull"
Effect = "Allow"
Principal = {
AWS = [
"arn:aws:iam::${var.staging_account_id}:root",
"arn:aws:iam::${var.prod_account_id}:root"
]
}
Action = [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchCheckLayerAvailability"
]
Resource = "arn:aws:ecr:${var.region}:${data.aws_caller_identity.current.account_id}:repository/*"
}
]
})
}
ENHANCED scanning uses AWS Inspector with Snyk integration and scans continuously rather than only on push. This means a new vulnerability published today against a library in an image you pushed three months ago shows up in your findings today, not the next time you push. For production registries this is the correct mode. Basic scanning only checks on push and uses a simpler vulnerability database.
The registry policy grants pull access to your staging and production accounts without requiring cross-account IAM roles for the pull operation itself. This is the correct pattern for multi-account organizations where the shared services account hosts the registry and application accounts pull images from it.
Lifecycle Policies
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep last 10 production-tagged images"
selection = {
tagStatus = "tagged"
tagPrefixList = ["v"]
countType = "imageCountMoreThan"
countNumber = 10
}
action = { type = "expire" }
},
{
rulePriority = 2
description = "Keep last 5 main branch images"
selection = {
tagStatus = "tagged"
tagPrefixList = ["main-"]
countType = "imageCountMoreThan"
countNumber = 5
}
action = { type = "expire" }
},
{
rulePriority = 3
description = "Expire untagged images after 1 day"
selection = {
tagStatus = "untagged"
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 1
}
action = { type = "expire" }
},
{
rulePriority = 4
description = "Expire all other tagged images after 30 days"
selection = {
tagStatus = "tagged"
tagPrefixList = ["sha-", "pr-"]
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 30
}
action = { type = "expire" }
}
]
})
}
Cross-Region Replication
resource "aws_ecr_replication_configuration" "main" {
replication_configuration {
rule {
destination {
region = "eu-west-1"
registry_id = data.aws_caller_identity.current.account_id
}
repository_filter {
filter = "production/*"
filter_type = "PREFIX_MATCH"
}
}
}
}
Cross-region replication ensures your DR region has images available before you need to deploy there during a failover. Without replication, a deployment to a secondary region during an incident requires pulling images across regions, which adds latency and creates a dependency on the primary region being partially functional. With replication, images are already present in the DR region when failover begins.
Pull-Through Cache
resource "aws_ecr_pull_through_cache_rule" "dockerhub" {
ecr_repository_prefix = "dockerhub"
upstream_registry_url = "registry-1.docker.io"
credential_arn = aws_secretsmanager_secret.dockerhub.arn
}
resource "aws_ecr_pull_through_cache_rule" "public_ecr" {
ecr_repository_prefix = "ecr-public"
upstream_registry_url = "public.ecr.aws"
}
Pull-through cache routes pulls of public images through ECR. When a Kubernetes node pulls nginx:latest from dockerhub via your ECR pull-through prefix, ECR fetches it from DockerHub once, caches it, and serves subsequent pulls from the cache. Benefits are eliminating DockerHub rate limits, reducing egress costs for frequently pulled base images, and creating an audit trail for all base image pulls in CloudTrail.
Scanning Findings in CI/CD
import boto3
import sys
ecr = boto3.client("ecr", region_name="us-east-1")
def check_image_findings(repository_name: str, image_tag: str, max_critical: int = 0, max_high: int = 5):
response = ecr.describe_image_scan_findings(
repositoryName = repository_name,
imageId = {"imageTag": image_tag}
)
findings = response["imageScanFindings"]
counts = findings.get("findingSeverityCounts", {})
critical = counts.get("CRITICAL", 0)
high = counts.get("HIGH", 0)
print(f"Scan findings: CRITICAL={critical}, HIGH={high}, MEDIUM={counts.get('MEDIUM', 0)}")
if critical > max_critical:
print(f"FAIL: {critical} CRITICAL findings exceeds threshold of {max_critical}")
sys.exit(1)
if high > max_high:
print(f"FAIL: {high} HIGH findings exceeds threshold of {max_high}")
sys.exit(1)
print("Image passed security gate")
check_image_findings(
repository_name = "production/app",
image_tag = "v1.2.3"
)
Running this check as a CI/CD gate after push and before deployment blocks images with critical vulnerabilities from reaching production. Set max_critical = 0 to enforce zero tolerance for critical CVEs. Adjust max_high based on your organization’s risk tolerance and the reality that some high-severity findings are in transitive dependencies or affect code paths not reachable in your application.
Closing Thoughts
ECR with immutable tags, continuous scanning, lifecycle policies, and cross-region replication is the container registry configuration that supports a mature delivery pipeline. Immutable tags make deployments reproducible and auditable. Continuous scanning surfaces vulnerabilities in production images before they are exploited. Lifecycle policies keep storage costs predictable. Cross-region replication makes DR deployments fast.
Enforce scanning gates in CI/CD. Set lifecycle policies on every repository. Enable pull-through cache for public dependencies. Replicate to your DR region. These four practices turn a container registry from storage into an active part of your security and reliability posture.
Enjoy the cloud.
Osama
#AWS #ECR #ContainerSecurity #ContainerRegistry #CloudArchitecture #Terraform #InfrastructureAsCode #AmazonWebServices #SolutionsArchitect #CloudComputing #DevSecOps #Docker #Kubernetes #TechBlog #CloudInfrastructure #VulnerabilityScanning #CICD #CloudSecurity #Containers #DevOps
Leave a Reply