LAB16_AWS_ECR Private Repository with Lifecycle Policy and Image Scanning

ECR Private Repository with Lifecycle Policy and Image Scanning

☁ AWS ⭐ Beginner ECR Container Registry Image Scanning Lifecycle Policy

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"