Skip to content

Commit 010f688

Browse files
MariusStorhaugCopilotmarst
authored
📖 [Docs]: Deployment capability documented, and Capability/Framework unified into one model (#103)
The docs now describe the deployment capability — how a change to managed resources is approved together with its effect and deployed exactly as approved — and they settle a long-standing split: `Capability` and `Framework` are one concept, documented the same way. ## New: Deployment capability A new capability under Capabilities covers deployment: a change's effect on every environment in the promotion path is computed and shown, the team approves the code change together with that effect as one decision, and the approved effect is exactly what deploys — failing closed on stale plans, with every action recorded. It ships a technology-agnostic spec plus a first design, **Deploying Azure from GitHub** (GitHub Actions and Terraform with passwordless OIDC identity). Each design is named "deploying a service provider from a CI/CD platform" so its two axes are clear, and the per-design pages live in a `designs/` subfolder so more combinations can hang off the same spec. ## Changed: One model for everything the ecosystem builds A capability and a framework only differed by atomic building block versus a composition of building blocks — a property of the thing, not a reason for two documentation models. Everything is now documented one way: a spec (why and what) plus a design (how). `Framework` becomes an adjective for a capability whose design composes other capabilities, and the Frameworks section is being absorbed into Capabilities over time. The Documentation Model and the Capabilities and Frameworks index pages are updated to reflect this. --- <details> <summary>Technical details</summary> - Deployment docs moved from `MSXOrg/Platform@terraform-deployment-research` into `src/docs/Capabilities/deployment/` (`index.md`, `spec.md`, `designs/azure-from-github.md`), following the `release-management` house style. Cross-repo absolute `msxorg.github.io` links were converted to relative links. - Designs are framed as "deploying a service provider from a CI/CD platform" (service provider = where resources live: Azure, AWS, GitHub; CI/CD platform = what runs the deployment: GitHub, Azure DevOps); the first design is `azure-from-github.md`. - `deployment/index.md` keeps hand-written Spec/Designs tables (no `INDEX:START/END` markers): the auto-generator only lists direct files and subfolders that carry their own `index.md`, so a generated table would drop the `designs/` design. - Registered the pages in `src/zensical.toml` nav under Capabilities. - Updated `Ways-of-Working/Documentation-Model.md`, `Capabilities/index.md`, and `Frameworks/index.md` for the taxonomy unification. - Disabled markdownlint `MD051`: it cannot resolve python-markdown `attr_list` heading anchors (`{ #fr1 }`) — the ecosystem's documented anchor standard — so it false-positives on valid same-page requirement links; `Test-DocumentationLink.ps1` remains the authoritative fragment validator. - Ran `Update-DocumentationIndex.ps1` and `Test-DocumentationLink.ps1` before marking ready. - Follow-on captured in #102: migrate Process-PSModule and Agentic-Development into Capabilities with their own spec+design (Process-PSModule's living in `MSXOrg/docs`). </details> <details> <summary>Relevant issues (or links)</summary> - Closes #104 - #102 </details> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: marst <marst@users.noreply.github.com>
1 parent c19a1dc commit 010f688

7 files changed

Lines changed: 507 additions & 0 deletions

File tree

‎.github/linters/.markdown-lint.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ MD029: false # Ordered list item prefix
2121
MD033: false # Allow inline HTML
2222
MD036: false # Emphasis used instead of a heading
2323
MD041: false # First line in file should be a top level heading, PULL_REQUEST_TEMPLATE.md is an exception
24+
MD051: false # Link fragments: docs use python-markdown attr_list anchors ({ #fr1 }) that markdownlint can't resolve; Test-DocumentationLink.ps1 validates fragments instead
2425
MD060: false # Table column style — content mixes compact and aligned tables
2526

2627
#################
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
---
2+
title: Deploying Azure from GitHub
3+
description: How the deployment spec is delivered with Azure as the service provider and GitHub as the CI/CD platform, using Terraform to compute and apply the approved effect.
4+
---
5+
6+
# Deploying Azure from GitHub
7+
8+
This design delivers the [Deployment spec](../spec.md) for one combination:
9+
**Azure** (including Microsoft Entra ID) as the service provider and **GitHub**
10+
as the CI/CD platform, with **Terraform (open source)** as the change
11+
engine. The computed effect the spec speaks of is a **saved Terraform plan**;
12+
the record of state is a remote Terraform state per environment.
13+
14+
Another design covers another combination — deploying Azure from Azure DevOps,
15+
deploying AWS from GitHub, deploying GitHub from GitHub — without changing the
16+
spec.
17+
18+
## Approach
19+
20+
A change is proposed as a pull request. On the pull request the process computes
21+
the effect for **every environment in the promotion path** and shows them all;
22+
approving and merging the pull request is the single approval of the code change
23+
together with all of those effects ([FR2](../spec.md#fr2), [FR3](../spec.md#fr3)).
24+
The merge then runs the promotion, deploying to each environment in order using
25+
the **exact plans that were approved** ([FR4](../spec.md#fr4)), running tests
26+
between environments ([FR7](../spec.md#fr7)) and recording every action
27+
([FR9](../spec.md#fr9)).
28+
29+
```mermaid
30+
flowchart TB
31+
subgraph Review [Pull request — one approval]
32+
C[Change as code] --> P[Plan per environment\ndev · test · preprod · prod]
33+
P --> R[All plans + tests shown in the PR]
34+
end
35+
R -->|team approves code + all effects, merges| P0
36+
subgraph Promote [Merge — promotion using approved plans]
37+
P0[Apply approved dev plan] --> T0[Automated tests] --> P1[Apply approved test plan]
38+
P1 --> T1[Automated tests] --> P2[Apply approved preprod plan]
39+
P2 --> T2[Automated tests] --> P3[Apply approved prod plan]
40+
end
41+
P3 --> Rec[Every action recorded]
42+
P0 -.effect no longer valid.-> Stop[Stop · recompute · re-approve]
43+
```
44+
45+
### Why this shape
46+
47+
- **One approval for the whole path.** Because the reviewer sees the effect on
48+
every environment, a single approval is sufficient; the design adds no
49+
per-environment reviewer gate, matching the spec's non-goal.
50+
- **Approved plans, not re-derived ones.** The saved plans reviewed on the pull
51+
request are the artifacts the promotion applies. This trades the convenience of
52+
re-planning at deploy time for the guarantee the spec requires — the deployed
53+
effect equals the approved effect.
54+
55+
### Alternatives considered
56+
57+
- **Re-plan at deploy.** Rejected: the deploy would compute its own effect, which
58+
can differ from the approved one — it fails [FR4](../spec.md#fr4).
59+
- **Approve only the source diff, plan at deploy.** Rejected: the reviewer never
60+
sees the effect, failing [FR2](../spec.md#fr2)–[FR3](../spec.md#fr3).
61+
- **A required-reviewer gate on each environment.** Rejected as redundant: the
62+
effects for all environments are already approved together; extra gates add
63+
friction without adding a decision.
64+
65+
## Components
66+
67+
| Component | Role |
68+
| --- | --- |
69+
| **Reusable workflow** (`.github/workflows/terraform.yml`, `workflow_call`) | The provider-agnostic core: init, format, validate, plan, render, policy scan, test, apply. |
70+
| **Plan (pull-request) workflow** | For every environment in the path, computes `plan -out`, renders it into the pull request, scans it, and uploads it as the approved artifact. |
71+
| **Promotion (merge) workflow** | Walks the environments in order, applying each approved plan and running tests between them. |
72+
| **Azure identity step** | `azure/login` federated sign-in; sets `ARM_USE_OIDC=true` so the `azurerm` and `azuread` providers use the same token. |
73+
| **State backend** | Azure Storage per environment, with blob-lease locking and encryption at rest — the record of state. |
74+
| **Environments** | GitHub Environments per environment name — the per-environment **identity boundary** and deployment branch policy (not an approval gate). |
75+
| **Break-glass workflow** (`workflow_dispatch`) | The emergency path. |
76+
| **Drift job** (`schedule`) | Periodic `plan -detailed-exitcode` that opens an issue on drift. |
77+
78+
## The effect is a saved plan
79+
80+
Terraform's saved plan is what makes "approve the effect, then apply exactly it"
81+
concrete:
82+
83+
1. `terraform plan -out=tfplan.<env>` computes the effect against that
84+
environment's state and writes it to a file that pins provider versions and
85+
the state's **serial** and **lineage**.
86+
2. `terraform show` renders it for humans (pull-request comment) and as JSON for
87+
policy scanning; `sha256(tfplan.<env>)` is recorded.
88+
3. `terraform apply tfplan.<env>` carries out **exactly** that effect, and
89+
refuses with `Saved plan is stale` if the state advanced — the native backstop
90+
for [FR5](../spec.md#fr5).
91+
92+
## Review and approval
93+
94+
The pull-request workflow computes a plan for **each** environment in the
95+
promotion path and makes all of them visible in the pull request, alongside
96+
format, validation, policy, and test results. These are **required checks**, so a
97+
change cannot merge unless every environment's effect computed successfully. A
98+
branch ruleset on `main` requires review (CODEOWNERS for `**/*.tf`), dismisses
99+
stale approvals on new commits, and requires branches to be up to date — so the
100+
shown effects reflect current `main`. Approving and merging is the single
101+
approval the spec requires ([FR3](../spec.md#fr3)); the approved plan artifacts
102+
are retained for the promotion.
103+
104+
## Promotion using the approved plans
105+
106+
On merge, the promotion workflow retrieves the plan artifacts approved on the
107+
pull request (keyed to the merge commit) and, for each environment in order:
108+
109+
1. **Verifies** `sha256(tfplan.<env>)` against the value recorded at plan time,
110+
defeating artifact swap.
111+
2. **Applies** `terraform apply tfplan.<env>` — exactly the approved effect
112+
([FR4](../spec.md#fr4)). If the plan is stale, the apply fails closed and the
113+
promotion stops; a fresh plan is produced and re-approved
114+
([FR5](../spec.md#fr5)).
115+
3. **Tests** — runs the environment's automated tests; a failing required test
116+
stops the promotion before the next environment ([FR7](../spec.md#fr7)).
117+
118+
Deployments against the same state are **serialised** by a concurrency group
119+
keyed per state with `cancel-in-progress: false`, backed by the state lock, and
120+
an in-flight apply is never cancelled ([FR6](../spec.md#fr6)).
121+
122+
## Identity and access (OIDC)
123+
124+
- The workflow requests the GitHub OIDC token with `permissions: id-token: write`;
125+
`azure/login` federates on it using the non-secret `client-id`, `tenant-id`,
126+
and `subscription-id` — no client secret exists ([NFR1](../spec.md#nfr1)).
127+
- Azure federated credentials pin the subject claim
128+
`repo:MSXOrg/Platform:environment:<env>`, binding identity to the environment
129+
([NFR3](../spec.md#nfr3)).
130+
- Two identities per environment: **read-only** for computing the effect and
131+
**write** for applying it, the write identity reachable only from that
132+
environment ([NFR2](../spec.md#nfr2)).
133+
- Entra ID resources use the `azuread` provider on the same federated token —
134+
Entra rides the Azure identity context, no separate secret. Managing GitHub
135+
itself uses the `github` provider with a GitHub App installation token, never a
136+
personal access token.
137+
138+
## Break-glass path
139+
140+
The emergency path ([FR8](../spec.md#fr8)) is a `workflow_dispatch` "emergency
141+
deploy" with required inputs `reason`, `incident_id`, target environment, and a
142+
typed confirmation, restricted to authorised operators. It still runs
143+
`plan -out` then `apply tfplan`, so the effect is shown before it deploys; it
144+
records the inputs and logs as an immutable run record, and opens a follow-up
145+
issue to bring the change back through the standard pull-request path. There is
146+
no flag on the standard workflow that skips review.
147+
148+
## Records and drift
149+
150+
Every run's rendered plans, apply logs, and test results are retained as run
151+
artifacts and logs, and each apply is recorded in the environment's deployment
152+
history — the audit trail for [FR9](../spec.md#fr9)/[NFR4](../spec.md#nfr4). A
153+
scheduled `plan -detailed-exitcode` job detects drift and opens an issue
154+
([FR10](../spec.md#fr10)).
155+
156+
## Interface
157+
158+
```yaml
159+
# .github/workflows/terraform.yml (reusable — workflow_call)
160+
on:
161+
workflow_call:
162+
inputs:
163+
environment: { type: string, required: true } # dev | test | preprod | prod
164+
working-directory: { type: string, required: true }
165+
action: { type: string, default: plan } # plan | apply
166+
terraform-version: { type: string, default: "1.9.x" }
167+
```
168+
169+
The core is provider-agnostic; the Azure sign-in is the single service-provider
170+
step. A different service provider (AWS) or CI/CD platform (Azure DevOps)
171+
reuses this design's shape with its own identity step and is documented as its
172+
own design under [Deployment](../index.md).
173+
174+
## Security boundaries and threats
175+
176+
- **Credential exposure** — OIDC only; no static cloud secret exists, and the
177+
default token is read-only.
178+
- **Plan artifact confidentiality** — a saved plan can contain sensitive values
179+
in plaintext. Retention is short, artifacts are restricted, sensitive variables
180+
and outputs are marked, and pull-request comments carry scrubbed renders; on a
181+
public repository plan artifacts are never exposed unscrubbed.
182+
- **Artifact swap** — each apply verifies `sha256(tfplan.<env>)` before applying.
183+
- **Cross-environment escalation** — federated subjects are pinned per
184+
environment; the write identity is reachable only from its environment.
185+
- **Stale apply** — Terraform's serial/lineage guard, plus serialisation and
186+
branch-freshness rules.
187+
188+
## Testing and operability
189+
190+
- **Dry run** the full path on `dev`: pull-request plans for every environment,
191+
review, merge, promotion with tests.
192+
- **Exercise the stale path** by mutating state between approval and deploy and
193+
confirming the apply fails closed.
194+
- **Exercise break-glass** end-to-end and confirm the reconciliation follow-up is
195+
opened and attribution is recorded.
196+
197+
## Repository hardening
198+
199+
Third-party actions are pinned to commit SHAs; allowed actions are restricted;
200+
secret scanning with push protection is enabled as defence in depth even under an
201+
OIDC model.
202+
203+
## Decisions
204+
205+
One-way-door decisions are recorded here beside the spec, per
206+
[Decision before change](../../../Ways-of-Working/Principles/AI-First-Development.md#decision-before-change).
207+
208+
- **Approve every environment's effect on the pull request; apply those exact
209+
saved plans on merge.** Chosen over re-plan-at-deploy (does not apply the
210+
approved effect) and per-environment gates (redundant given the effects are
211+
already approved together). Revisit only if a saved plan can no longer be the
212+
review-to-deploy contract.
213+
214+
## References
215+
216+
- GitHub — security hardening with OpenID Connect: <https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/about-security-hardening-with-openid-connect>
217+
- GitHub — configuring OIDC in Azure: <https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-azure>
218+
- GitHub — managing environments: <https://docs.github.com/en/actions/how-tos/deploy/configure-and-manage-deployments/manage-environments>
219+
- GitHub — reusing workflows: <https://docs.github.com/en/actions/using-workflows/reusing-workflows>
220+
- GitHub — control concurrency: <https://docs.github.com/en/actions/using-jobs/using-concurrency>
221+
- HashiCorp — automate Terraform with GitHub Actions: <https://developer.hashicorp.com/terraform/tutorials/automation/github-actions>
222+
- HashiCorp — `terraform plan` / `terraform apply` (saved plan): <https://developer.hashicorp.com/terraform/cli/commands/plan> · <https://developer.hashicorp.com/terraform/cli/commands/apply>
223+
- Terraform AzureRM provider — OIDC: <https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_oidc>
224+
- Azure — `azure/login`: <https://github.com/Azure/login>
225+
- Microsoft — GitHub Actions + Terraform OIDC on Azure sample: <https://learn.microsoft.com/en-us/samples/azure-samples/github-terraform-oidc-ci-cd/github-terraform-oidc-ci-cd/>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
title: Deployment
3+
description: How a change to managed resources is approved together with its effect and deployed exactly as approved — one spec, and one design for each combination of deploying a service provider from a CI/CD platform.
4+
---
5+
6+
# Deployment
7+
8+
How a change to the resources Platform manages moves from a proposal to live: its
9+
**effect on the resources** is computed and shown, the team approves the code
10+
change together with that effect across every environment it will pass through,
11+
and the approved effect is exactly what is deployed — with every action recorded.
12+
13+
The **spec** is the durable contract and is deliberately free of any technology.
14+
Each **design** delivers that contract for one combination, named **"deploying
15+
&lt;service provider&gt; from &lt;CI/CD platform&gt;"** so the two parts are
16+
clear: the **service provider** is where the resources live (Azure, AWS,
17+
GitHub), and the **CI/CD platform** is what runs the deployment (GitHub, Azure
18+
DevOps) — for example deploying Azure from GitHub, deploying Azure from Azure
19+
DevOps, deploying AWS from GitHub, or deploying GitHub from GitHub. Adding a
20+
combination adds a design; it never changes the spec.
21+
22+
## Spec
23+
24+
| Page | Description |
25+
| --- | --- |
26+
| [Spec](spec.md) | The why and what — a change is approved together with its effect, and the approved effect is exactly what deploys. |
27+
28+
## Designs
29+
30+
| Design | Service provider | CI/CD platform | Description |
31+
| --- | --- | --- | --- |
32+
| [Deploying Azure from GitHub](designs/azure-from-github.md) | Azure | GitHub | GitHub Actions and Terraform deploy Azure and Entra resources with passwordless identity, approving the code change together with its per-environment effect. |

0 commit comments

Comments
 (0)