Skip to the content.

📚 Quick links: Self-service provisioning · Setup: GitHub · Setup: Azure · Setup: AWS · Roadmap · Troubleshooting · Contributing · Pages info · Source on GitHub


A self-service platform that provisions and reconciles application resources across Azure, AWS and other clouds, driven by GitHub Actions and Terraform. External systems trigger the platform through repository_dispatch or workflow_dispatch events; the platform takes care of standing up secure, observable, production-grade environments without the requesting team having to write any infrastructure code.

The platform supports two operating modes in a single workflow:

Status: functional end-to-end. Plan → apply → verify, application repo creation from template, GitHub Environments + variables, OIDC federated credentials, CI observation and per-run tracking issue are all wired. See Roadmap for what’s next.


What this platform does

Given an existing cloud account, the platform provisions an opinionated container stack for an application across three environments — dev, staging, prod — following each cloud’s well-architected guidance for security, observability and connectivity.

Whatever the target, every environment comes out with the same properties:

The concrete services differ per cloud. Each archetype documents its own stack:

Cloud Stack Provision Setup
Azure App Service (Linux, container) with VNet integration, Private Endpoint, Managed Identity, Key Vault references, Application Insights + Log Analytics, deployment slots form guide
AWS ECS Fargate behind an Application Load Balancer, with VPC + NAT, ACM certificates, CloudWatch logs and alarms, X-Ray, CodeDeploy blue/green form guide

The GitHub side of the setup is identical for every cloud — do that once, then follow the guide for your target.


Architecture: Decoupled App & Infra Templates

The platform separates application code from infrastructure code, and the application phase is entirely optional:

Component Template Repo naming Role
Infra one template-terraform-* archetype {app-name}-infra IaC: networking, compute, monitoring, etc. Terraform modules. Always runs.
App Code (optional) template-helloworld-express {app-name} Runtime: Node.js, Python, Java, etc. Owns CI/CD. Runs only when app_template_repo is set.

When app_template_repo is provided (full mode), the platform:

  1. Creates {app-name}-infra from the infra template (Terraform) ← infrastructure provisioning
  2. Runs Terraform against the infra repo to stand up the cloud resources
  3. Creates {app-name} from the app template (e.g., Node.js starter) ← app CI/CD logic
  4. Sets GitHub environment variables and federated credentials on the app repo

When app_template_repo is omitted (infra-only mode), steps 3 and 4 are skipped — useful for Landing Zones, foundational platform components, or any scenario where application code is managed separately.

This decoupling means:

Infrastructure Templates (Archetypes)

Each infra template is a self-contained Terraform module set covering an infrastructure pattern:

Each archetype ships its own Checkov baselines — strict for prod (mandatory HA and private networking), relaxed for dev/staging — and its own scripts/verify.sh.

When provisioning, you specify which infra template to use as an input parameter, which is what selects the target architecture.

State management

State lives in the target cloud’s own object storage, with one backend per account + application so that unrelated apps sharing an account remain decoupled, and one state object per environment inside it. The platform bootstraps that backend itself — it is a platform concern, not something each template re-implements — and hardens it the same way everywhere: versioned, encrypted at rest, private, and reachable only over TLS.

Cloud Backend Identity model
Azure Storage account sttf<app12><sub8>, container tfstate AAD-auth only — no shared keys
AWS S3 bucket tf-state-<app>-<account8> IAM via OIDC role assumption

Beyond the cloud side, the platform also takes care of the application repository: it creates a new repo from a template you choose, configures GitHub Environments + variables, registers the per-env OIDC federated credentials on the platform service principal, observes the auto-triggered CI run, and writes a per-run issue summarising plan deltas and verification test counts.

Architecture at a glance

