Azure Bicep and AWS CloudFormation in InfraOrcinus: What-If, Change Sets and One Approval Gate
By Chirag Panchal · Founder & Lead Architect · · 9 min read
Azure Bicep and AWS CloudFormation now run through the same validate, plan, approve and apply lifecycle as Terraform, OpenTofu and Pulumi. How Bicep what-if and CloudFormation change sets map onto one plan model, how tightly each binds the approval to the apply, and what is not supported yet.
Development Status & Architectural Evolution: InfraOrcinus is in active development. Architecture specifications, internal interfaces, and operational protocols described herein reflect our current engineering design and are continuously refined to make InfraOrcinus robust, resilient, and enterprise-grade.
In large organizations, nobody runs a single IaC tool.
- Platform teams standardize on Terraform or OpenTofu for shared, cross-cloud services.
- Azure-centric teams write Azure Bicep: first-party syntax, new Azure resource types on day one, and no state file to look after.
- AWS-native teams keep CloudFormation for its server-side stacks, managed rollback and integration with the rest of AWS.
- Application teams often prefer Pulumi in a general-purpose language.
When each tool has its own pipeline, governance splits with it. Reviews happen in four places, approval rules drift apart, and an auditor has to reconcile four different change histories.
An earlier version of this article described Bicep and CloudFormation support as a roadmap. Both engines now ship. This article explains how they fit into InfraOrcinus's lifecycle, where they behave differently from Terraform, and the limits that still apply.
1. One Lifecycle, Five Engines
Every environment in InfraOrcinus goes through the same stages, whatever its engine:
Validate → Plan → Policy & Cost → Named Approval → Apply
The engine decides which commands run at each stage. It does not decide whether a stage happens. A Bicep deployment cannot skip approval any more than a Terraform apply can.
Inside the control plane, each engine is one module behind a single interface. The module owns everything specific to that tool: its CLI and container image, the scripts for each stage, how its settings are validated, and how its plan output is read. The rest of the platform never branches on the engine name. Adding an engine means adding one entry to a registry.
The step that makes this possible is plan normalization. Each engine's plan output is converted into the same structure:
// Every engine's plan becomes this shape before anything else reads it.
type PlanSummary struct {
Engine string `json:"engine,omitempty"`
EngineVersion string `json:"engineVersion,omitempty"`
ResourceChanges []ResourceChange `json:"resourceChanges"`
// The original plan document, kept for Rego policies that need it.
Raw map[string]interface{} `json:"-"`
}
Each ResourceChange carries an address, a type, the actions (create, update, delete, or create and delete for a replacement), the before and after values where the engine reports them, and a replacement reason where it gives one. Policy evaluation, cost estimation, drift detection, the approval screen and the AI assistant all read this one structure.
2. Azure Bicep: What-If as the Plan
Bicep has no state file. Azure Resource Manager (ARM) records each deployment, and that history is the record. A Bicep environment therefore needs only one extra setting: the resource group to deploy into.
| Stage | What runs |
|---|---|
| Validate | az bicep build (compile and lint), then az deployment group validate (ARM's preflight check against the resource group) |
| Plan | az deployment group what-if --no-pretty-print. The JSON result is uploaded as the plan |
| Approve | A named person reviews the what-if, policy results and cost |
| Apply | az deployment group create in ARM's default incremental mode, then the deployment's outputs are recorded |
What-if change types map onto the shared model: Create becomes create, Modify becomes update, Delete becomes delete, and NoChange or Ignore become no change. Anything else, such as Deploy, is treated as an update. When the engine cannot tell what an entry means, it reports something changes here rather than nothing changes.
Destroy. ARM cannot preview a delete the way what-if previews an update. The destroy preview therefore lists every resource recorded under the environment's latest deployment and shows each as a delete. This is an inventory of what will be removed, not a diff. After approval, the resources are deleted one by one. The step continues past individual failures and then fails overall, naming any resource it could not remove. Resources created in the same group by other means are left alone.
3. AWS CloudFormation: The Change Set Is the Plan
CloudFormation fits InfraOrcinus's lifecycle more naturally than any other engine, because it already has a real two-phase workflow. A change set is created and reviewed, then that same change set is executed.
| Stage | What runs |
|---|---|
| Validate | aws cloudformation validate-template on template.yaml (or the first *.cfn file) |
| Plan | create-change-set (CREATE or UPDATE, chosen by whether the stack exists), wait, then describe-change-set is uploaded as the plan |
| Approve | A named person reviews the change set, policy results and cost |
| Apply | execute-change-set, wait for the stack to finish, then record its outputs |
A few details matter in practice:
- Replacements are shown as replacements. CloudFormation reports a forced replacement as a single
Modifyentry withReplacement: True. InfraOrcinus expands it into create plus delete and keeps AWS's reason, for example replacement required (scope: Properties). A replacement never reads as an in-place update. - An empty change set is a valid plan. AWS reports "didn't contain changes" as a failed wait. InfraOrcinus recognizes that specific reason and records an empty plan. Any other failure still fails the step.
- Capabilities are explicit. The default is
CAPABILITY_IAMandCAPABILITY_NAMED_IAM. Macro expansion (CAPABILITY_AUTO_EXPAND) is granted only if you list it. - AWS owns rollback. If an execution fails, CloudFormation rolls the stack back and InfraOrcinus reports AWS's own status. The environment's Console tab shows the stack events that explain why.
4. How Tightly the Approval Binds the Apply
An approval only means something if what is applied is what was reviewed. This is where the engines differ most, so we state it plainly:
| Engine | What the apply runs | Binding |
|---|---|---|
| Terraform / OpenTofu | The saved binary plan file from the plan step | Exact. A plan whose state has since changed is refused |
| CloudFormation | execute-change-set on the change set created at plan time |
Exact. The reviewed object is the one that executes |
| Azure Bicep | az deployment group create on a fresh clone of the blueprint |
By content. The template is evaluated again at apply time |
| Pulumi | pulumi up on a fresh clone of the blueprint |
By content. The program is evaluated again at apply time |
For Bicep and Pulumi, pin the blueprint to an immutable Git tag rather than a branch. Then the content cannot move between plan and apply. If variables or credentials change while a run waits for approval, reject it and plan again.
5. Required Settings
Each engine declares what an environment needs before it can run:
| Engine | Required | Optional |
|---|---|---|
| Azure Bicep | Resource group (it must already exist) | Approval role, project |
| AWS CloudFormation | Region | Stack name (derived from the environment name if blank), role ARN, capabilities |
The server will not send a run to a runner while a required setting is missing. It has nowhere to deploy, so the run waits instead of failing halfway through.
6. Limits Today
Being precise matters more than sounding complete. These are the current limits:
- Bicep parameters are not yet passed to the deployment. Values entered for a Bicep environment's variables are stored but not sent to Azure, so the template's parameter defaults apply. Give every parameter a sensible default until this ships. CloudFormation parameters are passed, through
params.json. - Cost estimation does not price Bicep or CloudFormation resources yet. Their plans list resources as "pricing not available for this resource type". Rules on plan-level cost values only see the priced part.
- Built-in attribute policies are Terraform-shaped. Bicep resources all have the type
bicep_resource, and a CloudFormation change set does not carry full property values. Rules that match on type tokens, and Rego policies written against the plan document, do apply. - Stuck CloudFormation stacks are not recovered automatically. A stack left in
ROLLBACK_COMPLETEorREVIEW_IN_PROGRESSby a failed create must be deleted before the next run. - Credentials are static. Bicep signs in with a service principal. CloudFormation uses access keys, or short-lived keys minted from Vault. Workload identity federation (OIDC) is not supported yet.
Summary
Bicep and CloudFormation now go through the same validate, plan, approve and apply lifecycle as Terraform, OpenTofu and Pulumi. Bicep's what-if and CloudFormation's change sets are converted into one plan model, so policy, cost, drift detection, the approver's screen and the AI assistant read one structure. CloudFormation and Terraform bind the approval to exactly what is applied; Bicep and Pulumi bind by content, so pin their blueprints to a Git tag. The gaps that remain, Bicep parameters and cost for the native-cloud engines first among them, are listed above rather than left for you to find.