Pular para o conteúdo principal

ECR Repository - Container Registry

Create and manage private Docker (container) image registries on AWS with security, vulnerability scanning, and automatic lifecycle policies.

Prerequisite: AWSProvider Configuration​

Before creating any AWS resource, you need to configure an AWSProvider that manages credentials and authentication with AWS.

IRSA:

apiVersion: aws-infra-operator.runner.codes/v1alpha1
kind: AWSProvider
metadata:
name: production-aws
namespace: default
spec:
region: us-east-1
roleARN: arn:aws:iam::123456789012:role/infra-operator-role
defaultTags:
managed-by: infra-operator
environment: production

Static Credentials:

apiVersion: v1
kind: Secret
metadata:
name: aws-credentials
namespace: default
type: Opaque
stringData:
access-key-id: test
secret-access-key: test
---
apiVersion: aws-infra-operator.runner.codes/v1alpha1
kind: AWSProvider
metadata:
name: localstack
namespace: default
spec:
region: us-east-1
accessKeyIDRef:
name: aws-credentials
key: access-key-id
secretAccessKeyRef:
name: aws-credentials
key: secret-access-key
defaultTags:
managed-by: infra-operator
environment: test

Check Status:

kubectl get awsprovider
kubectl describe awsprovider production-aws
aviso

For production, always use IRSA (IAM Roles for Service Accounts) instead of static credentials.

Create IAM Role for IRSA​

To use IRSA in production, you need to create an IAM Role with the required permissions:

Trust Policy (trust-policy.json):

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:infra-operator-system:infra-operator-controller-manager",
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
}
}
}
]
}

IAM Policy - ECR (ecr-policy.json):

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:CreateRepository",
"ecr:DeleteRepository",
"ecr:DescribeRepositories",
"ecr:PutImageScanningConfiguration",
"ecr:PutImageTagMutability",
"ecr:PutLifecyclePolicy",
"ecr:GetLifecyclePolicy",
"ecr:TagResource",
"ecr:UntagResource",
"ecr:ListTagsForResource"
],
"Resource": "*"
}
]
}

Create Role with AWS CLI:

# 1. Get OIDC Provider from EKS cluster
export CLUSTER_NAME=my-cluster
export AWS_REGION=us-east-1
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

OIDC_PROVIDER=$(aws eks describe-cluster \
--name $CLUSTER_NAME \
--region $AWS_REGION \
--query "cluster.identity.oidc.issuer" \
--output text | sed -e "s/^https:\/\///")

# 2. Update trust-policy.json with correct values
cat > trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_PROVIDER}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_PROVIDER}:sub": "system:serviceaccount:infra-operator-system:infra-operator-controller-manager",
"${OIDC_PROVIDER}:aud": "sts.amazonaws.com"
}
}
}
]
}
EOF

# 3. Create IAM Role
aws iam create-role \
--role-name infra-operator-ecr-role \
--assume-role-policy-document file://trust-policy.json \
--description "Role for Infra Operator ECR management"

# 4. Create and attach policy
aws iam put-role-policy \
--role-name infra-operator-ecr-role \
--policy-name ECRManagement \
--policy-document file://ecr-policy.json

# 5. Get Role ARN
aws iam get-role \
--role-name infra-operator-ecr-role \
--query 'Role.Arn' \
--output text

Annotate Operator ServiceAccount:

# Add annotation to operator ServiceAccount
kubectl annotate serviceaccount infra-operator-controller-manager \
-n infra-operator-system \
eks.amazonaws.com/role-arn=arn:aws:iam::123456789012:role/infra-operator-ecr-role
observação

Replace 123456789012 with your AWS Account ID and EXAMPLED539D4633E53DE1B71EXAMPLE with your OIDC provider ID.

Overview​

Amazon ECR (Elastic Container Registry) is a fully managed private Docker container registry service that makes it easy to store, manage, and deploy Docker/container images. It integrates seamlessly with ECS, EKS, Lambda, and CI/CD pipelines.