operator                ┌─────────────────────────────────────────────┐
  ├─ web UI (Pages) ──► │  GitHub Actions: <cloud> - Provision &      │
  ├─ trigger script ──► │                  Reconcile App Resources    │
  └─ raw curl ────────► │   1. resolve & validate inputs              │
                        │   2. checkov scan (per env)                 │
                        │   3. terraform fmt                          │
                        │   4. bootstrap tfstate (reusable)           │
                        │   5. terraform plan (per env)               │
                        │   6. terraform apply (per env)              │
                        │   7. verify (reusable, per env)             │
                        │   8. create app repo from template          │
                        │   9. open per-run tracking issue            │
                        │  10. configure GitHub Environments + vars   │
                        │  11. register OIDC fed-creds on SP          │
                        │  12. observe CI run (first creation only)   │
                        │  13. summarise + comment on the issue       │
                        └────┬────────────────────────────────┬───────┘
                             │                                │
            OIDC, no secrets │                                │ GH_PAT
                             ▼                                ▼
      ┌──────────────────────────────────┐  ┌────────────────────────────────┐
      │ cloud account                    │  │  GitHub: app repo              │
      │  ├── state backend, per app      │  │   ├── from <app_template_repo> │
      │  │     └── one object per env    │  │   ├── envs: dev/staging/prod   │
      │  │           tfstate/{env}/      │  │   ├── per-env variables        │
      │  └── one group per environment   │  │   ├── ci.yml — build & test &  │
      │        ├── networking + flow log │  │   │   deploy to dev triggered  │
      │        ├── monitoring + tracing  │  │   │   by push on main          │
      │        └── compute + ingress     │  │   ├── release.yml — deploy to  │
      │                                  │  │   │   staging/prod triggered   │
      │                                  │  │   │   by new release creation  │
      │                                  │  │   └── per-run issue + summary  │
      └──────────────────────────────────┘  └────────────────────────────────┘

Repository layout

.
├── .checkov.nonprod.yaml                    # Relaxed skips for dev/staging
├── .checkov.yaml                            # Checkov rules + skips for prod (strict)
├── .github/workflows/                       # one file per cloud; names are prefixed "Azure - " / "AWS - "
│   ├── bootstrap-tfstate.yml                # Reusable: create the state backend in Azure
│   ├── bootstrap-tfstate-aws.yml            #   └── AWS counterpart
│   ├── delete-app-resources-all.yml         # Manual: delete every env + state for an app in Azure
│   ├── delete-app-resources-all-aws.yml.    #   └── AWS counterpart
│   ├── delete-app-resources-single.yml      # Manual/callable: delete one environment in Azure
│   ├── delete-app-resources-single-aws.yml. #   └── AWS counterpart
│   ├── delete-resource-group.yml            # Reusable: delete a single resource group in Azure
│   ├── delete-resource-group-aws.yml        #   └── AWS counterpart
│   ├── detect-drift.yml                     # Scheduled/callable: read-only drift sweep in Azure
│   ├── detect-drift-aws.yml                 #   └── AWS counterpart
│   ├── provision-infrastructure.yml         # Main workflow: end-to-end pipeline in Azure
│   ├── provision-infrastructure-aws.yml     #   └── AWS counterpart
│   ├── tag-app-resources.yml                # Manual: merge compliance tags onto all app resources in Azure
│   ├── tag-app-resources-aws.yml            #   └── AWS counterpart
│   ├── verify-infrastructure.yml            # Reusable: runs the infra repo's verify script in Azure
│   └── verify-infrastructure-aws.yml        #   └── AWS counterpart
├── docs/                                    # GitHub Pages site (Jekyll-rendered)
│   ├── _config.yml                          # Jekyll config
│   ├── CONTRIBUTING.md                      # Contribution guidelines (Pages mirror)
│   ├── index.md                             # Pages homepage (this file)
│   ├── pages.md                             # Pages site map & how to enable
│   ├── provision.html                       # Provisioning entry point — picks a cloud
│   ├── provision-azure.html                 # Self-service provisioning form (Azure)
│   ├── provision-aws.html                   # Self-service provisioning form (AWS)
│   ├── setup-github.md                      # One-time GitHub setup (every cloud)
│   ├── setup-azure.md                       # One-time Azure setup
│   └── troubleshooting.md                   # GitHub/Actions, tfstate & cloud-login issues
├── scripts/                                 # cloud-specific scripts carry an -azure / -aws suffix
│   ├── bootstrap-tfstate-azure.sh           # Idempotent az-cli state bootstrap (storage account)
│   ├── bootstrap-tfstate-aws.sh             # Idempotent aws-cli state bootstrap (S3 bucket)
│   ├── tag-app-resources-azure.sh           # Merge an arbitrary tag set onto all app resources
│   ├── tag-app-resources-aws.sh             # Same, via the Resource Groups Tagging API
│   ├── trigger-provision-azure.sh           # CLI wrapper around repository_dispatch
│   ├── trigger-provision-aws.sh             # Same, for the AWS workflow
│   └── watch-run.sh                         # Poll a remote workflow run + outputs (cloud-agnostic)

