ECR Private Repository with Lifecycle Policy and Image Scanning
Scenario
Your team is containerising the web application and needs a private Docker image repository in AWS. You also need to automatically clean up old images to control storage costs — keeping only the last 10 tagged images and removing all untagged images after 1 day.
Your Objectives
- Create an aws_ecr_repository with image tag immutability and scan-on-push enabled.
- Attach a lifecycle policy that expires untagged images after 1 day and keeps only the latest 10 tagged images.
- Attach a repository policy (resource-based IAM policy) allowing the current account to push and pull images.
- Output the repository URL (used in docker push commands).
Additional Context
ECR lifecycle policies use a JSON format unique to ECR — not IAM policy JSON. Rules with lower rulePriority are evaluated first. The policy is attached via the aws_ecr_lifecycle_policy resource. Tag immutability prevents existing image tags from being overwritten, which is important for reproducible deployments.
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}variables.tf
variable "aws_region" { type = string; default = "us-east-1" }
variable "repo_name" { type = string; default = "web-app" }
variable "environment" { type = string; default = "prod" }main.tf
# main.tf
provider "aws" {
region = var.aws_region
}
data "aws_caller_identity" "current" {}
# ECR private repository.
resource "aws_ecr_repository" "app" {
name = "${var.repo_name}-${var.environment}"
image_tag_mutability = "IMMUTABLE" # prevent overwriting existing tags
# Scan every image on push — reports critical/high CVEs.
image_scanning_configuration {
scan_on_push = true
}
# Encrypt images using AWS-managed KMS key.
encryption_configuration {
encryption_type = "AES256"
}
tags = { Environment = var.environment, ManagedBy = "terraform" }
}
# Lifecycle policy — controls image retention to manage storage costs.
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
# JSON lifecycle policy — ECR-specific format (not IAM JSON).
policy = jsonencode({
rules = [
{
# Rule 1: delete untagged images after 1 day.
# Untagged images accumulate from CI builds and waste storage.
rulePriority = 1
description = "Expire untagged images after 1 day"
selection = {
tagStatus = "untagged"
countType = "sinceImagePushed"
countUnit = "days"
countNumber = 1
}
action = { type = "expire" }
},
{
# Rule 2: keep only the latest 10 tagged images per repository.
rulePriority = 2
description = "Keep latest 10 tagged images"
selection = {
tagStatus = "tagged"
tagPrefixList = ["v"] # only applies to images tagged with "v" prefix (e.g., v1.0.0)
countType = "imageCountMoreThan"
countNumber = 10
}
action = { type = "expire" }
}
]
})
}
# Repository policy — IAM resource policy on the ECR repository.
resource "aws_ecr_repository_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "AllowAccountPushPull"
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
}]
})
}outputs.tf
output "repository_url" { value = aws_ecr_repository.app.repository_url }
output "repository_arn" { value = aws_ecr_repository.app.arn }Workflow Commands
terraform init
terraform apply
# Authenticate Docker with ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin $(terraform output -raw repository_url | cut -d/ -f1)
# Build and push an image
docker build -t $(terraform output -raw repository_url):v1.0.0 .
docker push $(terraform output -raw repository_url):v1.0.0
terraform destroy
✓ Expected Output After Apply
Apply complete! Resources: 3 added, 0 changed, 0 destroyed. Outputs: repository_arn = "arn:aws:ecr:us-east-1:123456789012:repository/web-app-prod" repository_url = "123456789012.dkr.ecr.us-east-1.amazonaws.com/web-app-prod"

