status: accepted date: 2025-01-28 decision-makers: Solutions Architect ποΈ, Pack Leader πΊ, Code Puppy πΆ consulted: Experience Architect π¨ (deployment UX), Web Puppy π΅οΈ (cost research) informed: All engineering teams, DevOps relates-to: INFRA-004
Context and Problem Statement
The Azure Governance Platform requires a container registry for storing Docker images used in App Service deployments. Initially, we used Azure Container Registry (ACR) as the native Azure solution. However, as the project matured, several friction points emerged:
Cost Concerns: ACR Standard tier costs approximately $5/day ($150/month) for a single region, even with minimal image storage. For a pre-revenue SaaS product, this is significant operational overhead.
Authentication Complexity: ACR requires service principal credentials or admin passwords in deployment pipelines. This conflicts with our evolving zero-secrets architecture (see ADR-0007).
CI/CD Integration: GitHub Actions workflows required additional secrets (ACR_USERNAME, ACR_PASSWORD) and Azure CLI authentication steps to push images.
Vendor Lock-in: Using ACR creates an Azure-specific dependency for a component (container storage) that doesn't require Azure-specific features.
How should we host container images to minimize cost while maintaining security and simplifying CI/CD?
Decision Drivers
- Cost (K.O. criterion for pre-revenue): Must be free or significantly cheaper than ACR ($150/month)
- Security: Must align with zero-secrets architecture; no long-lived credentials in CI/CD
- GitHub integration: Native integration with existing GitHub Actions workflows
- Multi-tenant readiness: Must support image distribution to multiple Azure subscriptions
- Azure compatibility: Azure App Service must be able to pull images from chosen registry
- Implementation effort: Migration must be completable in < 1 day
Considered Options
- GitHub Container Registry (GHCR) β GitHub's native container registry, free for public repos, integrated with GitHub Actions via
GITHUB_TOKEN - Azure Container Registry (ACR) with Basic tier β Stay on ACR but downgrade to Basic ($0.17/day) with reduced features
- Docker Hub β Public registry with free tier for open source projects
- Self-hosted registry β Deploy our own container registry on Azure compute
Decision Outcome
Chosen option: "GitHub Container Registry (GHCR)", because it is completely free for public repositories, eliminates all authentication secrets (uses GITHUB_TOKEN), has native GitHub Actions integration, and supports Azure App Service deployment via managed identity.
Option 2 (ACR Basic) was rejected because it still costs ~$5/month and doesn't solve the authentication complexity problem.
Option 3 (Docker Hub) was rejected because it requires separate authentication management and doesn't integrate as cleanly with GitHub Actions.
Option 4 (Self-hosted) was rejected because it creates operational burden and security responsibilities we don't need.
Migration Summary
| Aspect | Before (ACR) | After (GHCR) | Savings |
|---|---|---|---|
| Monthly cost | ~$150 | $0 | $1800/year |
| Authentication | Service principal secrets | GITHUB_TOKEN (ephemeral) |
Zero secrets |
| CI/CD complexity | Azure login + ACR login + push | Direct push | 50% reduction |
| Azure compatibility | Native | Requires managed identity | Same capability |
Architecture
Before (ACR):
βββββββββββββββββββ βββββββββββββββ βββββββββββββββββββ
β GitHub Actions ββββββΆβ ACR Login ββββββΆβ Azure Container β
β Workflow β β (secrets) β β Registry β
βββββββββββββββββββ βββββββββββββββ βββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β ACR admin credentials stored in GitHub Secrets β
β Rotate quarterly (operational burden) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
After (GHCR):
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β GitHub Actions ββββββΆβ GITHUB_TOKEN ββββββΆβ GitHub Containerβ
β Workflow β β (auto-injected) β β Registry (GHCR) β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Zero secrets required β
β Token auto-generated by GitHub, 15-minute lifetime β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
App Service Pull:
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Azure App ββββββΆβ Managed Identity ββββββΆβ GHCR (pull) β
β Service β β (UAMI) β β Public image β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
Implementation Details
GitHub Actions Workflow Changes:
# Before (ACR) - name: Login to ACR uses: azure/docker-login@v1 with: login-server: myregistry.azurecr.io username: ${{ secrets.ACR_USERNAME }} password: ${{ secrets.ACR_PASSWORD }} # After (GHCR) - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Auto-injected, no setup!Image Tag Changes:
# Before myregistry.azurecr.io/azure-governance-platform:staging # After ghcr.io/tygranlund/azure-governance-platform:stagingBicep Template Changes:
// App Service container configuration properties: { siteConfig: { linuxFxVersion: 'DOCKER|ghcr.io/tygranlund/azure-governance-platform:${environment}' } }Migration Script:
scripts/migrate-to-ghcr.shhandles:- Backfill existing images to GHCR
- Update all environment configurations
- Validate App Service can pull from GHCR
- Cleanup ACR images after validation
Consequences
Good:
- Cost: $1800/year savings (enough to fund other infrastructure)
- Security: Zero secrets in CI/CD or configuration
- Simplicity: GitHub-native workflow, no Azure-specific steps for container push
- Auditability: All container pushes visible in GitHub security tab
- Compatibility: Azure App Service supports GHCR via managed identity or public pulls
Bad:
- Public images: GHCR free tier requires public repositories (acceptable for open-core model)
- Azure dependency reduction: While good for portability, some Azure-native features (geo-replication, content trust) are lost
- Migration downtime: 5-minute window to update App Service container settings
Confirmation
Validation performed during migration:
- Image push validation:
docker push ghcr.io/tygranlund/azure-governance-platform:test - App Service pull validation: Deploy to staging slot, verify container starts
- Integration test: Full E2E test suite passing against GHCR-deployed instance
- Cleanup: ACR registry deleted after 7-day validation period
Migration completed January 28, 2025. Runbook: docs/runbooks/acr-to-ghcr-migration.md
STRIDE Security Analysis
| Threat Category | Risk Level | Mitigation |
|---|---|---|
| Spoofing | Low | GHCR images are tied to GitHub repository; only authorized workflows can push |
| Tampering | Low | GITHUB_TOKEN is ephemeral and scoped to the workflow run; no long-lived credentials to compromise |
| Repudiation | Low | GitHub audit log records all package publish events; signed commits required for workflow triggers |
| Information Disclosure | Low | Public repository and images align with open-core model; no secrets in image layers |
| Denial of Service | Medium | GHCR has rate limits (1000 pulls/hour unauthenticated); mitigated by Azure managed identity for authenticated pulls |
| Elevation of Privilege | Low | GITHUB_TOKEN has minimal scope (packages:write) and cannot access other repositories or workflows |
Overall Security Posture: GHCR improves security posture by eliminating persistent credentials. The ephemeral GITHUB_TOKEN with minimal scope reduces attack surface compared to ACR service principals with long-lived secrets.
Pros and Cons of the Options
Option 1: Stay on ACR Standard
- Good, because geo-replication and content trust features available
- Bad, because $150/month for pre-revenue product is unsustainable
- Bad, because requires ongoing credential rotation
- Bad, because Azure-specific lock-in without using Azure-specific features
Option 2: ACR Basic Tier
- Good, because reduces cost to ~$5/month
- Bad, because still requires secrets and credential management
- Bad, because Basic tier has storage and bandwidth limits that may constrain growth
- Bad, because doesn't solve the fundamental authentication complexity
Option 3: Docker Hub
- Good, because free tier available for open source
- Bad, because requires separate Docker Hub authentication
- Bad, because additional vendor to manage (rate limits, availability)
- Bad, because doesn't integrate as cleanly with GitHub Actions
Option 4: Self-Hosted Registry
- Good, because full control over infrastructure
- Bad, because creates operational burden (backups, updates, monitoring)
- Bad, because security responsibility shifts to our team
- Bad, because not cost-effective for expected scale
More Information
- Migration runbook:
docs/runbooks/acr-to-ghcr-migration.md - GHCR setup guide:
docs/GHCR_SETUP.md - Related authentication ADR:
docs/decisions/adr-0007-auth-evolution.md - Cost analysis: Saved $1800/year, redirected to Azure SQL Free Tier (see ADR-0009)
Template Version: MADR 4.0 (September 2024) with STRIDE Security Analysis
Last Updated: 2025-01-28
Maintained By: Solutions Architect ποΈ