Features:

  • Secure Private Registry: Store private Docker images on AWS
  • No Infrastructure Management: Fully managed by AWS
  • High Availability: Automatically replicated across multiple AZs
  • IAM Integration: Granular access control with IAM policies
  • Image Scanning: Automatically detect vulnerabilities in images
  • Encryption at Rest: AES-256 encryption or custom KMS keys
  • Image Tag Mutability: Prevent tag overwrites (recommended for production)
  • Lifecycle Policies: Automatically delete old images
  • Cross-Account Access: Share images with other AWS accounts
  • Repository Policies: Granular access control per repository
  • Audit Integration: Audit all operations
  • Replication Rules: Replicate images across regions
  • Cost Effective: Pay only for storage used
  • Docker Push/Pull Native: Standard docker commands work natively

Status: ⚠️ Requires LocalStack Pro or Real AWS

Quick Start​

Basic ECR Repository:

apiVersion: aws-infra-operator.runner.codes/v1alpha1
kind: ECRRepository
metadata:
name: e2e-app-images
namespace: default
spec:
providerRef:
name: localstack
repositoryName: e2e-app-images
imageTagMutability: MUTABLE
scanOnPush: true
encryptionConfiguration:
encryptionType: AES256
tags:
environment: test
managed-by: infra-operator
purpose: e2e-testing
deletionPolicy: Delete

ECR Repository with Lifecycle Policy:

apiVersion: aws-infra-operator.runner.codes/v1alpha1
kind: ECRRepository
metadata:
name: e2e-production-images
namespace: default
spec:
providerRef:
name: localstack
repositoryName: e2e-production-images
imageTagMutability: IMMUTABLE
scanOnPush: true
encryptionConfiguration:
encryptionType: AES256
lifecyclePolicy:
policyText: |
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 10 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {
"type": "expire"
}
}
]
}
tags:
environment: production
managed-by: infra-operator
purpose: e2e-testing
deletionPolicy: Delete

Complete ECR Repository:

apiVersion: aws-infra-operator.runner.codes/v1alpha1
kind: ECRRepository
metadata:
name: app-backend
namespace: default
spec:
providerRef:
name: production-aws

# Repository name (optional prefix)
repositoryName: app/backend

# Tag mutability (IMMUTABLE recommended for production)
imageTagMutability: IMMUTABLE

# Vulnerability scanning
scanOnPush: true

# KMS encryption (optional)
encryptionConfiguration:
encryptionType: KMS
kmsKey: alias/ecr-encryption

# Lifecycle policy
lifecyclePolicy:
policyText: |
{
"rules": [{
"rulePriority": 1,
"description": "Keep last 10 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {
"type": "expire"
}
}]
}

# Tags for organization
tags:
Environment: production
Application: backend
ManagedBy: infra-operator

# Keep repository if CR is deleted
deletionPolicy: Retain

Apply:

kubectl apply -f ecr-repository.yaml

Check Status:

kubectl get ecrrepositories
kubectl describe ecrrepository e2e-app-images
kubectl get ecrrepository e2e-app-images -o yaml

Configuration Reference​

Required Fields​

Reference to AWSProvider resource for authentication

AWSProvider resource name

ECR repository name. Can include prefix with / (e.g., app/backend)

Rules:

  • 2 to 256 characters
  • Lowercase letters, numbers, hyphens, underscores, slashes (/)
  • Must start with letter or number
  • No spaces

Example:

repositoryName: myapp/backend
# or without prefix
repositoryName: my-backend-service

Optional Fields - Scanning​

Enable automatic vulnerability scanning when image is pushed

Example:

scanOnPush: true

Options:

  • true: Automatic scan (recommended for production)
  • false: Manual scan only (default)

Details:

  • Uses AWS vulnerability database
  • Detects CVEs (Common Vulnerabilities and Exposures)
  • Results available in DescribeImages
  • No additional cost for scanning

Optional Fields - Tag Mutability​

Allow image tag overwrites

