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
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
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 rulesrulePriority: Execution order (lower first)description: Rule descriptionselection: Criteria for which images the rule applies toaction: What to do (onlyexpiresupported)
Selection - tagStatus:
tagged: Applies only to tagged imagesuntagged: Applies only to untagged imagesany: Applies to all images
Selection - countType:
imageCountMoreThan: If there are more than N imagessinceImagePushed: If pushed more than N days/months/years ago
Selection - countUnit: (for sinceImagePushed)
daysmonthsyears
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 layersecr:BatchGetImage: Download images (pull)ecr:PutImage: Push images (push)ecr:InitiateLayerUpload: Initiate layer uploadecr:UploadLayerPart: Partial layer uploadecr: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 unmanagedOrphan: 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:
- Not authenticated with ECR (missing docker login)
- Expired AWS credentials
- Insufficient IAM permissions
- 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:
- Wrong priority rule
- Selection too broad (tagStatus: any)
- 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:
- Many images stored
- Image size too large
- Old versions not deleted
- 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:
- Insufficient IAM permissions
- KMS key not accessible (if using encryption)
- 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
- 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