OpenTofu ECR Provisioning
What I do
I guide you through provisioning AWS Elastic Container Registry (ECR) repositories following BETEKK infrastructure standards:
- •ECR Repository Creation: Create ECR repositories with proper configuration and lifecycle policies
- •GitHub OIDC Integration: Setup IAM roles for GitHub Actions to push/pull images
- •State Management: Configure S3 backend for Terraform state management
- •IAM Role Patterns: Implement standardized IAM roles with OIDC trust relationships
- •Variable Naming: Follow BETEKK naming conventions for consistency
- •Modular Structure: Use reusable modules for ECR and IAM resources
- •Best Practices: Apply AWS Well-Architected Framework principles
When to use me
Use this skill when you need to:
- •Provision a new ECR repository for Docker container images
- •Setup GitHub Actions CI/CD for ECR integration
- •Create standardized AWS infrastructure following BETEKK patterns
- •Implement OIDC authentication for GitHub Actions
- •Migrate existing ECR repositories to BETEKK standards
- •Create reusable ECR infrastructure templates
Reference Implementation: ecr/betekk_probe_engine_main/
Prerequisites
- •OpenTofu CLI installed: Install from https://opentofu.org/docs/intro/install/
- •AWS Account: Valid AWS account with appropriate permissions
- •AWS Credentials: Access keys or IAM role for authentication
- •GitHub OIDC Provider: OIDC provider configured for GitHub Actions (optional, can be created as part of workflow)
- •Basic OpenTofu/Terraform Knowledge: Understanding of HCL syntax and provider concepts
BETEKK Standards
Variable Naming Convention
Follow these naming patterns for consistency:
variable "environment" {
description = "Environment type (e.g., dev, prod, uat)"
type = string
default = ""
}
variable "project_prefix" {
description = "Project Prefix"
type = string
default = "betekk"
}
variable "repo_name" {
description = "ECR Repository Name Prefix"
type = string
default = "betekk-<service-name>"
}
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-southeast-1"
}
variable "state_management_bucket_name" {
description = "State Management Bucket Name"
type = string
default = "betekk-state-management-bucket"
}
variable "github_organization_old" {
description = "Github Organization Name (old) for migration support"
type = string
default = "nus-cee"
}
variable "github_organization_new" {
description = "Github Organization Name (new)"
type = string
default = "betekk"
}
Folder Structure
Standard directory structure for ECR provisioning:
<service-name>/ ├── modules/ │ ├── ecr/ │ │ ├── main.tf # ECR repository resource │ │ └── variables.tf # ECR module variables │ └── iam-role-assume/ │ ├── main.tf # IAM assume role policy │ └── variables.tf # IAM module variables ├── .gitignore ├── .terraform.lock.hcl ├── README.md ├── data.tf # Data sources (caller identity, etc.) ├── iam_policies.tf # IAM policy definitions ├── iam_policy_attachments.tf ├── iam_roles.tf # IAM role resources ├── main.tf # Root module with module calls ├── outputs.tf # Output values ├── providers.tf # AWS provider configuration ├── terraform.tf # Terraform configuration └── variables.tf # Input variables
IAM Role Naming Patterns
# ECR Upload Role
resource "aws_iam_role" "ecr_<project_prefix>_github_workflows_upload_role" {
name = "${upper(var.project_prefix)}-GithubWorkflowsUploadRole"
# ...
}
# Lambda Deploy Role
resource "aws_iam_role" "betekk_lambda_github_workflows_deploy_role" {
name = "${upper(var.project_prefix)}-GithubWorkflowsLambdaDeployRole"
# ...
}
Steps
Step 1: Initialize Project
Create project directory and initialize Terraform:
# Create project directory mkdir -p <service-name>/modules/ecr mkdir -p <service-name>/modules/iam-role-assume cd <service-name> # Initialize Terraform tofu init
Step 2: Configure Terraform
Create terraform.tf:
terraform {
required_version = "~> 1.8"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = var.state_management_bucket_name
key = "ecr/<service-name>/terraform.tfstate"
region = var.aws_region
}
}
Step 3: Configure AWS Provider
Create providers.tf:
provider "aws" {
region = var.aws_region
}
Step 4: Define Variables
Create variables.tf:
variable "environment" {
description = "Environment type (e.g., dev, prod, uat)"
type = string
default = ""
}
variable "project_prefix" {
description = "Project Prefix"
type = string
default = "betekk"
}
variable "repo_name" {
description = "ECR Repository Name Prefix"
type = string
default = "betekk-<service-name>"
}
variable "aws_region" {
description = "AWS region"
type = string
default = "ap-southeast-1"
}
variable "state_management_bucket_name" {
description = "State Management Bucket Name"
type = string
default = "betekk-state-management-bucket"
}
variable "github_organization_old" {
description = "Github Organization Name (old)"
type = string
default = "nus-cee"
}
variable "github_organization_new" {
description = "Github Organization Name (new)"
type = string
default = "betekk"
}
Step 5: Create Data Sources
Create data.tf:
data "aws_caller_identity" "current" {}
Step 6: Create ECR Module
Create modules/ecr/main.tf:
# Create an ECR repository where Docker images will be stored
# The repository URL will be used by:
# 1. Docker push commands to upload images
# 2. Kubernetes/Helm to pull images during deployment
resource "aws_ecr_repository" "repository" {
name = var.repository_name
force_delete = false
}
# Output ECR repository URL for use in other parts of infrastructure
output "repository_url" {
description = "ECR repository URL"
value = aws_ecr_repository.repository.repository_url
}
output "repository_arn" {
description = "ECR repository ARN"
value = aws_ecr_repository.repository.arn
}
Create modules/ecr/variables.tf:
variable "repository_name" {
type = string
description = "ECR repository name"
}
Step 7: Create IAM Assume Role Module
Create modules/iam-role-assume/main.tf:
# Trust relationship is assumed to be set on role already.
# Attach policy to allow user to assume role
resource "aws_iam_policy" "assume_role_policy" {
name = "${var.project_prefix}_AssumeRolePolicy"
description = "Policy allowing assume role access to a specific role"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = "sts:AssumeRole",
Resource = var.assume_role_arn
}
]
})
}
resource "aws_iam_policy_attachment" "assume_role_policy_attachment" {
name = "${var.project_prefix}-AssumeRoleAttachment"
policy_arn = aws_iam_policy.assume_role_policy.arn
users = [var.user_arn]
}
Create modules/iam-role-assume/variables.tf:
variable "project_prefix" {
type = string
description = "Project prefix for naming"
}
variable "assume_role_arn" {
type = string
description = "ARN of role to assume"
}
variable "user_arn" {
type = string
description = "ARN of user to attach policy"
}
Step 8: Create IAM Roles
Create iam_roles.tf:
# Create IAM Role for GitHub ECR Upload
resource "aws_iam_role" "ecr_betekk_probe_engine_github_workflows_upload_role" {
name = "${upper(var.project_prefix)}-GithubWorkflowsUploadRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
"Federated" = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringLike = {
"token.actions.githubusercontent.com:aud" : "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub" : [
"repo:${var.github_organization_old}/<repo-name>:*",
"repo:${var.github_organization_new}/<repo-name>:*"
],
}
}
}
]
})
}
# Create IAM Role for Lambda Deployment
resource "aws_iam_role" "betekk_lambda_github_workflows_deploy_role" {
name = "${upper(var.project_prefix)}-GithubWorkflowsLambdaDeployRole"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
"Federated" = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringLike = {
"token.actions.githubusercontent.com:aud" : "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub" : [
"repo:${var.github_organization_old}/<repo-name>:*",
"repo:${var.github_organization_new}/<repo-name>:*"
]
}
}
}
]
})
}
Step 9: Create IAM Policies
Create iam_policies.tf:
# Policy for ECR Upload
resource "aws_iam_policy" "ecr_upload_policy" {
name = "${var.project_prefix}-ECRUploadPolicy"
description = "Policy allowing ECR operations"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:DescribeRepositories",
"ecr:GetRepositoryPolicy",
"ecr:ListImages",
"ecr:DeleteRepository",
"ecr:BatchDeleteImage",
"ecr:SetRepositoryPolicy",
"ecr:DeleteRepositoryPolicy"
]
Resource = "*"
}
]
})
}
# Policy for Lambda Deployment
resource "aws_iam_policy" "lambda_deploy_policy" {
name = "${var.project_prefix}-LambdaDeployPolicy"
description = "Policy allowing Lambda operations"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"lambda:CreateFunction",
"lambda:UpdateFunctionCode",
"lambda:UpdateFunctionConfiguration",
"lambda:GetFunction",
"lambda:DeleteFunction"
]
Resource = "*"
}
]
})
}
Step 10: Attach Policies
Create iam_policy_attachments.tf:
# Attach ECR policy to upload role
resource "aws_iam_role_policy_attachment" "ecr_upload_attachment" {
role = aws_iam_role.ecr_betekk_probe_engine_github_workflows_upload_role.name
policy_arn = aws_iam_policy.ecr_upload_policy.arn
}
# Attach Lambda policy to deploy role
resource "aws_iam_role_policy_attachment" "lambda_deploy_attachment" {
role = aws_iam_role.betekk_lambda_github_workflows_deploy_role.name
policy_arn = aws_iam_policy.lambda_deploy_policy.arn
}
Step 11: Create Root Module
Create main.tf:
data "aws_caller_identity" "current" {}
module "ecr_betekk_<service-name>" {
source = "./modules/ecr"
repository_name = var.repo_name
}
Step 12: Create Outputs
Create outputs.tf:
output "ecr_repository_url" {
description = "ECR repository URL"
value = module.ecr_betekk_<service-name>.repository_url
}
output "ecr_repository_arn" {
description = "ECR repository ARN"
value = module.ecr_betekk_<service-name>.repository_arn
}
output "ecr_upload_role_arn" {
description = "ARN of ECR upload role"
value = aws_iam_role.ecr_betekk_probe_engine_github_workflows_upload_role.arn
}
output "lambda_deploy_role_arn" {
description = "ARN of Lambda deploy role"
value = aws_iam_role.betekk_lambda_github_workflows_deploy_role.arn
}
Step 13: Initialize and Apply
# Initialize Terraform tofu init # Plan changes tofu plan -out=tfplan # Apply changes tofu apply tfplan # Show outputs tofu output
Best Practices
Security
- •Least Privilege: Grant minimal permissions in IAM roles and policies
- •OIDC Authentication: Use GitHub OIDC instead of long-lived credentials
- •Repository Scanning: Enable ECR image scanning for security vulnerabilities
- •Encryption: Enable ECR encryption at rest (enabled by default)
- •Tagging: Use consistent tags for all resources
High Availability
- •Multi-Region: Deploy repositories to multiple regions if needed
- •Cross-Account: Use cross-account access patterns for multi-account deployments
- •Image Replication: Consider ECR replication for disaster recovery
Cost Optimization
- •Lifecycle Policies: Implement ECR lifecycle policies to clean up old images
- •Image Tagging: Use consistent image tagging for easy cleanup
- •Monitoring: Monitor repository size and image count
State Management
- •Remote State: Use S3 backend for state management
- •State Locking: Enable DynamoDB table for state locking (optional)
- •State Encryption: Encrypt state files
- •State Versioning: Enable versioning on state bucket
Common Issues
Issue: Repository Already Exists
Symptom: Error Error: ... already exists
Solution:
# Import existing repository tofu import aws_ecr_repository.repository <repository-name>
Issue: OIDC Provider Not Found
Symptom: Error Error: OIDC provider does not exist
Solution:
# Create OIDC provider aws iam create-open-id-connect-provider \ --url https://token.actions.githubusercontent.com \ --client-id-list sts.amazonaws.com
Issue: Authentication Failed
Symptom: Error Error: error configuring Terraform AWS Provider
Solution:
# Verify credentials aws sts get-caller-identity # Check environment variables echo $AWS_ACCESS_KEY_ID echo $AWS_SECRET_ACCESS_KEY echo $AWS_DEFAULT_REGION
Issue: State Lock Error
Symptom: Error Error: Error acquiring the state lock
Solution:
# Check who has the lock tofu state pull # Force unlock (caution!) tofu force-unlock <LOCK_ID> # Or wait for other operation to complete
Examples
Complete ECR Repository Setup
# modules/ecr/main.tf
resource "aws_ecr_repository" "repository" {
name = var.repository_name
image_tag_mutability = "IMMUTABLE"
encryption_configuration {
encryption_type = "AES256"
}
image_scanning_configuration {
scan_on_push = true
}
}
ECR Lifecycle Policy
resource "aws_ecr_lifecycle_policy" "main" {
repository = aws_ecr_repository.repository.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "Keep last 50 images and remove older ones"
selection = {
tagStatus = "tagged"
tagPrefixList = ["v"]
countType = "imageCountMoreThan"
countNumber = 50
}
action = {
type = "expire"
}
}
]
})
}
GitHub Actions Workflow Example
name: Build and Push to ECR
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::123456789012:role/BETEKK-GithubWorkflowsUploadRole
aws-region: ap-southeast-1
- name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v1
- name: Build and push Docker image
run: |
docker build -t <repository-url>:latest .
docker push <repository-url>:latest
Reference Documentation
- •AWS ECR Documentation: https://docs.aws.amazon.com/AmazonECR/
- •Terraform AWS Provider (ECR): https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecr_repository
- •GitHub OIDC: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services
- •OpenTofu Documentation: https://opentofu.org/docs/
- •BETEKK Reference: ecr/betekk_probe_engine_main/
Related Skills
- •opentofu-aws-explorer: For managing other AWS resources
- •opentofu-provider-setup: For configuring AWS authentication and backends
- •opentofu-provisioning-workflow: For general infrastructure provisioning patterns