Managing AWS ECR with Terraform

A comprehensive guide to setting up Amazon Elastic Container Registry (ECR) using Terraform Infrastructure as Code

Managing AWS ECR with Terraform

Amazon Elastic Container Registry (ECR) is a fully managed container registry service. This guide shows how to set up ECR using Terraform.

Prerequisites

  • AWS CLI configured
  • Terraform installed
  • Docker installed locally
  • Container images to store

Project Structure

aws-ecr-terraform/
├── main.tf
├── variables.tf
├── outputs.tf
└── terraform.tfvars

Basic ECR Configuration

# main.tf
provider "aws" {
  region = var.aws_region
}

# ECR Repository
resource "aws_ecr_repository" "main" {
  name                 = var.repository_name
  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 = var.environment
  }
}

# KMS Key for Encryption
resource "aws_kms_key" "ecr" {
  description             = "KMS key for ECR encryption"
  deletion_window_in_days = 7
  enable_key_rotation     = true

  tags = {
    Environment = var.environment
  }
}

# Repository Policy
resource "aws_ecr_repository_policy" "main" {
  repository = aws_ecr_repository.main.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowPull"
        Effect = "Allow"
        Principal = {
          AWS = var.allowed_account_arns
        }
        Action = [
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "ecr:BatchCheckLayerAvailability"
        ]
      }
    ]
  })
}

# Lifecycle Policy
resource "aws_ecr_lifecycle_policy" "main" {
  repository = aws_ecr_repository.main.name

  policy = jsonencode({
    rules = [
      {
        rulePriority = 1
        description  = "Keep last 30 production images"
        selection = {
          tagStatus     = "tagged"
          tagPrefixList = ["prod"]
          countType     = "imageCountMoreThan"
          countNumber   = 30
        }
        action = {
          type = "expire"
        }
      },
      {
        rulePriority = 2
        description  = "Keep last 10 development images"
        selection = {
          tagStatus     = "tagged"
          tagPrefixList = ["dev"]
          countType     = "imageCountMoreThan"
          countNumber   = 10
        }
        action = {
          type = "expire"
        }
      },
      {
        rulePriority = 3
        description  = "Remove untagged images after 7 days"
        selection = {
          tagStatus   = "untagged"
          countType   = "sinceImagePushed"
          countUnit   = "days"
          countNumber = 7
        }
        action = {
          type = "expire"
        }
      }
    ]
  })
}

Variables Configuration

# variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-west-2"
}

variable "repository_name" {
  description = "Name of the ECR repository"
  type        = string
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
}

variable "allowed_account_arns" {
  description = "List of AWS account ARNs allowed to pull images"
  type        = list(string)
  default     = []
}

Best Practices

  1. Repository Management

    • Use immutable tags
    • Enable image scanning
    • Implement lifecycle policies
    • Regular cleanup
  2. Security

    • Enable encryption
    • Use proper IAM roles
    • Implement repository policies
    • Regular security reviews
  3. Cost Optimization

    • Monitor storage usage
    • Clean up unused images
    • Use appropriate lifecycle rules
    • Regular cost reviews
  4. Performance

    • Use image layers effectively
    • Optimize image size
    • Consider multi-region replication
    • Regular performance reviews

Cross-Account Access

# Repository Policy for Cross-Account Access
resource "aws_ecr_repository_policy" "cross_account" {
  repository = aws_ecr_repository.main.name

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowCrossAccountPull"
        Effect = "Allow"
        Principal = {
          AWS = var.cross_account_arns
        }
        Action = [
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "ecr:BatchCheckLayerAvailability"
        ]
      },
      {
        Sid    = "AllowCrossAccountPush"
        Effect = "Allow"
        Principal = {
          AWS = var.cross_account_push_arns
        }
        Action = [
          "ecr:PutImage",
          "ecr:InitiateLayerUpload",
          "ecr:UploadLayerPart",
          "ecr:CompleteLayerUpload"
        ]
      }
    ]
  })
}

Replication Configuration

