Terraform on Azure for a Small Team: What to Set Up First

Terraform on Azure for a small team: remote state, one folder per environment, naming rules, pinned versions, OIDC sign-in, Key Vault secrets and plan-in-review.

· 6 min

Most teams start with Terraform the same way. Someone writes a main.tf on their laptop, runs terraform apply, and an App Service, a database and a storage account appear in Azure. It works, and for a few weeks nobody thinks about it again.

Then a second environment is needed. A colleague wants to change a setting. The CI pipeline should deploy the infrastructure instead of a person. At that point the early shortcuts start to cost something: a state file that lives on one laptop, names that differ between staging and production, a client secret pasted into a pipeline variable.

Picture a team of three running a customer portal on Azure: a web app, a PostgreSQL database, a storage account for uploads and a Key Vault. None of them works on infrastructure full time. The eight decisions below are what we would settle for a team like this, ideally before the second environment exists.

1. Put the state in Azure Storage from day one

Terraform's state file is the record of what it created. If it sits on one laptop, nobody else can safely run Terraform, and losing that laptop means Terraform no longer knows what it manages.

Keep the state in an Azure Storage account using the azurerm backend. The backend locks the state with a blob lease while someone runs a plan or apply, so two people cannot change the same environment at the same time. Turn on blob versioning and soft delete on that storage account, so a damaged state can be rolled back.

Create this storage account once, by hand or with a small separate configuration, and keep it outside the infrastructure it describes. Give each environment its own state file.

2. One folder per environment, shared modules underneath

There are several ways to separate staging and production. For a small team, the easiest to read is one folder per environment that calls the same modules with different values.

infra/
  modules/
    web-app/
    database/
  envs/
    staging/
      main.tf
      terraform.tfvars
    production/
      main.tf
      terraform.tfvars

Simplified sketch.

Each environment folder has its own backend configuration and its own state. A change to a module reaches both environments, but you apply it to staging first and see the plan for production separately. Workspaces can do the same job, but the folders make it obvious which environment you are standing in.

3. Agree on a naming rule and keep it in one place

Azure names are awkward. Storage account names must be 3 to 24 characters, lowercase letters and numbers only, and unique across all of Azure. Other resources allow hyphens and longer names. If every person invents names as they go, the portal fills up with test2, portal-db-new and stgportalprod01.

Write the rule down once, for example project, environment, resource type and region, and build the names in a single locals block that every module uses. Then a resource's name tells you what it is and where it belongs without opening anything.

4. Pin Terraform and provider versions

The azurerm provider changes often, and major versions rename or remove arguments. If one person runs a newer provider than the pipeline, the same code can produce a different plan.

Set required_version for Terraform and a version constraint for each provider, and commit the .terraform.lock.hcl file to the repository. Upgrade on purpose, in a branch, and read the plan before merging.

5. Let the pipeline sign in without a stored secret

The pipeline that runs Terraform needs permission to change Azure. The usual shortcut is a service principal with a client secret saved as a pipeline variable. That secret expires, gets copied, and is hard to trace.

GitHub Actions and Azure DevOps can sign in to Azure with OpenID Connect instead: Azure trusts tokens issued for a specific repository and branch, and no long-lived secret is stored anywhere. Give that identity only the roles it needs on the resource groups it manages, not Owner on the whole subscription.

6. Keep secrets in Key Vault, and treat the state as sensitive

Database passwords and API keys do not belong in terraform.tfvars or in the repository. Generate them or store them in Key Vault, and let the application read them from there.

Remember that Terraform writes the values of the resources it manages into the state, including passwords, in plain text. That makes the state storage account sensitive: limit who can read it, and do not copy state files into chat messages or tickets.

7. Plan in the pull request, apply after merge

The safest routine is also the simplest: every change to infra/ goes through a pull request, the pipeline runs terraform plan and posts the result, a second person reads it, and the pipeline runs apply after the merge. Nobody applies from a laptop.

Sooner or later someone will change a setting in the Azure portal during an incident. That is fine as long as it comes back into code. A scheduled terraform plan that runs every night and reports any difference shows this drift before it surprises the next person who applies.

8. Protect the things that are expensive to lose

Terraform will destroy a resource if the code says so, and a renamed resource can look like "delete and create" in a plan. For the database, the storage account with customer uploads and the Key Vault, add lifecycle { prevent_destroy = true } so Terraform refuses to remove them. On the production resource group, an Azure CanNotDelete lock adds a second guard that also covers changes made outside Terraform.

These two settings take minutes and make the worst mistake much harder to make.

In short

Before a small team's Terraform grows past one environment, settle eight things: remote state in Azure Storage with locking and versioning, one folder per environment over shared modules, a written naming rule in one place, pinned versions with the lock file committed, OIDC sign-in for the pipeline, secrets in Key Vault with the state treated as sensitive, plan in review and apply after merge, and protection for the resources you cannot afford to lose. None of these is difficult on day one; all of them are harder to add after the second environment.

In our cloud and DevOps work we define servers, networking and databases in Terraform so that each environment can be rebuilt from scratch and every change is recorded and reviewable.

Planning a project like this?

Plan it in 10 steps