SecurityDevOps

Container Security Scanning in CI/CD with AWS ECR and Trivy

NT

Naveen Teja

3/2/2026

Container Security Scanning in CI/CD with AWS ECR and Trivy

Shipping container images without vulnerability scanning is the equivalent of deploying code without tests. Every base image you pull from Docker Hub contains hundreds of packages, any of which may carry known CVEs. If you push a vulnerable image to production, attackers with knowledge of that CVE can exploit it directly against your running containers.

The modern DevSecOps approach embeds security scanning directly into the CI/CD pipeline as a hard gate. AWS Elastic Container Registry (ECR) provides native image scanning powered by the Clair open-source engine on push. However, for more granular control and richer reporting, Trivy by Aqua Security has become the industry standard. Trivy scans OS packages, language-specific dependencies (npm, pip, Go modules), and Kubernetes manifests simultaneously.

The workflow is: build image, push to ECR, run Trivy scan, fail the pipeline if any CRITICAL or HIGH severity CVEs are found, and only proceed to deployment on a clean scan. Embedding this as a required CI check ensures no unscanned image ever reaches ECS or EKS. The configuration below shows both the ECR Terraform setup with scan-on-push enabled and the GitHub Actions pipeline step that runs Trivy as a blocking gate.

ecr-scanning.tf
# Terraform: ECR with scan-on-push
resource "aws_ecr_repository" "app" {
  name                 = "my-application"
  image_tag_mutability = "IMMUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }

  encryption_configuration {
    encryption_type = "AES256"
  }
}

# GitHub Actions step (add to your .github/workflows/deploy.yml)
# - name: Scan image with Trivy
#   uses: aquasecurity/trivy-action@master
#   with:
#     image-ref: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPO }}:${{ github.sha }}
#     format: table
#     exit-code: '1'
#     severity: 'CRITICAL,HIGH'

# ECR Lifecycle Policy — keep only last 10 tagged images
resource "aws_ecr_lifecycle_policy" "app_policy" {
  repository = aws_ecr_repository.app.name

  policy = jsonencode({
    rules = [{
      rulePriority = 1
      description  = "Keep last 10 images"
      selection = {
        tagStatus   = "any"
        countType   = "imageCountMoreThan"
        countNumber = 10
      }
      action = { type = "expire" }
    }]
  })
}