Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .azure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Azure Infrastructure — terrain-gpu-demo

This directory contains Bicep templates that provision the Azure infrastructure for the `terrain-gpu-demo` Web App.

## Resources Created

| Resource | Name | Details |
|---|---|---|
| App Service Plan | `ASP-terrain-gpu` | B1, Linux |
| Web App | `terrain-gpu-demo` | Container-based, system-assigned managed identity |
| Role Assignment | AcrPull on `maxfelkershared` ACR | Grants the webapp's MI pull access to the ACR |

## Prerequisites

- [Azure CLI](https://learn.microsoft.com/en-us/cli/azure/install-azure-cli) (`az`)
- [Bicep CLI](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/install) (installed automatically with recent `az` versions)
- Contributor access on the `maxfelker.com` resource group
- Owner / User Access Administrator access on the `shared` resource group (required to create the AcrPull role assignment)

## Deploy

```bash
az deployment group create \
--resource-group maxfelker.com \
--template-file .azure/main.bicep \
--parameters .azure/main.bicepparam
```

## How Managed Identity / No-Secret Auth Works

1. The Web App is created with a **system-assigned managed identity** (MI).
2. A **AcrPull role assignment** is created on the `maxfelkershared` ACR, scoped to the webapp's MI principal ID.
3. The site config sets `acrUseManagedIdentityCreds: true`, so the App Service host authenticates to ACR using the MI token — **no registry credentials or secrets are stored anywhere**.
4. The GitHub Actions workflow (see below) pushes a new image tag to ACR and then restarts/updates the webapp; the runtime pull still uses the same MI flow.

## GitHub Actions Auto-Deploy

The workflow at `.github/workflows/deploy.yml` triggers on every push to `main`:

1. Logs in to Azure via OIDC federated credentials (no long-lived secrets).
2. Builds and pushes the Docker image to `maxfelkershared.azurecr.io/terrain-gpu:latest`.
3. Calls `az webapp restart` (or `az webapp config container set`) to pick up the new image.

The OIDC trust is established between the GitHub Actions environment and an Azure AD app registration / federated credential — no passwords or client secrets are used in CI either.
25 changes: 25 additions & 0 deletions .azure/acr-role.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
@description('Name of the Azure Container Registry')
param acrName string

@description('Principal ID of the managed identity to assign the role to')
param principalId string

@description('Role definition ID (e.g. AcrPull: 7f951dda-4ed3-4680-a7ca-43fe172d538d)')
param roleDefinitionId string

@description('Resource ID of the web app (used for unique role assignment name)')
param webAppId string

resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = {
name: acrName
}

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(acr.id, webAppId, roleDefinitionId)
scope: acr
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
principalId: principalId
principalType: 'ServicePrincipal'
}
}
83 changes: 83 additions & 0 deletions .azure/main.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
@description('Azure region for all resources')
param location string = 'eastus'

@description('Name of the App Service Plan')
param appServicePlanName string = 'ASP-terrain-gpu'

@description('Name of the Web App')
param webAppName string = 'terrain-gpu-demo'

@description('Container image to deploy')
param containerImage string = 'maxfelkershared.azurecr.io/terrain-gpu:latest'

@description('Name of the Azure Container Registry')
param acrName string = 'maxfelkershared'

@description('Resource group containing the Azure Container Registry')
param acrResourceGroup string = 'shared'

// AcrPull built-in role definition ID
var acrPullRoleDefinitionId = '7f951dda-4ed3-4680-a7ca-43fe172d538d'

resource acr 'Microsoft.ContainerRegistry/registries@2023-07-01' existing = {
name: acrName
scope: resourceGroup(acrResourceGroup)
}

resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: appServicePlanName
location: location
kind: 'linux'
sku: {
name: 'B1'
tier: 'Basic'
}
properties: {
reserved: true
}
}

resource webApp 'Microsoft.Web/sites@2023-01-01' = {
name: webAppName
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
linuxFxVersion: 'DOCKER|${containerImage}'
acrUseManagedIdentityCreds: true
appSettings: [
{
name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
value: 'false'
}
{
name: 'PORT'
value: '80'
}
{
name: 'DOCKER_REGISTRY_SERVER_URL'
value: 'https://maxfelkershared.azurecr.io'
}
]
}
httpsOnly: true
}
}

module acrPullRoleAssignment 'acr-role.bicep' = {
name: 'acrPullRoleAssignment'
scope: resourceGroup(acrResourceGroup)
params: {
acrName: acrName
principalId: webApp.identity.principalId
roleDefinitionId: acrPullRoleDefinitionId
webAppId: webApp.id
}
}

output webAppName string = webApp.name
output webAppUrl string = 'https://${webApp.properties.defaultHostName}'
output managedIdentityPrincipalId string = webApp.identity.principalId
8 changes: 8 additions & 0 deletions .azure/main.bicepparam
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using './main.bicep'

param location = 'eastus'
param appServicePlanName = 'ASP-terrain-gpu'
param webAppName = 'terrain-gpu-demo'
param containerImage = 'maxfelkershared.azurecr.io/terrain-gpu:latest'
param acrName = 'maxfelkershared'
param acrResourceGroup = 'shared'
48 changes: 48 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Build and Deploy to Azure WebApp

on:
push:
branches: [main]

permissions:
id-token: write
contents: read

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Azure login (OIDC)
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: ACR login (ephemeral token via OIDC session)
run: az acr login --name maxfelkershared

- name: Build and push image to ACR
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
maxfelkershared.azurecr.io/terrain-gpu:${{ github.sha }}
maxfelkershared.azurecr.io/terrain-gpu:latest
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Deploy to Azure WebApp
uses: azure/webapps-deploy@v3
with:
app-name: terrain-gpu-demo
resource-group-name: maxfelker.com
images: maxfelkershared.azurecr.io/terrain-gpu:${{ github.sha }}