Control-plane verification is no longer defined here. It lives in each infra template at the canonical path scripts/verify.sh, so the orchestrator stays template-agnostic — verify-infrastructure.yml checks out the provisioned infra repo and runs that script.

Quick start

Setup is two guides: the GitHub part, identical for every cloud, then the part for the cloud you’re targeting. At a glance:

  1. Setup for GitHub — push this repo, create the dev / staging / prod Environments, add the GH_PAT secret for cross-repo work.
  2. Setup for your cloudAzure or AWS: create the platform identity, federate it to GitHub via OIDC so no secrets are stored, and grant it the least privilege needed to bootstrap state and plan changes.
  3. Trigger a run — via the self-service web UI, a CLI wrapper (scripts/trigger-provision-azure.sh / scripts/trigger-provision-aws.sh), or the GitHub Actions UI / API directly.

The state backend itself needs no manual setup: the platform bootstraps it on first run and every run thereafter, idempotently.

Triggering the platform

The provisioning page explains how a run works and hands off to a per-cloud form. Each form validates inputs in-browser, shows the equivalent curl command for review, and dispatches the workflow with a token the operator pastes. See Pages info for how the pages are hosted and what token scope the operator needs.

CLI

One wrapper per cloud, each building that cloud’s repository_dispatch payload. For Azure:

scripts/trigger-provision-azure.sh \
      --app-name               myapp \
      --environment            dev \
      --azure-tenant-id        $AZURE_TENANT_ID \
      --azure-subscription-id  $AZURE_SUBSCRIPTION_ID \
      --azure-client-id        $AZURE_CLIENT_ID \
      --infra-template-repo    your-org/template-terraform-azure-webapp \
      --app-template-repo      your-org/template-helloworld-express

And for AWS — --main-domain is required, since it drives the ACM certificate and Route 53 records:

scripts/trigger-provision-aws.sh \
      --app-name            myapp \
      --environment         dev \
      --aws-region          eu-west-1 \
      --aws-role-arn        arn:aws:iam::$AWS_ACCOUNT_ID:role/GitHubActionsRole \
      --main-domain         example.com \
      --infra-template-repo your-org/template-terraform-aws-fargate \
      --app-template-repo   your-org/template-helloworld-express

Flags fall back to upper-case env vars (ENVIRONMENT, APP_NAME, …) and the scripts auto-detect the platform repo from the current git remote. --help for the full reference.

GitHub UI

Actions → <cloud> - Provision & Reconcile Application Resources → Run workflow and fill the inputs. Every workflow name is prefixed with its cloud, so the Azure and AWS pipelines sit side by side in the Actions list.

Raw repository_dispatch

The event_type selects the cloud — for example, azure-provision-infrastructure in the case of Azure. The payload carries that cloud’s identity and region parameters:

curl -X POST \
      -H "Authorization: Bearer $GH_TOKEN" \
      -H "Accept: application/vnd.github+json" \
      https://api.github.com/repos/<org>/<repo>/dispatches \
      -d '{
            "event_type": "azure-provision-infrastructure",
            "client_payload": {
                  "app_name":               "myapp",
                  "environment":            "dev",
                  "azure_tenant_id":        "$AZURE_TENANT_ID",
                  "azure_subscription_id":  "$AZURE_SUBSCRIPTION_ID",
                  "azure_client_id":        "$AZURE_CLIENT_ID",
                  "azure_location":         "westeurope",
                  "infra_template_repo":    "your-org/template-terraform-azure-webapp",
                  "infra_template_ref":     "main",
                  "app_template_repo":      "your-org/template-helloworld-express",
                  "app_template_ref":       "main",
                  "container_image":        "appsvc/staticsite:latest",
                  "container_registry_url": "mcr.microsoft.com"
            }
      }'

And the AWS equivalent — same endpoint, different event_type and identity parameters (main_domain is required):

