When working with AWS ECR repositories in Terraform, we often need to apply policies across multiple repositories and accounts. The core challenge is implementing nested iterations similar to bash's nested for-loops, but in Terraform's declarative syntax.
Terraform doesn't support traditional loops, but we can achieve similar functionality using count
meta-argument and combinations of built-in functions. Here's how to properly structure the solution:
variable "list_of_allowed_accounts" {
type = list(string)
default = ["111111111", "2222222"]
}
variable "list_of_images" {
type = list(string)
default = ["alpine", "java", "jenkins"]
}
First, let's create a proper template file (ecr_policy.tpl):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCrossAccountPull",
"Effect": "Allow",
"Principal": {
"AWS": "${account_id}"
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
]
}
]
}
Here's the correct way to implement nested iteration in Terraform:
data "template_file" "ecr_policy_allowed_accounts" {
count = length(var.list_of_images) * length(var.list_of_allowed_accounts)
template = file("${path.module}/ecr_policy.tpl")
vars = {
account_id = var.list_of_allowed_accounts[count.index % length(var.list_of_allowed_accounts)]
image_name = var.list_of_images[floor(count.index / length(var.list_of_allowed_accounts))]
}
}
resource "aws_ecr_repository" "images" {
count = length(var.list_of_images)
name = var.list_of_images[count.index]
}
resource "aws_ecr_repository_policy" "repo_policy_allowed_accounts" {
count = length(var.list_of_images) * length(var.list_of_allowed_accounts)
repository = aws_ecr_repository.images[
floor(count.index / length(var.list_of_allowed_accounts))
].name
policy = data.template_file.ecr_policy_allowed_accounts[count.index].rendered
}
For more complex scenarios, consider using for_each
with maps:
locals {
combinations = {
for pair in setproduct(var.list_of_images, var.list_of_allowed_accounts) :
"${pair[0]}-${pair[1]}" => {
image = pair[0]
account = pair[1]
}
}
}
resource "aws_ecr_repository_policy" "repo_policy_foreach" {
for_each = local.combinations
repository = aws_ecr_repository.images[
index(var.list_of_images, each.value.image)
].name
policy = templatefile("${path.module}/ecr_policy.tpl", {
account_id = each.value.account
})
}
When implementing this pattern:
- Be mindful of the 0.12+ syntax changes
- Remember that count.index is zero-based
- The modulo (%) operation helps cycle through the nested list
- floor() helps with integer division for the outer loop
If you encounter errors:
- Check for off-by-one errors in index calculations
- Verify all variables are properly interpolated
- Ensure your template file exists at the specified path
- Validate the generated JSON policy against AWS requirements
When working with AWS ECR repositories, we often need to create cross-account access policies where multiple accounts need access to multiple container images. Terraform's declarative nature makes nested loops non-trivial compared to imperative languages.
We have two key variables to work with:
variable "list_of_allowed_accounts" {
type = list(string)
default = ["111111111", "2222222"]
}
variable "list_of_images" {
type = list(string)
default = ["alpine", "java", "jenkins"]
}
Instead of true nested loops, we use a flattened combination with setproduct:
locals {
policy_combinations = [
for pair in setproduct(var.list_of_images, var.list_of_allowed_accounts) : {
image = pair[0]
account_id = pair[1]
}
]
}
Here's the complete solution:
data "template_file" "ecr_policy_allowed_accounts" {
for_each = { for idx, val in local.policy_combinations : idx => val }
template = file("${path.module}/ecr_policy.tpl")
vars = {
account_id = each.value.account_id
image_name = each.value.image
}
}
resource "aws_ecr_repository" "images" {
for_each = toset(var.list_of_images)
name = each.value
}
resource "aws_ecr_repository_policy" "repo_policy_allowed_accounts" {
for_each = { for idx, val in local.policy_combinations : idx => val }
repository = aws_ecr_repository.images[each.value.image].name
policy = data.template_file.ecr_policy_allowed_accounts[each.key].rendered
}
The policy template (ecr_policy.tpl) would look like:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CrossAccountAccess",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::${account_id}:root"
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
}
]
}
For newer Terraform versions, dynamic blocks provide cleaner syntax:
resource "aws_ecr_repository_policy" "repo_policy" {
for_each = aws_ecr_repository.images
repository = each.value.name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
for account in var.list_of_allowed_accounts : {
Sid = "CrossAccountAccess-${account}"
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${account}:root" }
Action = [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability"
]
}
]
})
}