Options:

  • MUTABLE: Tags can be overwritten (default, less secure)
  • IMMUTABLE: Tags cannot be overwritten (recommended for production)

Example:

imageTagMutability: IMMUTABLE

Recommendation: Use IMMUTABLE in production to ensure versioned tags are not accidentally overwritten

Optional Fields - Encryption​

Encryption configuration for stored images

Example:

encryptionConfiguration:
encryptionType: KMS
kmsKey: arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012

encryptionType Options:

  • AES256: AWS-managed encryption (default, no additional cost)
  • KMS: AWS KMS encryption (requires CMK, additional cost)

If using KMS:

  • kmsKey: ARN or alias of KMS key
  • Example: arn:aws:kms:us-east-1:123456789012:key/12345678...
  • Or alias: alias/ecr-encryption
  • KMS key MUST exist and have ECR permissions

Optional Fields - Lifecycle Policy​

Policy to automatically manage images (delete old ones)

Example:

lifecyclePolicy:
policyText: |
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 10 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {
"type": "expire"
}
},
{
"rulePriority": 2,
"description": "Expire untagged images after 30 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 30
},
"action": {
"type": "expire"
}
}
]
}

JSON document of lifecycle policy

Structure:

  • rules[]: Array of lifecycle rules
  • rulePriority: Execution order (lower first)
  • description: Rule description
  • selection: Criteria for which images the rule applies to
  • action: What to do (only expire supported)

Selection - tagStatus:

  • tagged: Applies only to tagged images
  • untagged: Applies only to untagged images
  • any: Applies to all images

Selection - countType:

  • imageCountMoreThan: If there are more than N images
  • sinceImagePushed: If pushed more than N days/months/years ago

Selection - countUnit: (for sinceImagePushed)

  • days
  • months
  • years

Optional Fields - Policies and Control​

JSON policy for repository access control (similar to bucket policies)

Example:

repositoryPolicyText: |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/ECS-Task-Execution-Role"
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
]
}
]
}

Common ECR Actions:

  • ecr:GetDownloadUrlForLayer: Download image layers
  • ecr:BatchGetImage: Download images (pull)
  • ecr:PutImage: Push images (push)
  • ecr:InitiateLayerUpload: Initiate layer upload
  • ecr:UploadLayerPart: Partial layer upload
  • ecr:CompleteLayerUpload: Complete layer upload

Usage: Cross-account access, IP restrictions, etc.

Optional Fields - Tags and Deletion​

Key-value pairs for organization and billing

Example:

tags:
Environment: production
Application: backend
Team: platform
CostCenter: engineering
ManagedBy: infra-operator

What happens to the repository when the CR is deleted

Options:

  • Delete: Repository is deleted from AWS (⚠️ WARNING: images will be lost)
  • Retain: Repository remains in AWS but unmanaged
  • Orphan: Remove management only

Example:

deletionPolicy: Retain

Recommendation: Use Retain in production to avoid accidental loss of images

Status Fields​

After the ECR Repository is created, the following status fields are populated:

true when the repository is created and ready for use

Full ARN of the ECR repository

arn:aws:ecr:us-east-1:123456789012:repository/app/backend

Repository URI for docker push/pull

123456789012.dkr.ecr.us-east-1.amazonaws.com/app/backend

AWS registry ID where the repository exists (usually the account ID)

Number of images stored in the repository

Repository creation date/time (ISO 8601 format)

Timestamp of last synchronization with AWS (ISO 8601 format)

Additional status message (errors, warnings, etc.)

Examples​

Basic ECR Repository​

Simple repository to get started:

apiVersion: aws-infra-operator.runner.codes/v1alpha1
kind: ECRRepository
metadata:
name: my-app
namespace: default
spec:
providerRef:
name: production-aws

repositoryName: my-company/my-app

# Basic: without scanning, mutable tags
imageTagMutability: MUTABLE
scanOnPush: false

encryptionConfiguration:
encryptionType: AES256

tags:
Environment: development
Application: my-app

