Skip to content

Commit 006a853

Browse files
justinprinceribbybibbyclaude
authored
Add terraform to deploy ACR Image Copy resources (#317)
Co-authored-by: Rob Best <rob.best@chainguard.dev> Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent b47d27e commit 006a853

14 files changed

Lines changed: 3627 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,5 @@ website/vendor
3535
*.winfile eol=crlf
3636

3737
.terraform.lock.hcl
38+
39+
.pre-commit-config.yaml

image-copy-acr/Dockerfile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Azure ACA only supports amd64, so we need to specify the platform for both stages of the build.
2+
FROM --platform=linux/amd64 cgr.dev/chainguard/go:latest-dev AS build
3+
4+
WORKDIR /app
5+
6+
COPY go.mod go.sum ./
7+
RUN go mod edit -dropreplace=github.com/chainguard-dev/platform-examples || true
8+
RUN go mod download
9+
10+
COPY main.go ./
11+
12+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/image-copy-acr .
13+
14+
FROM --platform=linux/amd64 cgr.dev/chainguard/static:latest-glibc
15+
16+
COPY --from=build /out/image-copy-acr /image-copy-acr
17+
18+
EXPOSE 8080
19+
ENTRYPOINT ["/image-copy-acr"]

image-copy-acr/README.md

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# image-copy-acr
2+
3+
This repo contains a Go application and a Terraform module that deploys the `image-copy-acr` service to Azure Container Apps. The service subscribes to Chainguard registry push events and automatically copies new images into an Azure Container Registry (ACR) when the event is received.
4+
5+
> This is an image mirroring service, not a pull-through caching service.
6+
> This service will mirror all images in your Chainguard registry to your Azure Container Registry; it is not currently configured for targeted image replication.
7+
8+
If you supply `existing_acr_name` (and `existing_acr_resource_group`), Terraform will reuse that registry instead of creating a new ACR instance. It will still create a new Resource Group to deploy the other required resources. If you want to create a new ACR instance, leave both variables unset and Terraform creates a fresh Basic-tier ACR in the generated resource group. The rest of the deployment (Container App, managed identity, Chainguard identity and subscription) is identical.
9+
10+
## How it works
11+
12+
```mermaid
13+
flowchart LR
14+
subgraph CG[Chainguard]
15+
REG[(Chainguard Registry)]
16+
SUB[/Chainguard Subscription/]
17+
REG -- image pushed --> SUB
18+
end
19+
20+
subgraph AZ[Azure]
21+
APP{{"Container App<br/>(ca-cgr-replicator)"}}
22+
ACR[(Azure Container Registry)]
23+
APP -- copies image --> ACR
24+
end
25+
26+
SUB -- "CloudEvent<br/>(HTTPS POST)" --> APP
27+
APP -- "pull image<br/>(OIDC)" --> REG
28+
```
29+
30+
1. Chainguard sends a CloudEvent (HTTPS POST) to the Container App's public HTTPS endpoint whenever an image is pushed to any repository in your group.
31+
2. The app requests a managed identity token scoped to a dedicated Azure AD application.
32+
3. The app copies the image to the target ACR using its managed identity for authentication.
33+
34+
---
35+
36+
## Resources created
37+
38+
### Azure
39+
40+
| Resource | Name | Notes |
41+
|---|---|---|
42+
| Resource Group | `rg-cgr-imagereplication-<random>` | All resources below are placed here unless noted |
43+
| Container Registry | `acrcgr<random>` | Created only when no existing ACR is supplied |
44+
| User-Assigned Managed Identity | `mi-cgr-acr-pushpull` | Used by the Container App to pull/push ACR images |
45+
| Role Assignment | `AcrPull` | Scoped to the resource group that contains the ACR |
46+
| Role Assignment | `AcrPush` | Scoped to the resource group that contains the ACR |
47+
| Container App Environment | `ace-cgr-replicator` | Consumption (serverless) plan |
48+
| Container App | `ca-cgr-replicator` | Runs the ko-built replicator image |
49+
| Azure AD Application | `cgr-image-copier-chainguard-audience` | Permission-free app used as the token audience for Chainguard STS exchange |
50+
| Azure AD Service Principal | `cgr-image-copier-chainguard-audience` | Required for Azure AD to recognise the app as a valid token resource |
51+
52+
### Chainguard
53+
54+
| Resource | Description |
55+
|---|---|
56+
| `chainguard_identity` | Workload identity bound to the managed identity via Azure AD claim matching |
57+
| `chainguard_rolebinding` | Grants the identity `registry.pull` on the target group |
58+
| `chainguard_rolebinding` | Grants the identity `viewer` on the target group (needed for signature verification) |
59+
| `chainguard_subscription` | Sends push events to the Container App's public URL |
60+
61+
---
62+
63+
## Prerequisites
64+
65+
### Tools
66+
67+
- [Terraform](https://developer.hashicorp.com/terraform/install) >= 1.3
68+
- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) — authenticated with `az login`
69+
- [Chainguard CLI (`chainctl`)](https://edu.chainguard.dev/chainguard/chainguard-enforce/how-to-install-chainctl/) — authenticated with `chainctl auth login`
70+
71+
### Azure permissions
72+
73+
The identity running Terraform needs:
74+
75+
- **Contributor** (or equivalent) on the subscription or resource group to create resources
76+
- **User Access Administrator** on the ACR's resource group to assign AcrPull/AcrPush roles
77+
- **Permission to register Azure AD applications** — required when `create_application = true` (the default). Enabled for all users in most tenants. If app registration is disabled in your tenant, have an admin create the app, set `create_application = false` and supply `token_scope` and `claim_match_audience` (see security note in the Variables section).
78+
79+
### Chainguard permissions
80+
81+
The identity running Terraform needs **Owner** or **Editor** on the target Chainguard group to create identities, role bindings, and subscriptions.
82+
83+
---
84+
85+
## Deployment steps
86+
87+
### 1. Authenticate
88+
89+
```sh
90+
az login
91+
chainctl auth login
92+
```
93+
94+
### 2. Copy and edit tfvars
95+
96+
```sh
97+
cp iac/terraform.tfvars.example iac/terraform.tfvars
98+
```
99+
100+
Edit `iac/terraform.tfvars`. See the [Variables](#variables) section below for the full list of available variables.
101+
102+
### 3. Authenticate to the ACR
103+
104+
Terraform builds and pushes the container image using `ko` during `tf apply`. The Azure CLI must be authenticated to the ACR before running `tf apply` so that ko can push the image.
105+
106+
**New ACR (Terraform will create it):** Targeted apply that will create the ACR registry which you will then log into using the `az acr login` command:
107+
108+
```sh
109+
cd iac
110+
tf init
111+
tf apply -target=azurerm_container_registry.new
112+
az acr login --name $(tf output -raw new_acr_name)
113+
```
114+
115+
**Existing ACR:**
116+
117+
```sh
118+
az acr login --name <existing_acr_name>
119+
```
120+
121+
### 4. Initialize and apply
122+
123+
```sh
124+
cd iac #If not already there from previous step
125+
tf init
126+
tf apply
127+
```
128+
129+
Review the plan and confirm. On the first run this takes a few minutes — ko builds and pushes the image before the Container App is created.
130+
131+
### 5. Verify
132+
133+
```sh
134+
tf output webhook_url # Chainguard subscription sink
135+
tf output dst_repo # Where images are copied to
136+
```
137+
138+
You can also check the Container App logs in the Azure Portal or via:
139+
140+
```sh
141+
az containerapp logs show \
142+
--name ca-cgr-replicator \
143+
--resource-group $(tf output -raw resource_group) \
144+
--follow
145+
```
146+
147+
You should see output similar to:
148+
```sh
149+
{"TimeStamp": "2026-04-27T21:07:15.01427", "Log": "Connecting to the container 'replicator'..."}
150+
{"TimeStamp": "2026-04-27T21:07:15.05442", "Log": "Successfully Connected to container: 'replicator' [Revision: 'ca-cgr-replicator--ralzvog', Replica: 'ca-cgr-replicator--ralzvog-76f4c6f8ff-z9dtl']"}
151+
```
152+
153+
Successful replication events will look like:
154+
```sh
155+
{"TimeStamp": "2026-04-27T21:49:57.5512087+00:00", "Log": "21:49:57 Copying cgr.dev/.../replication-test-004:latest-dev to acrcgrqiwfg3i1.azurecr.io/cgr/replication-test-004:latest-dev..."}
156+
{"TimeStamp": "2026-04-27T21:49:57.8207404+00:00", "Log": "21:49:57 Copied!"}
157+
```
158+
159+
---
160+
161+
## Variables
162+
163+
| Variable | Required | Default | Description |
164+
|---|---|---|---|
165+
| `chainguard_org` | yes || Chainguard organization name (e.g. `your.org.com`) |
166+
| `location` | no | `eastus` | Azure region (Update providers.tf as well for GovCloud) |
167+
| `dst_repo_prefix` | no | `cgr` | Path prefix in the ACR for copied images |
168+
| `ignore_referrers` | no | `true` | Skip copying signature/attestation tags |
169+
| `verify_signatures` | no | `false` | Verify Chainguard signatures before copying |
170+
| `existing_acr_name` | no | `""` | Name of an existing ACR to use; leave blank to create one |
171+
| `existing_acr_resource_group` | no | `""` | Resource group of the existing ACR; required when `existing_acr_name` is set |
172+
| `create_application` | no | `true` | Create a dedicated Azure AD app to scope Chainguard tokens |
173+
| `token_scope` | no | `""` | OAuth2 scope passed to GetToken (e.g. `api://<client-id>`); required when `create_application = false` |
174+
| `claim_match_audience` | no | `""` | Expected `aud` claim in the issued token; required when `create_application = false` |
175+
| `claim_match_issuer` | no | `""` | Expected `iss` claim; defaults to the v2 tenant-specific Azure AD issuer |
176+
177+
### Token audience security note
178+
179+
When `create_application = true` (the default), Terraform creates a dedicated, permission-free Entra ID application and automatically derives `token_scope` and `claim_match_audience` from it. Because the app has no permissions and its client ID is unique to this deployment, the token cannot be used to access any Azure resource or accepted by any other federated identity service.
180+
181+
If you don't have permissions to create an application, ask your administrator
182+
to create one for you and provide the client ID with the `token_scope` and
183+
`claim_match_audience` variables directly.
184+
185+
```hcl
186+
create_application = false
187+
token_scope = "api://<client-id>"
188+
claim_match_audience = "<client-id>"
189+
```
190+
191+
If you can't create an application, you may consider using the `api://AzureADTokenExchange` scope. However, be aware that `api://AzureADTokenExchange` is intended for inbound and cross-tenant workload identity federation. Tokens issued for it may be usable across other tenants or for applications that allow federation from other clouds — it is not scoped exclusively to Chainguard.
192+
193+
```hcl
194+
create_application = false
195+
token_scope = "api://AzureADTokenExchange"
196+
claim_match_audience = "fb60f99c-7a34-4190-8149-302f77469936"
197+
```
198+
199+
## Outputs
200+
201+
| Output | Description |
202+
|---|---|
203+
| `resource_group` | Name of the generated resource group |
204+
| `new_acr_name` | Name of the newly created ACR; null when reusing an existing one |
205+
| `acr_login_server` | ACR hostname (e.g. `myregistry.azurecr.io`) |
206+
| `acr_id` | Full Azure resource ID of the ACR |
207+
| `dst_repo` | Full destination repo prefix for copied images |
208+
| `webhook_url` | Public URL of the Container App / Chainguard subscription sink |
209+
| `managed_identity_id` | Resource ID of `mi-cgr-acr-pushpull` |
210+
| `chainguard_identity_id` | Chainguard identity ID used by the Container App |
211+
212+
---
213+
214+
## Teardown
215+
216+
```sh
217+
cd iac
218+
tf destroy
219+
```
220+
221+
This removes all resources created by this module, including the Chainguard subscription, identity, and role binding.

0 commit comments

Comments
 (0)