Skip to content

Commit b47d27e

Browse files
ribbybibbyclaude
andauthored
aws-ecr-pull-through: add example for caching cgr.dev images via ECR (#322)
New module that wires up an AWS ECR pull-through cache rule for cgr.dev and a Lambda that mints, rotates, and cleans up Chainguard pull tokens backing the rule's Secrets Manager credential. The Lambda authenticates to Chainguard via AWS outbound identity federation (sts:GetWebIdentityToken), creates a static-keyed identity with a signed JWT, binds registry.pull, writes the credential into the secret, and deletes any expired identities of the same name. It runs once at apply time via aws_lambda_invocation and on a configurable EventBridge schedule (default daily) thereafter. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 72b012b commit b47d27e

11 files changed

Lines changed: 978 additions & 0 deletions

File tree

aws-ecr-pull-through/README.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# aws-ecr-pull-through
2+
3+
Terraform module that configures an **AWS ECR pull-through cache** for `cgr.dev`. The pull token is automatically rotated by a scheduled Lambda function.
4+
5+
## How it works
6+
7+
```mermaid
8+
flowchart TD
9+
Schedule([first run / on schedule]) --> Lambda
10+
Lambda[Lambda<br/>rotator] -->|mints| Identity[Chainguard identity<br/>static, RSA-keyed]
11+
Lambda -->|writes| Secret[Secrets Manager<br/>ecr-pullthroughcache/...]
12+
Rule[ECR pull-through<br/>cache rule] -->|service-linked role reads| Secret
13+
Rule -->|on cache miss, pulls from cgr.dev| CGR[(cgr.dev)]
14+
Consumer([AWS IAM role / user]) -->|docker pull| Rule
15+
```
16+
17+
1. The Lambda authenticates to the Chainguard API using an [assumable identity](https://edu.chainguard.dev/chainguard/administration/assumable-ids/identity-examples/aws-identity-oidc/).
18+
2. It creates a pull token.
19+
3. It updates the Secrets Manager secret used by the pull-through cache rule.
20+
4. On each rotation, the Lambda deletes expired tokens.
21+
22+
---
23+
24+
## Prerequisites
25+
26+
### Tools
27+
28+
- [Terraform](https://developer.hashicorp.com/terraform/install) >= 1.5
29+
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) — authenticated (`aws configure` or `AWS_PROFILE`)
30+
- [Chainguard CLI (`chainctl`)](https://edu.chainguard.dev/chainguard/chainguard-enforce/how-to-install-chainctl/) — authenticated with `chainctl auth login`
31+
- [`ko`](https://ko.build/) credential helper available so Terraform can push the Lambda image to ECR
32+
33+
### AWS account configuration
34+
35+
[Outbound identity federation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_outbound_identity_federation.html) must be enabled on the AWS account. This is the mechanism the Lambda uses to authenticate to Chainguard.
36+
37+
### AWS permissions
38+
39+
The identity running Terraform needs to create:
40+
41+
- ECR repositories and pull-through cache rules
42+
- Secrets Manager secrets
43+
- IAM roles + inline policies
44+
- Lambda functions (and invoke them once for bootstrap)
45+
- EventBridge schedules
46+
47+
### Chainguard permissions
48+
49+
The identity running Terraform needs **Owner** or **Editor** on the target Chainguard group to create the Lambda's identity, the custom cleaner role, and the role bindings.
50+
51+
---
52+
53+
## Deployment
54+
55+
### 1. Authenticate
56+
57+
```sh
58+
aws configure # or set AWS_PROFILE
59+
chainctl auth login
60+
```
61+
62+
### 2. Configure
63+
64+
Set your tfvars (at minimum, `org_name`):
65+
66+
```hcl
67+
# iac/terraform.tfvars
68+
org_name = "your.org"
69+
```
70+
71+
### 3. Apply
72+
73+
```sh
74+
cd iac
75+
tf init
76+
tf apply
77+
```
78+
79+
### 4. Use the cache
80+
81+
```sh
82+
PREFIX=$(tf output -raw pull_through_cache_uri)
83+
docker pull ${PREFIX}/<image>:<tag>
84+
# e.g. for cgr.dev/your.org/python:latest
85+
docker pull ${PREFIX}/python:latest
86+
```
87+
88+
The first pull of any tag is slow (ECR fetches it from `cgr.dev` synchronously and creates the cached repo). Subsequent pulls are cached.
89+
90+
---
91+
92+
## Operations
93+
94+
### Force a rotation
95+
96+
```sh
97+
aws lambda invoke --function-name $(tf output -raw rotator_function_name) /dev/null
98+
```
99+
100+
### Inspect minted pull tokens
101+
102+
```sh
103+
chainctl iam identity list --parent $(tf output -raw chainguard_identity_id | cut -d/ -f1) --type pull_token
104+
```
105+
106+
### Teardown
107+
108+
```sh
109+
cd iac
110+
tf destroy
111+
```
112+
113+
**Note**: pull-token identities the Lambda created in Chainguard are not deleted by `tf destroy` — they'll expire on their own (within `token_ttl`). To remove them immediately, list them with `chainctl iam identity list` and delete them manually.
114+
115+
---
116+
117+
## Resources created
118+
119+
### AWS
120+
121+
| Resource | Notes |
122+
|---|---|
123+
| `aws_ecr_pull_through_cache_rule` | Maps `cgr.dev/<group>` to the local `<ecr_repository_prefix>` namespace |
124+
| `aws_secretsmanager_secret` | Named `ecr-pullthroughcache/<name>`; holds the rotated pull token |
125+
| `aws_secretsmanager_secret_version` | Initial placeholder; `secret_string` is owned by the Lambda thereafter (`ignore_changes`) |
126+
| `aws_ecr_repository` | Dedicated repo for the rotator Lambda's container image |
127+
| `aws_iam_role` (lambda) | Lambda execution role with `secretsmanager:PutSecretValue` on the secret |
128+
| `aws_iam_role` (scheduler) | EventBridge Scheduler role that invokes the Lambda |
129+
| `aws_lambda_function` | The rotator, packaged as an OCI image built with `ko` |
130+
| `aws_lambda_invocation` | One-shot bootstrap so the secret is populated immediately on apply |
131+
| `aws_scheduler_schedule` | Recurring rotation (default `rate(1 day)`) |
132+
133+
### Chainguard
134+
135+
| Resource | Description |
136+
|---|---|
137+
| `chainguard_identity` | Workload identity for the Lambda, bound to the AWS role |
138+
| `chainguard_rolebinding` (creator) | Grants the built-in `registry.pull_token_creator` role on the group |
139+
| `chainguard_role` (cleaner) | Custom role with `iam.identities.delete` only |
140+
| `chainguard_rolebinding` (cleaner) | Grants the custom cleaner role to the Lambda's identity |
141+
142+
The Lambda also creates one `chainguard_identity` (of type static, with an attached `chainguard_rolebinding`) per rotation. These are the actual pull tokens — outside Terraform's management — and the Lambda is responsible for deleting them once they expire.
143+
144+
---
145+
146+
## Variables
147+
148+
| Variable | Required | Default | Description |
149+
|---|---|---|---|
150+
| `org_name` | yes || Chainguard organization name (e.g. `your.org`) |
151+
| `suffix` | no | (random) | Suffix appended to every resource name; auto-generated if empty |
152+
| `ecr_repository_prefix` | no | `chainguard-<suffix>` | Local ECR namespace for cached images |
153+
| `upstream_repository_prefix` | no | `var.org_name` | Upstream prefix on `cgr.dev` |
154+
| `token_ttl` | no | `336h` | Pull-token lifetime as a Go duration string (e.g. `336h` = 14 days) |
155+
| `rotation_schedule` | no | `rate(1 day)` | EventBridge schedule expression for rotation |
156+
157+
---
158+
159+
## Outputs
160+
161+
| Output | Description |
162+
|---|---|
163+
| `suffix` | Suffix used across all resource names in this deployment |
164+
| `ecr_repository_prefix` | ECR namespace for cached images |
165+
| `pull_through_cache_uri` | Full base URI to pull through (e.g. `123456789.dkr.ecr.us-east-1.amazonaws.com/cgr`) |
166+
| `secret_arn` | ARN of the Secrets Manager secret backing the cache rule |
167+
| `rotator_function_name` | Lambda function name for the rotator |
168+
| `chainguard_identity_id` | Chainguard identity ID for the rotator Lambda |
169+

aws-ecr-pull-through/go.mod

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
module github.com/chainguard-dev/platform-examples/aws-ecr-pull-through
2+
3+
go 1.25.7
4+
5+
require (
6+
chainguard.dev/sdk v0.1.54
7+
github.com/aws/aws-lambda-go v1.54.0
8+
github.com/aws/aws-sdk-go-v2 v1.41.6
9+
github.com/aws/aws-sdk-go-v2/config v1.32.16
10+
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.41.6
11+
github.com/aws/aws-sdk-go-v2/service/sts v1.42.0
12+
github.com/go-jose/go-jose/v4 v4.1.4
13+
github.com/kelseyhightower/envconfig v1.4.0
14+
google.golang.org/protobuf v1.36.11
15+
)
16+
17+
require (
18+
chainguard.dev/go-grpc-kit v0.17.17 // indirect
19+
cloud.google.com/go/auth v0.20.0 // indirect
20+
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
21+
cloud.google.com/go/compute/metadata v0.9.0 // indirect
22+
github.com/aws/aws-sdk-go-v2/credentials v1.19.15 // indirect
23+
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.22 // indirect
24+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect
25+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect
26+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.23 // indirect
27+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.8 // indirect
28+
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.22 // indirect
29+
github.com/aws/aws-sdk-go-v2/service/signin v1.0.10 // indirect
30+
github.com/aws/aws-sdk-go-v2/service/sso v1.30.16 // indirect
31+
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.20 // indirect
32+
github.com/aws/smithy-go v1.25.0 // indirect
33+
github.com/beorn7/perks v1.0.1 // indirect
34+
github.com/bits-and-blooms/bitset v1.24.4 // indirect
35+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
36+
github.com/chainguard-dev/clog v1.8.0 // indirect
37+
github.com/go-logr/logr v1.4.3 // indirect
38+
github.com/go-logr/stdr v1.2.2 // indirect
39+
github.com/google/s2a-go v0.1.9 // indirect
40+
github.com/google/uuid v1.6.0 // indirect
41+
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
42+
github.com/googleapis/gax-go/v2 v2.21.0 // indirect
43+
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 // indirect
44+
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
45+
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
46+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
47+
github.com/prometheus/client_golang v1.23.2 // indirect
48+
github.com/prometheus/client_model v0.6.2 // indirect
49+
github.com/prometheus/common v0.67.5 // indirect
50+
github.com/prometheus/procfs v0.20.1 // indirect
51+
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
52+
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
53+
go.opentelemetry.io/otel v1.43.0 // indirect
54+
go.opentelemetry.io/otel/metric v1.43.0 // indirect
55+
go.opentelemetry.io/otel/trace v1.43.0 // indirect
56+
go.yaml.in/yaml/v2 v2.4.4 // indirect
57+
golang.org/x/crypto v0.49.0 // indirect
58+
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
59+
golang.org/x/net v0.52.0 // indirect
60+
golang.org/x/oauth2 v0.36.0 // indirect
61+
golang.org/x/sys v0.43.0 // indirect
62+
golang.org/x/text v0.36.0 // indirect
63+
google.golang.org/api v0.276.0 // indirect
64+
google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect
65+
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
66+
google.golang.org/grpc v1.80.0 // indirect
67+
)

0 commit comments

Comments
 (0)