deletionPolicy: Delete

ECR Repository with Automatic Scan​

Repository that automatically detects vulnerabilities:

apiVersion: aws-infra-operator.runner.codes/v1alpha1
kind: ECRRepository
metadata:
name: secure-backend
namespace: default
spec:
providerRef:
name: production-aws

repositoryName: app/backend-service

# Scan each push
scanOnPush: true

# Tags cannot be overwritten
imageTagMutability: IMMUTABLE

# Default encryption
encryptionConfiguration:
encryptionType: AES256

tags:
Environment: production
Application: backend
SecurityScanned: "true"

deletionPolicy: Retain

ECR Repository with Lifecycle Policy​

Repository with automatic cleanup of old images:

apiVersion: aws-infra-operator.runner.codes/v1alpha1
kind: ECRRepository
metadata:
name: continuous-build
namespace: default
spec:
providerRef:
name: production-aws

repositoryName: ci-cd/app-builder

scanOnPush: true
imageTagMutability: IMMUTABLE

encryptionConfiguration:
encryptionType: AES256

# Automatic cleanup
lifecyclePolicy:
policyText: |
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 30 tagged images",
"selection": {
"tagStatus": "tagged",
"countType": "imageCountMoreThan",
"countNumber": 30
},
"action": {
"type": "expire"
}
},
{
"rulePriority": 2,
"description": "Delete untagged images after 7 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 7
},
"action": {
"type": "expire"
}
}
]
}

tags:
Environment: development
Type: build-cache

deletionPolicy: Delete

ECR Repository with KMS Encryption​

Repository with custom encryption via KMS:

apiVersion: aws-infra-operator.runner.codes/v1alpha1
kind: ECRRepository
metadata:
name: encrypted-production
namespace: default
spec:
providerRef:
name: production-aws

repositoryName: app/production-images

scanOnPush: true
imageTagMutability: IMMUTABLE

# Custom KMS encryption
encryptionConfiguration:
encryptionType: KMS
kmsKey: arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012

lifecyclePolicy:
policyText: |
{
"rules": [{
"rulePriority": 1,
"description": "Keep last 20 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 20
},
"action": {"type": "expire"}
}]
}

tags:
Environment: production
Compliance: required
DataClassification: confidential

deletionPolicy: Retain

ECR Repository with Cross-Account Access​

Repository shared with another AWS account:

apiVersion: aws-infra-operator.runner.codes/v1alpha1
kind: ECRRepository
metadata:
name: shared-images
namespace: default
spec:
providerRef:
name: production-aws

repositoryName: shared/base-images

scanOnPush: true
imageTagMutability: IMMUTABLE

encryptionConfiguration:
encryptionType: AES256

# Policy to allow cross-account access
repositoryPolicyText: |
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPullFromOtherAccount",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::999888777666:role/ECS-Task-Execution-Role"
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:DescribeImages"
]
},
{
"Sid": "AllowPushFromCI",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/GitLab-Runner"
},
"Action": [
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload"
]
}
]
}

lifecyclePolicy:
policyText: |
{
"rules": [{
"rulePriority": 1,
"description": "Keep last 50 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 50
},
"action": {"type": "expire"}
}]
}

tags:
Environment: shared
Type: base-images

deletionPolicy: Retain

Verification​

Check Status via kubectl​

Command:

# List all repositories
kubectl get ecrrepositories

# Get detailed information
kubectl get ecrrepository secure-backend -o yaml

# Follow creation in real-time
kubectl get ecrrepository secure-backend -w

# View events and status
kubectl describe ecrrepository secure-backend

Verify on AWS​

AWS CLI:

# List repositories
aws ecr describe-repositories

# Get specific details
aws ecr describe-repositories \
--repository-names app/backend-service

# View images in repository
aws ecr describe-images \
--repository-name app/backend-service

# View image details
aws ecr describe-images \
--repository-name app/backend-service \
--image-ids imageTag=latest