curl -X POST \
      -H "Authorization: Bearer $GH_TOKEN" \
      -H "Accept: application/vnd.github+json" \
      https://api.github.com/repos/<org>/<repo>/dispatches \
      -d '{
            "event_type": "aws-provision-infrastructure",
            "client_payload": {
                  "app_name":            "myapp",
                  "environment":         "dev",
                  "aws_role_arn":        "arn:aws:iam::$AWS_ACCOUNT_ID:role/GitHubActionsRole",
                  "aws_region":          "eu-west-1",
                  "main_domain":         "example.com",
                  "infra_template_repo": "your-org/template-terraform-aws-fargate",
                  "infra_template_ref":  "main",
                  "app_template_repo":   "your-org/template-helloworld-express",
                  "app_template_ref":    "main",
                  "container_image":     "<REFERENCE_IMAGE>"
            }
      }'

The token must have repo scope (classic) or Contents: write permission (fine-grained) on the platform repository. Actions: write is not enough for the repository_dispatch endpoint.

Compliance tagging

The tag-app-resources-<cloud>.yml workflows merge an arbitrary set of tags onto every resource that belongs to an app — tag-app-resources.yml for Azure, tag-app-resources-aws.yml for AWS. It is a day-2 operation — run it any time after resources exist to back-fill governance metadata, adopt a new tagging standard, or correct values without reprovisioning.

What it tags

The workflow discovers all resource groups for the app and tags each group and every resource inside it:

Resource group pattern Purpose
rg-<app_name>-dev / staging / prod Per-environment application resources
rg-<app_name>-tfstate Terraform state backend

Tags are merged — existing tags not present in tags_json are left unchanged. Re-running the workflow with updated values is safe.

On AWS, a resource group is a saved tag query rather than a container, so the workflow resolves each group’s query and tags the resources it matches. The group names and the merge semantics are the same.

Inputs

Three inputs are the same on every cloud; the rest are that cloud’s identity and region parameters:

Input Required Description
app_name Yes Application name — used to discover resource groups
tags_json Yes JSON object whose keys/values become the tags, e.g. {"airid":"309005","Application":"myapp","CreatedBy":"user"}
dry_run No (default: false) When true, discovers and lists all resources that would be tagged without writing anything
cloud credentials Yes Azure: azure_tenant_id, azure_subscription_id, azure_client_id. AWS: aws_region, aws_role_arn

How to run

Actions → <cloud> - Tag Application Resources → Run workflow, fill the form, and paste the JSON tags object. Use dry_run: true first to preview the full scope — the step summary shows the tag list and the run log shows every resource that would be tagged.

The workflow authenticates via OIDC using the same identity the provisioning workflow uses — no client secret or access key is required.

Running locally

The underlying script can also be run directly against an authenticated CLI session. For Azure:

scripts/tag-app-resources-azure.sh \
  --app-name               myapp \
  --azure-tenant-id        $AZURE_TENANT_ID \
  --azure-subscription-id  $AZURE_SUBSCRIPTION_ID \
  --azure-client-id        $AZURE_CLIENT_ID \
  --tags-json              '{"tag1":"value1","tag2":"value2","CreatedBy":"user"}' \
  --dry-run

For AWS, with credentials already configured:

scripts/tag-app-resources-aws.sh \
  --app-name   myapp \
  --aws-region eu-west-1 \
  --tags-json  '{"tag1":"value1","tag2":"value2","CreatedBy":"user"}' \
  --dry-run

Remove --dry-run to apply. Both require jq plus that cloud’s CLI.


Conventions

These hold across clouds; where a cloud forces an exception it is called out.

Local development

Terraform now lives in the generated {app-name}-infra repository (from infra_template_repo). To inspect it locally, clone that repo and run checks there. State writes should always go through CI.

git clone https://github.com/<org>/<app-name>-infra.git
cd <app-name>-infra/

The generated infra repository includes a detailed, step-by-step guide in its own documentation that explains how to configure the tools, run the Checkov checks, create and apply plans locally, validate changes, to be able to evolve the infrastructure when new requirements arise.

Roadmap

See the issue tracker for the current and planned features.

Contributing

See Contributing for development guidelines, the change workflow, and how to propose new modules or environment policies.

License

MIT — see the license file for details.