# Replication Configuration
resource "aws_ecr_replication_configuration" "main" {
  replication_configuration {
    rule {
      destination {
        region      = var.destination_region
        registry_id = var.destination_account_id
      }

      repository_filter {
        filter      = "prod-*"
        filter_type = "PREFIX_MATCH"
      }
    }
  }
}

Monitoring Configuration

# CloudWatch Dashboard
resource "aws_cloudwatch_dashboard" "ecr" {
  dashboard_name = "${var.repository_name}-dashboard"

  dashboard_body = jsonencode({
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 6

        properties = {
          metrics = [
            ["AWS/ECR", "RepositoryPullCount", "RepositoryName", aws_ecr_repository.main.name],
            ["AWS/ECR", "RepositoryPushCount", "RepositoryName", aws_ecr_repository.main.name]
          ]
          period = 300
          stat   = "Sum"
          region = var.aws_region
          title  = "ECR Repository Activity"
        }
      }
    ]
  })
}

# CloudWatch Alarms
resource "aws_cloudwatch_metric_alarm" "image_scan_findings" {
  alarm_name          = "${var.repository_name}-scan-findings"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "1"
  metric_name         = "ImageScanFindingsCount"
  namespace           = "AWS/ECR"
  period             = "300"
  statistic          = "Maximum"
  threshold          = "0"
  alarm_description  = "This metric monitors ECR image scan findings"
  alarm_actions      = [var.sns_topic_arn]

  dimensions = {
    Repository = aws_ecr_repository.main.name
  }
}

Deployment Steps

  1. Initialize Terraform:
terraform init
  1. Plan the deployment:
terraform plan
  1. Apply the configuration:
terraform apply

Clean Up

Remove all resources when done:

terraform destroy

Common Use Cases

  1. Multi-Environment Setup
resource "aws_ecr_repository" "environments" {
  for_each             = toset(["dev", "staging", "prod"])
  name                 = "${var.repository_name}-${each.key}"
  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 = each.key
  }
}

resource "aws_ecr_lifecycle_policy" "environments" {
  for_each   = aws_ecr_repository.environments
  repository = each.value.name

  policy = jsonencode({
    rules = [
      {
        rulePriority = 1
        description  = "Keep last ${each.key == "prod" ? "30" : "10"} images"
        selection = {
          tagStatus   = "any"
          countType   = "imageCountMoreThan"
          countNumber = each.key == "prod" ? 30 : 10
        }
        action = {
          type = "expire"
        }
      }
    ]
  })
}
  1. CI/CD Integration
# IAM Role for CI/CD
resource "aws_iam_role" "cicd" {
  name = "${var.repository_name}-cicd-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "codebuild.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "cicd" {
  name = "${var.repository_name}-cicd-policy"
  role = aws_iam_role.cicd.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:GetRepositoryPolicy",
          "ecr:DescribeRepositories",
          "ecr:ListImages",
          "ecr:DescribeImages",
          "ecr:BatchGetImage",
          "ecr:InitiateLayerUpload",
          "ecr:UploadLayerPart",
          "ecr:CompleteLayerUpload",
          "ecr:PutImage"
        ]
        Resource = aws_ecr_repository.main.arn
      }
    ]
  })
}

Advanced Features

# Pull Through Cache Configuration
resource "aws_ecr_pull_through_cache_rule" "upstream" {
  ecr_repository_prefix = "upstream"
  upstream_registry_url = "public.ecr.aws"
}

# Registry Policy
resource "aws_ecr_registry_policy" "main" {
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid    = "AllowPull"
        Effect = "Allow"
        Principal = {
          AWS = "*"
        }
        Action = [
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "ecr:BatchCheckLayerAvailability"
        ]
        Condition = {
          StringLike = {
            "aws:PrincipalOrgID": data.aws_organizations_organization.current.id
          }
        }
      }
    ]
  })
}

Conclusion

This setup provides a comprehensive foundation for deploying ECR using Terraform. Remember to:

  • Plan your repository structure carefully
  • Implement proper security measures
  • Monitor repository usage and costs
  • Keep your configurations versioned
  • Test thoroughly before production deployment

The complete code can be customized based on your specific requirements and use cases.