# View scanning findings
aws ecr describe-image-scan-findings \
--repository-name app/backend-service \
--image-id imageTag=latest

# View lifecycle policy
aws ecr get-lifecycle-policy \
--repository-name app/backend-service

# View repository policy
aws ecr get-repository-policy \
--repository-name app/backend-service

# Get authentication token for docker
aws ecr get-authorization-token

# List all images with tags
aws ecr list-images \
--repository-name app/backend-service

Docker CLI:

# Login to ECR
aws ecr get-authorization-token --output text --query 'authorizationData[].authorizationToken' | base64 -d | cut -d: -f2 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

# Or using helper script (easier)
aws ecr get-authorization-token --output text --query 'authorizationData[].authorizationToken' | base64 -d | docker login --username AWS --password-stdin https://123456789012.dkr.ecr.us-east-1.amazonaws.com

# Local image tagging
docker tag my-app:v1.0.0 123456789012.dkr.ecr.us-east-1.amazonaws.com/app/backend:v1.0.0

# Push to ECR
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/app/backend:v1.0.0

# Pull from ECR
docker pull 123456789012.dkr.ecr.us-east-1.amazonaws.com/app/backend:v1.0.0

# List local images
docker images | grep ecr

LocalStack:

# For testing with LocalStack
export AWS_ENDPOINT_URL=http://localhost:4566

aws ecr describe-repositories

aws ecr list-images \
--repository-name app/backend-service

# Docker login for LocalStack
aws ecr get-authorization-token | jq -r '.authorizationData[0].authorizationToken' | base64 -d | cut -d: -f2 | docker login --username AWS --password-stdin localhost:4566

Expected Output​

Example:

status:
repositoryArn: arn:aws:ecr:us-east-1:123456789012:repository/app/backend-service
repositoryUri: 123456789012.dkr.ecr.us-east-1.amazonaws.com/app/backend-service
registryId: "123456789012"
creationDate: "2025-11-22T20:15:30Z"
imageTagMutability: IMMUTABLE
encryptionType: AES256
imageScanningConfiguration:
scanOnPush: true
ready: true
lastSyncTime: "2025-11-22T20:15:45Z"

Troubleshooting​

Docker push denied: requested access to the resource is denied​

Symptoms: Error pushing Docker image

Common causes:

  1. Not authenticated with ECR (missing docker login)
  2. Expired AWS credentials
  3. Insufficient IAM permissions
  4. Repository does not exist

Solutions:

# Check if repository exists
aws ecr describe-repositories --repository-names app/backend

# Re-authenticate
aws ecr get-authorization-token --output text --query 'authorizationData[].authorizationToken' | base64 -d | cut -d: -f2 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

# Check AWS credentials
aws sts get-caller-identity

# Verify IAM policy has ecr:PutImage
aws iam get-user-policy --user-name <username> --policy-name <policy>

# Tag the image correctly
docker tag myapp:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/app/backend:latest

# Try push again
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/app/backend:latest

# If still failing, force logout and login
docker logout 123456789012.dkr.ecr.us-east-1.amazonaws.com
aws ecr get-authorization-token --output text --query 'authorizationData[].authorizationToken' | base64 -d | cut -d: -f2 | docker login --username AWS --password-stdin https://123456789012.dkr.ecr.us-east-1.amazonaws.com

Image scan finds critical vulnerabilities (CVE)​

Symptoms: Scan findings show HIGH or CRITICAL vulnerabilities

Cause: Image dependencies have known CVEs

Solutions:

# View vulnerability details
aws ecr describe-image-scan-findings \
--repository-name app/backend \
--image-id imageTag=v1.0.0 \
--output table

# Remediation options:
# 1. Update dependencies in Dockerfile
# 2. Use latest base image
# 3. Remove unnecessary dependencies

# Improved Dockerfile example
# FROM node:20-alpine (use latest alpine)
# RUN npm ci --only=production (don't install dev deps)
# RUN npm audit fix (fix vulnerabilities)

