Skip to content

Phase 6: deploy the metrics infrastructure (I2–I7) - #12

Merged
ziadantar90 merged 6 commits into
developmentfrom
claude/project-thread-y5bd3l
Nov 1, 2026
Merged

ziadantar90 merged 6 commits into
developmentfrom
claude/project-thread-y5bd3l

Conversation

@ziadantar90

Copy link
Copy Markdown
Contributor

Requested by Ziad · project thread

Completes Phase 6 of the plan in one PR, one commit per item.

What's in it

Item Commit What
I2 Add a container registry and the app's pull identity Basic ACR (no admin user, no anonymous pull); user-assigned identity with AcrPull
I3 Run the metrics API in Container Apps Log Analytics + Consumption environment; Container App on HTTPS :8080 with /healthz probes, scale-to-zero; Cosmos Built-in Data Contributor via sqlRoleAssignments; AUTH_MODE, ENTRA_TENANT_ID, ENTRA_AUDIENCE set (key mode stores METRICS_API_KEY as a secret)
I4 infra/setup-entra.sh API app reg with the Events.Write application role and an app ID URI; dashboard app reg with redirect URI, assignment required, signed-in user or --dashboard-group assigned. Idempotent; writes .env.entra (mode 600, git-ignored). CI's Bicep job now also runs shellcheck on infra/*.sh (job name unchanged, so required checks are unaffected)
I5 Container Apps sign-in authConfigs/current with the Entra provider; excludedPaths: ['/events', '/healthz']. main.bicepparam reads the tag, IDs and secret from env vars, so nothing secret lands in the repo
I6 infra/grant-events-write.sh Grants Events.Write to the pipeline's service principal; header documents the single az rest equivalent
I7 infra/README.md Two-pass deploy, outputs, options, teardown; root README and CHANGELOG updated

One deviation from the plan

I3 says system-assigned identity. This uses a user-assigned identity instead: a system-assigned identity only exists after the app is created, so its AcrPull grant would arrive after the first image pull and the first deploy would fail. The app finds the identity through AZURE_CLIENT_ID, which M12's DefaultAzureCredential already supports.

Deploy flow

  1. Deploy with no imageTag: Cosmos, ACR, identity, environment; outputs the app's future metricsUrl.
  2. setup-entra.sh --app-url <metricsUrl>.
  3. az acr build the image.
  4. source .env.entra and deploy again with PULLPICKET_IMAGE_TAG set.
  5. grant-events-write.sh --principal <service connection client id>.

Testing

  • Bicep 0.48.1 (the CI pin): build + lint of every file under infra/, zero findings.
  • shellcheck clean on both scripts.
  • Both scripts run against a stub az covering first run, rerun (no duplicate secret or assignment), tenant refusing api://<name> (falls back to api://<client id>), --dashboard-group, an already-granted principal and an unknown principal.
  • Not deployed to a real subscription; that is R3 in the plan.

Generated by Claude Code

Basic ACR with no admin user and no anonymous pull. A user-assigned
identity holds AcrPull on it, created before the Container App so the
app's first image pull already has the grant (a system-assigned identity
would only exist after that pull).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8A4vwVykbRMCMnpzV6KzL
Adds a Consumption Container Apps environment with Log Analytics, and the
Container App itself: external HTTPS ingress on 8080, /healthz probes,
scale to zero by default, image pulled from ACR with the user-assigned
identity. The identity gets the Cosmos DB Built-in Data Contributor
data-plane role through sqlRoleAssignments (key auth stays disabled), and
the app finds it through AZURE_CLIENT_ID.

The app gets AUTH_MODE (default entra), ENTRA_TENANT_ID (this tenant by
default) and ENTRA_AUDIENCE, without which it refuses to start; key mode
stores METRICS_API_KEY as a Container Apps secret.

The app is only deployed when imageTag is set, so the first deploy creates
the registry and environment, and reports the app's future URL for the
Entra redirect URI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8A4vwVykbRMCMnpzV6KzL
Bicep cannot create app registrations (D8), so an az CLI script does:

- <prefix>-metrics-api with the Events.Write application role (service
  principals only) and an application ID URI, api://<prefix>-metrics by
  default, falling back to api://<client id> where the tenant requires it.
- <prefix>-metrics-dashboard, the Container Apps sign-in app, with its
  redirect URI from the first deployment's metricsUrl, assignment
  required, and the signed-in user (or --dashboard-group) assigned.

It is safe to rerun: registrations are found by name, and the dashboard
client secret is only added on creation or with --rotate-secret. Results
go to .env.entra (mode 600, git-ignored) as export lines for
main.bicepparam.

The Bicep CI job now also runs shellcheck over infra/*.sh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8A4vwVykbRMCMnpzV6KzL
The Container App gets an authConfigs/current with the Entra provider
(the dashboard app registration from setup-entra.sh, issuer pinned to the
tenant): unauthenticated requests are sent to sign-in, except
excludedPaths /events (which checks its own credential, AUTH_MODE) and
/healthz. Who may sign in is set in Entra: the dashboard app requires
assignment, so only assigned users or groups get through.

The client secret is a Container Apps secret. main.bicepparam reads the
image tag, Entra IDs and that secret from the environment (the
.env.entra file setup-entra.sh writes), so none of them land in a file in
the repo. The app module requires both dashboard values, so the app
cannot be deployed without sign-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8A4vwVykbRMCMnpzV6KzL
Assigns the API's Events.Write app role to the service principal behind
an Azure DevOps service connection (or any sender), given its client id
or object id. It finds the API from .env.entra or by name, checks the
role exists, and skips the grant if it is already there. The header
documents the equivalent single az rest call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8A4vwVykbRMCMnpzV6KzL
infra/README.md walks through the two-pass deploy: base resources and the
app's URL, Entra registration, image build with az acr build, the app,
the pipeline's Events.Write grant and its variable group, and a check
that sign-in and the 401 on POST /events work. It also lists outputs,
options and teardown, including the app registrations.

The root README now links it instead of the in-progress note, and the
CHANGELOG records the new infrastructure and scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D8A4vwVykbRMCMnpzV6KzL
@ziadantar90 ziadantar90 self-assigned this Nov 1, 2026
@ziadantar90
ziadantar90 merged commit ead2d0f into development Nov 1, 2026
5 checks passed
@ziadantar90
ziadantar90 deleted the claude/project-thread-y5bd3l branch November 1, 2026 20:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants