The Use Case
Recently, I took on a project to deploy a new Lambda function in each of my AWS accounts to serve as a filter for Guard Duty alerts. Based on internal logic, this function would determine whether it requires immediate handling or can be addressed later. Because this function needed to be deployed across multiple accounts and regions (basically in each of my accounts), I faced the challenge of ensuring it could access a container image stored in a central Elastic Container Registry (ECR) repository in one of my AWS accounts.
This blog will focus on setting up cross-account access to ECR with region replication, rather than the specifics of my GuardDuty setup, which I might cover in a future post.
Reading the AWS documentation gave me a good sense of what configurations are necessary. Nonetheless, in this blog I will show you how I did it - the terraform way, while providing some tips and tricks to achieve the solution for my use case.
Terraform, an Infrastructure as Code (IaC) tool, provides a consistent and automated way to manage resources across multiple AWS accounts and regions. While this blog won't dive into all the details of Terraform, you can learn more about it here.
It is worth mentioning, that this blog requires a basic understanding of Terraform.
Overview
Many organizations in today’s cloud environments centralize container images within a single AWS account to streamline management, versioning, and security. However, additional configurations are required when Lambda functions across different accounts and regions need access to these images.
Following AWS’s same-region access requirement, each Lambda function must be able to access the image in the same region as its deployment. For that reason, I used the ECR’s region replication feature. This allowed the container image to be replicated to the necessary region.
A High-Level Overview of the Solution
Now that we have a high-level idea of what must be done, let's jump into the setup.
Some Facts to Consider
I manage multiple repositories in Account ID 111111111111
region us-east-2
. Some of my repositories will require Lambda access to pull their images, but not all of my repositories' images will require a replication. However, all repositories replicated to another region will also need lambda access.
In short:
- Lambda access ≠ replicating an image
- Replicating an image = Lambda access
The Solution
The Terraform module iterates over a list of repository names (var.repository_names
) using for_each.
Each repository is tagged with: Lambda-Access = true/false
The tag value is determined dynamically using the lookup function on a map (var.ecr_lambda_access
) that specifies which repositories should have Lambda access.
ecr_lambda_access = {
"repo_name_1" = "true"
"repo_name_2" = "true"
"repo_name_3" = "true"
}
Here’s the implementation:
resource "aws_ecr_repository" "example_registry" {
for_each = toset(var.repository_names)
name = each.value
tags = {
Lambda-Access = lookup(var.ecr_lambda_access, each.value, false)
}
}
To grant cross-account and same-account access to ECR repositories, along with Lambda-specific access, we add three statements to the aws_ecr_repository_policy resource:
Cross-account access grants general access to all repositories for specific external accounts.
Cross-account Lambda access grants Lambda functions in external accounts access to repositories tagged with Lambda-Access = true
.
Same-account Lambda access grants Lambda functions in the same account access to repositories tagged with Lambda-Access = true
.
Here’s the complete policy:
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "CrossAccountAccess",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::123456789012:root",
"arn:aws:iam::222222222222:root"
]
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:BatchCheckLayerAvailability",
"ecr:ListImages"
]
},
{
"Sid": "LambdaECRImageCrossAccountRetrievalPolicy",
"Effect": "Allow",
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
"ecr:ListImages"
],
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Condition": {
"StringLike": {
"aws:sourceArn": "arn:aws:lambda:us-east-2:123456789012:function:*"
},
"StringEquals": {
"aws:ResourceTag/Lambda-Access": "true"
}
}
},
{
"Sid": "LambdaECRSameAccountImageRetrievalPolicy",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer"
],
"Condition": {
"StringEquals": {
"aws:ResourceTag/Lambda-Access": "true"
}
}
}
]
}
After solving cross-account and same-account access for the default region, we address cross-account, same-region access using the aws_ecr_replication_configuration resource.
We replicate only to specific regions where the Lambda function resides. In our case, it is eu-west-1
in account ID 222222222222
. The replication configuration includes a rule specifying destination regions and repository filters.
The dynamic block is used to iterate over var.repository_to_replicate
, a list of repository prefixes to include in replication. It avoids hardcoding multiple repository_filter
blocks, making the configuration scalable and easier to manage as the list of repositories grows.
For each prefix, it dynamically generates a repository_filter
block with:
-
filter
: The prefix for repositories to match. -
filter_type
: Set toPREFIX_MATCH
This filter will control which repositories will be replicated.
Here’s the implementation:
resource "aws_ecr_replication_configuration" "example" {
replication_configuration {
rule {
destination {
region = "eu-west-1"
registry_id = data.aws_caller_identity.current.account_id
}
dynamic "repository_filter" {
for_each = toset(var.repository_to_replicate)
content {
filter = repository_filter.value
filter_type = "PREFIX_MATCH"
}
}
}
}
}
Region-Specific Repository Policies
We also need to define region-specific repository policies for the replicated repositories. This ensures that Lambda, in those regions, can access the replicated repositories.
To achieve this, we leverage the aws provider's alias for the replicated region (e.g. eu-west-1
) and create repository policies for the replicated repositories.
Here's how it looks:
provider "aws" {
alias = "ireland_region"
region = "eu-west-1"
}
resource "aws_ecr_repository_policy" "elastic_container_registry_policy_ireland" {
for_each = toset(var.repository_to_replicate)
provider = aws.ireland_region
repository = each.value
depends_on = [module.elastic_container_registry]
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "repositoryPerRegionStatement",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com",
"AWS": "arn:aws:iam::222222222222:root"
},
"Action": [
"ecr:BatchCheckLayerAvailability",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
"ecr:ListImages"
]
}
]
}
EOF
}
This approach keeps our configuration modular and ensures seamless access to replicated resources across regions.
Conclusion
That’s how I tackled the challenge of setting up cross-account and cross-region access for Lambda functions to pull container images from a centralized ECR repository, using a scalable and modular solution with Terraform.
I hope this blog provided some insights or inspiration for your own projects. Feel free to share your thoughts or questions—I’d be happy to hear from you!
Thank you for reading!
Top comments (0)