# Rebuild image
docker build -t app/backend:v1.0.1 .

# Re-scan
aws ecr describe-image-scan-findings \
--repository-name app/backend \
--image-id imageTag=v1.0.1

# Ignore vulnerabilities if necessary (with documentation)
# But always prefer to fix!

Lifecycle policy deleting wrong images​

Symptoms: Important images are deleted by lifecycle policy

Causes:

  1. Wrong priority rule
  2. Selection too broad (tagStatus: any)
  3. Count number too low

Solutions:

# View current policy
aws ecr get-lifecycle-policy \
--repository-name app/backend

# Test policy before applying (conceptual dry-run)
# Review which images would be deleted

# Example: more conservative policy
lifecyclePolicy:
policyText: |
{
"rules": [
{
"rulePriority": 1,
"description": "Keep all tagged",
"selection": {
"tagStatus": "tagged",
"countType": "imageCountMoreThan",
"countNumber": 100
},
"action": {"type": "expire"}
},
{
"rulePriority": 2,
"description": "Delete old untagged images",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 30
},
"action": {"type": "expire"}
}
]
}

# Update policy
kubectl patch ecrrepository app-backend \
--type merge \
-p '{"spec":{"lifecyclePolicy":"..."}}'

# Check images before and after
aws ecr list-images --repository-name app/backend

Cross-account access denied​

Symptoms: Error trying to pull image from another AWS account

Cause: Repository policy does not allow access from other account

Solutions:

# Get ARN of role from other account
# Example: arn:aws:iam::999888777666:role/ECS-Task-Role

# Update repository policy
kubectl patch ecrrepository shared-images \
--type merge \
-p '{
"spec": {
"repositoryPolicyText": "{
\"Version\": \"2012-10-17\",
\"Statement\": [{
\"Effect\": \"Allow\",
\"Principal\": {
\"AWS\": \"arn:aws:iam::999888777666:role/ECS-Task-Role\"
},
\"Action\": [
\"ecr:GetDownloadUrlForLayer\",
\"ecr:BatchGetImage\"
]
}]
}"
}
}'

# Check if policy was applied
aws ecr get-repository-policy \
--repository-name shared/images

# In the other account, test pull
# Ensure the role has ecr permission in the other account too
aws ecr get-authorization-token --endpoint-url https://ecr.us-east-1.amazonaws.com

# If using assume role, check trust relationship
aws iam get-role --role-name ECS-Task-Role

High ECR costs​

Symptoms: AWS account with unexpected ECR costs

Causes:

  1. Many images stored
  2. Image size too large
  3. Old versions not deleted
  4. Cross-region replication active

Solutions:

# Check space used
aws ecr describe-repositories --repository-names app/backend

# View size of all images
aws ecr describe-images \
--repository-name app/backend \
--query 'imageDetails[].{Tag:imageTags[0],Size:imageSizeBytes}' \
--output table

# Implement more aggressive lifecycle policy
lifecyclePolicy:
policyText: |
{
"rules": [{
"rulePriority": 1,
"description": "Keep only 10 images",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": {"type": "expire"}
}]
}

# Delete specific large images
aws ecr batch-delete-image \
--repository-name app/backend \
--image-ids imageTag=old-build-1234

# Optimize image size in Dockerfile
# - Use multi-stage builds
# - Remove unnecessary layers
# - Use alpine base images
# - Combine RUN commands

# Optimized Dockerfile example
# FROM golang:1.21 AS builder
# COPY . /src
# WORKDIR /src
# RUN go build -o app
#
# FROM alpine:3.18
# RUN apk add --no-cache ca-certificates
# COPY --from=builder /src/app /usr/local/bin/
# CMD ["app"]

Repository stuck in NotReady​

Symptoms: Repository remains NotReady after creation

Causes:

  1. Insufficient IAM permissions
  2. KMS key not accessible (if using encryption)
  3. Connectivity problem

Solutions:

# View detailed events
kubectl describe ecrrepository app-backend

