Imported from Gogorichielab/terraform-azurerm-scaffold (
AGENTS.md). Install upstream withnpx skills add Gogorichielab/terraform-azurerm-scaffold. Copyright stays with the author.
AGENTS.md
Instructions for AI coding agents working in this repository. Humans may find it useful too, but it is written for agents.
What this repo is
terraform-azurerm-scaffold stands up the Azure side of a live-coding project fast and cheap. It is optimised for starting projects, so defaults must stay on free or consumption tiers — an idle scaffold should cost nothing.
The repository root is the module. The .tf files at the root are what consumers get, which is what the terraform-<PROVIDER>-<NAME> repo name promises and what the Terraform Registry requires. Do not move them into a subdirectory.
*.tf the module itself, consumed by other repos
examples/ runnable root modules, one per deployment combination
docs/ reference documentation
.github/workflows/ CI
Cost and SKU decisions
Consult docs/free-azure-services.md before choosing or changing any SKU, tier, or size. It records what each Azure service gives away for free.
Rules:
- New resources default to their free tier when one exists. If none exists, default to the cheapest consumption option and say so in the README's cost notes.
- Never raise a default SKU for performance without being asked. Add an input instead and leave the default alone.
- Check the Last verified date at the top of that document. If it is more than a quarter old, treat its figures as unconfirmed and verify against the Azure free services page before relying on them.
- When you learn a free-tier allowance has changed, update that document and its Revision history table, per its own Keeping this current section.
- Free-tier figures describe the always-free tier on pay-as-you-go. The 12-month introductory offers are deliberately out of scope — do not mix them in.
Terraform conventions
Follow the Standard Module Structure and HashiCorp's module creation pattern.
Structure
main.tf,variables.tf,outputs.tf, andversions.tfalways exist, even when thin. Split resource creation into topic files (function_app.tf,monitoring.tf) oncemain.tfstops being scannable.- Never put a
providerblock in a module. Modules declarerequired_providersonly; examples configure providers. - Every externally usable module needs a
README.md, or Terraform tooling treats it as internal.
Inputs
- Name variables in full.
enable_function_app, notfunc. - Every variable gets a
description, atype, and avalidationblock wherever Azure constrains the value. - Required inputs have no default. Optional inputs have a default that is correct for most callers.
- Write error messages that say what to do, not just what is wrong.
Resources
- Conditional resources use
count = var.enable_x ? 1 : 0on the whole resource. Do not scatter conditional expressions through resource arguments. - Outputs for conditional resources use
one(resource.name[*].attr)so they returnnullrather than erroring when the toggle is off. - Mark every secret-bearing output
sensitive = true. - Apply the merged
local.tagsto every taggable resource.
Lock files
Provider lock files belong to root modules. The root .terraform.lock.hcl is gitignored, because this repository is a consumed module and pinning providers here misleads — the consuming configuration's lock file is the one that applies.
Verifying changes
Run these before committing any .tf change. CI runs the same checks.
terraform fmt -check -recursive
terraform init -backend=false && terraform validate
To validate an example, rewrite its module source to the local checkout first — examples point at the public git address on purpose, so validating them as-is would test the published tag instead of your working copy. .github/workflows/terraform.yml shows the exact sed.
terraform validate is not sufficient. Validation rules that reference other variables are evaluated at plan time, not by validate. A green validate proves syntax and provider schema only — it does not prove that a combination of inputs is accepted. To exercise those rules without a subscription, run terraform plan with dummy ARM_* credentials: variable validation runs before provider authentication, so the validation error surfaces ahead of the auth failure.
Test validations negatively. A validation nobody has seen reject anything is not known to work.
terraform plan/apply against a real subscription needs credentials that CI does not have. Never claim a module is deploy-verified on the strength of validate alone — say plainly what was and was not run.
Commits
Commit messages follow Conventional Commits 1.0.0:
<type>[optional scope][!]: <description>
[optional body]
[optional footer(s)]
Types used in this repo:
| Type | Use for |
|---|---|
feat |
A new input, output, or resource the module creates |
fix |
A defect corrected — behaviour moving toward what was already documented |
docs |
README, AGENTS.md, CHANGELOG, the cheat sheet, code comments |
refactor |
Restructuring that leaves behaviour and the public interface unchanged |
test |
Validation tests and test scaffolding |
build |
Provider and Terraform version constraints — anything a consumer must satisfy |
ci |
Workflows, Dependabot config, and anything else that only runs in CI |
chore |
Repository housekeeping with no effect on the module |
Rules:
- Description in the imperative mood, lower case, no trailing period.
add failure anomalies toggle, notAdded failure anomalies toggle. - Scope is optional and names the area touched:
feat(monitoring):,ci(deps):. - A breaking change takes a
!before the colon and aBREAKING CHANGE:footer saying what consumers must do about it. - Use the body to explain why. The diff already shows what changed.
- When a change cannot work as specified, say so in the body rather than quietly building something else — same rule as Working agreements.
How commit types map to the version bump
| Commit | Release |
|---|---|
any ! or BREAKING CHANGE: footer |
MAJOR |
feat |
MINOR |
fix, docs, refactor, test, build, ci, chore |
PATCH |
This is the same interface rule as Versioning and releases, applied per commit. Where the two seem to disagree, the interface rule wins — the type is a label, the interface is the contract.
The trap to watch for: a provider floor bump arrives from Dependabot as build(deps): bump ..., which reads as PATCH from the table above. It is not. Raising a floor changes what consumers must satisfy and is MAJOR. Add the ! and a BREAKING CHANGE: footer when you merge one, and treat it as the release decision it is.
Commits made before this convention was adopted do not follow it. Do not rewrite them.
Versioning and releases
This repo follows Semantic Versioning 2.0.0. Releases are git tags of the form vMAJOR.MINOR.PATCH, and consumers pin them:
source = "git::https://github.com/Gogorichielab/terraform-azurerm-scaffold.git?ref=v0.1.0"
The module's public interface is its input variables, its outputs, and the infrastructure a given set of inputs produces. Version against that interface, not against the size of the diff.
| Bump | When |
|---|---|
| MAJOR | Removing or renaming an input or output. Adding a required input. Changing a default such that an unchanged config deploys different infrastructure. Any change that forces replacement of an existing resource. Raising the Terraform or provider floor. |
| MINOR | Adding an optional input whose default preserves current behaviour. Adding an output. Adding a resource behind a toggle that defaults off. |
| PATCH | Bug fixes that move behaviour toward what was documented. Documentation, comments, and error-message wording that does not change what is accepted or rejected. |
While the module is 0.x, MINOR carries breaking changes and MAJOR stays at 0 — a 0.x tag is not a stability promise. Once the input surface settles, tag v1.0.0 and the table above applies strictly.
Every release gets a CHANGELOG.md entry under its version heading, in Keep a Changelog format. Add entries under Unreleased as you work; do not wait for release time.
Tags cover the whole repository, docs included, so a docs-only release still moves the tag. That is expected: ?ref= guarantees the module's behaviour is fixed, not that every tag changed it.
Dependency updates
Dependabot runs weekly, configured in .github/dependabot.yml.
Updates are grouped so a batch arrives as one pull request rather than one per dependency. GitHub Actions bumps share a single group. Terraform providers use group-by: dependency-name, which collapses the directory dimension — a provider bump lands as one pull request updating the root module and both examples together, so the examples can never end up pinned to a different floor than the module.
Only GitHub Actions minor and patch bumps auto-merge. They are CI-only and invisible to consumers.
Terraform provider bumps never auto-merge, at any level. Raising a provider floor changes what consumers must satisfy, which is a MAJOR release under Versioning and releases. Merging one is a release decision: bump the major version and say so in CHANGELOG.md. Do not relax this rule to reduce review load.
Working agreements
- Match the surrounding style. Comments in this repo explain why, not what — a comment restating the resource type is noise.
- Write commit messages per Commits.
- Update
CHANGELOG.mdin the same commit as the change it describes. - When a spec you were handed cannot work, say so and explain why before building an alternative. Record the deviation in
CHANGELOG.mdand the PR body. Do not silently "fix" a spec. - Report honestly what you verified. If a check was skipped or failed, say which and why.
Known constraints
Things that have already cost time here. Check these before debugging from scratch.
FC1is the only supported plan SKU. The module deploysazurerm_function_app_flex_consumption, which runs on Flex Consumption and nothing else.azurerm_linux_function_appcannot emit thefunctionAppConfigsection an FC1 plan requires — the pairing fails at apply. Other plan types need a different function app resource.- Flex Consumption is Linux only.
- Application Insights must be workspace-based. Classic was retired 29 February 2024. Omitting
workspace_idlets Azure provision a workspace out of band, which then shows up as a permanent plan diff. - The Failure Anomalies rule requires an action group.
smartDetectorAlertRulesrejects an emptyactionGroups.groupIds. - Storage account names are globally unique, 3–24 characters, lowercase alphanumerics only. That is why the module appends a
random_stringsuffix rather than deriving names fromworkloadandenvironmentalone. - Region availability is narrow. The Static Web App Free tier and the Flex Consumption plan are each offered in a subset of regions, and
locationmust satisfy both when the function app is enabled. - The module assigns the managed identity no roles. It cannot know what your functions need. Callers grant roles using the
managed_identity_principal_idoutput.