agentsclimarketplace

Scaffolding multi tenant azure apps

Skill alexpizarro/azure-lean-stack-skills/skills/scaffolding-multi-tenant-azure-apps

Azure apps that cost nothing when nobody's using them. A Claude Code skill pack — 14 composable skills, 37+ documented gotchas, branch-per-env CI/CD.

Install
npx -y skills add alexpizarro/azure-lean-stack-skills --skill scaffolding-multi-tenant-azure-apps

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Scaffolds a multi-tenant Azure app where each tenant gets its own resource group on a shared subscription, with one Bicep main.bicep driven by a single tenant parameter that cascades through every resource name. The default tenant keeps the original (single-tenant) names byte-identical so the pattern works for the first deployment with no migration. Use when onboarding the second tenant onto an app that started single-tenant, designing a multi-school/multi-clinic/multi-org SaaS, or refactoring shared resources to be per-tenant.

SKILL.md

6.4 KB, as published. Nobody here has run it

Scaffolding Multi-Tenant Azure Apps

Multi-tenant pattern with one resource group per tenant on a shared subscription. One tenant parameter drives the RG and every resource name. Adding a tenant = redeploy with a new param value.

When to use this pattern

NeedThis skill
One product, multiple isolated customers (schools, clinics, agencies)Yes
Per-tenant DB, per-tenant storage, per-tenant ACAYes
Multi-tenant SaaS where customers share a single DBNo — that's row-level multi-tenancy, not infra-level
Different products for different customersNo — use per-product subscriptions instead

Workflow checklist

Copy this checklist and tick items off:

Onboarding a new tenant:
- [ ] Step 1: Agree on the tenant slug (kebab-case, ≤20 chars, no Azure reserved words)
- [ ] Step 2: Verify the derived storage account name fits (≤24 chars, alphanumeric) — provide override if not
- [ ] Step 3: Copy infra/environments/prod.parameters.json → infra/environments/{tenant}.parameters.json
- [ ] Step 4: Edit tenant slug + storageAccountName override if needed
- [ ] Step 5: Create a new branch for this tenant if using branch-per-tenant (recommended for full isolation)
- [ ] Step 6: Add OIDC SP + federated credential bound to the new branch (skill: configuring-azure-oidc-for-github-actions)
- [ ] Step 7: Generate per-tenant SQL password + add as GitHub secret
- [ ] Step 8: Push to the tenant branch → workflow provisions the isolated stack
- [ ] Step 9: Verify per-tenant cost reporting: `az resource list --tag tenant=$SLUG -o table`

The core idea

main.bicep accepts a tenant param. Default value matches your original (single-tenant) naming so the first deployment doesn't need to migrate anything:

@description('Tenant slug — drives all resource names + the RG.')
param tenant string = 'acme-default'

// Storage account name can\'t have hyphens, ≤24 chars. Provide an override
// for tenants whose slug doesn\'t yield a valid storage name.
@description('Storage account name override (no hyphens, ≤24 chars). Empty = derive from tenant.')
param storageAccountName string = ''

module rg 'modules/resourceGroup.bicep' = {
  name: 'deploy-rg-${environment}'
  params: { name: '${tenant}-rg-${environment}' }
}

module storage 'modules/storage.bicep' = {
  scope: resourceGroup('${tenant}-rg-${environment}')
  params: {
    name: empty(storageAccountName) ? toLower(replace('${tenant}store', '-', '')) : storageAccountName
  }
}

// ... every other module follows the same pattern

See references/tenant-isolation.md for the trade-offs and references/one-rg-per-tenant.md for the why.

Adding a new tenant

# 1. Copy the parameter file
cp infra/environments/prod.parameters.json infra/environments/acme-corp.parameters.json

# 2. Edit the new file — set tenant slug + storage override if needed
{
  "parameters": {
    "environmentName": { "value": "prod" },
    "tenant":          { "value": "acme-corp" },
    "storageAccountName": { "value": "acmecorpstoreprod" }
  }
}

# 3. Deploy
az deployment sub create \
  --location australiaeast \
  --template-file infra/main.bicep \
  --parameters @infra/environments/acme-corp.parameters.json \
  --parameters sqlAdminPassword="$NEW_PASSWORD"

The deploy creates a fully isolated stack: new RG, new SQL, new Storage, new SWA, new ACA — same code paths, separate resources.

Per-tenant naming

ResourceName
Resource Group{tenant}-rg-{env}
Static Web App{tenant}-swa-{env}
SQL Server{tenant}-sql-{env}
SQL Database{tenant}-db-{env}
Storage account{tenant}store{env} (no hyphens, ≤24 chars)
Container App{tenant}-aca-{env}
App Insights{tenant}-ai-{env}

The org + project formula becomes a tenant formula. If you already shipped single-tenant with {org}-{project}-* names, set tenant = '{org}-{project}' as the default — the names stay byte-identical.

Per-tenant secrets

GitHub secrets are per-repo, not per-tenant. Two options:

  1. One repo per tenant — heavy, but each tenant gets its own SP, secrets, deployment pipeline. Best for fully separate compliance domains.
  2. One repo, environment-scoped secrets — use GitHub Environments named after the tenant. Each environment has its own SQL_ADMIN_PASSWORD, AZURE_CLIENT_ID, etc. Best for ~5–20 tenants.

The federated credential subject changes to:

repo:{owner}/{repo}:environment:acme-corp-prod

Per-tenant cost tracking

Tag every resource with the tenant slug. Azure Cost Management can then group by tag:

var tags = {
  tenant: tenant
  environment: environment
  managedBy: 'bicep'
}

Then in Cost Management:

Group by: Tag: tenant

Per-tenant invoices/chargebacks become trivial.

When to break this pattern

  • Shared resources by design — e.g. one ACR shared across all tenants for pulling the same image. Declare those in a separate infra/shared/main.bicep deployed once per region; refer to them by resource ID in tenant deployments.
  • Tenant-specific overrides — sometimes a customer needs a different SKU. Add a tenant-scoped param file and override.

Composes with

Templates

FilePurpose
templates/multi-tenant-main.bicepmain.bicep with the tenant param pattern

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.