# View operator logs
kubectl logs -n infra-operator-system \
deploy/infra-operator-controller-manager \
--tail=100 | grep -i ecr

# Check AWSProvider is ready
kubectl get awsprovider
kubectl describe awsprovider production-aws

# If using KMS, check if it exists
aws kms describe-key --key-id alias/ecr-encryption

# If using KMS, check trust policy
aws kms get-key-policy --key-id alias/ecr-encryption --policy-name default

# Force synchronization
kubectl annotate ecrrepository app-backend \
force-sync="$(date +%s)" --overwrite

# Last resort: delete and recreate
kubectl patch ecrrepository app-backend \
--type merge \
-p '{"spec":{"deletionPolicy":"Retain"}}'

kubectl delete ecrrepository app-backend

# Then recreate
kubectl apply -f ecr-repository.yaml

Best Practices​

Best Practices
  • Enable scanOnPush for all repositories — Automatically detect vulnerabilities, prevent deployment of images with critical CVEs, and monitor scanning findings regularly
  • Use IMMUTABLE tags in production — Prevent overwrite of versioned tags, ensure v1.0.0 is always v1.0.0, and use semantic versioning (v1.0.0, v2.1.3)
  • Implement lifecycle policies — Automatically delete old images, save storage costs, keep last N images, and delete untagged after X days
  • Use KMS encryption for compliance — Encryption with custom key for HIPAA/PCI-DSS requirements, access control via IAM, and audit via CloudTrail
  • Configure repository policies carefully — Controlled cross-account access, restrict pull/push by role/user, follow principle of least privilege
  • Tag all repositories consistently — Include environment (dev/staging/prod), application, team, and CostCenter for governance and billing
  • Use consistent naming conventions — Pattern company/application-name, descriptive names, use prefix for organization
  • Optimize Docker layer caching — Structure Dockerfile to reuse layers, frequently changing commands at end, use multi-stage builds
  • Monitor ECR costs — ~$0.10/GB/month, use lifecycle policies to save costs, use cost allocation tags
  • Integrate with Kubernetes properly — EKS requires imagePullSecrets if cross-account, use IAM roles not static credentials, deploy with versioned tags
  • Consider cross-region replication — Low latency in each region, DR (disaster recovery), automatic replication based on rules
  • Maintain audit trail — Logging records all accesses, immutable tags for audit, image manifest digest for versioning
  • Keep images small — Use alpine base (20MB vs 100MB+), multi-stage builds, remove unnecessary layers, use distroless when possible

Workflow CI/CD​

Build → Tag → Push → Deploy​

Typical pipeline:

# GitLab CI Example
stages:
- build
- push
- deploy

build:
stage: build
script:
- docker build -t app:rev1 .
artifacts:
reports:
dotenv: build.env

push:
stage: push
script:
- aws ecr get-authorization-token | base64 -d | docker login --username AWS --password-stdin
- docker tag app:rev1 $ECR_REGISTRY/app/backend:rev1
- docker tag app:rev1 $ECR_REGISTRY/app/backend:latest
- docker push $ECR_REGISTRY/app/backend:rev1
- docker push $ECR_REGISTRY/app/backend:latest

deploy:
stage: deploy
script:
- kubectl set image deployment/app app=$ECR_REGISTRY/app/backend:rev1

Integration with GitHub Actions:

name: Build and Push to ECR

on:
push:
branches: [ main ]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-role
aws-region: us-east-1

- name: Login to ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1

- name: Build and push
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: app/backend
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest
docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

Scan results integration:

# Wait for scan to complete after push
aws ecr wait image-scan-complete \
--repository-name app/backend \
--image-id imageTag=latest

# Check for critical vulnerabilities
FINDINGS=$(aws ecr describe-image-scan-findings \
--repository-name app/backend \
--image-id imageTag=latest \
--query 'imageScanFindings.findingSeverityCounts.CRITICAL')

if [ "$FINDINGS" -gt 0 ]; then
echo "Critical vulnerabilities found!"
exit 1
fi