Terraform Provider best practices
Organize, secure, version, and automate Azion Terraform Provider configurations so that state, tokens, and changes stay under control.
A Terraform configuration describes infrastructure that several people and pipelines change over time. When its files follow one layout, its state lives in one shared place, and its token stays out of the repository, every run starts from the same picture of the account. Without that, two applies can write the same state at once, a token leaks with a commit, and a provider release reaches a configuration that never asked for it.
These practices apply to configurations that use the Azion Terraform Provider v2.0, which works with Azion API v4 only.
In order, the practices cover the file layout, resource names, the remote backend and its lock, the sensitive token variable, the .gitignore file, and the token in the environment. They then cover modules, the CI/CD pipeline, the provider version, outputs, references between resources, comments, and module README files.
Split the configuration into files by role
Terraform reads every .tf file in a directory as one configuration, so splitting the files changes nothing at run time. It tells a reader where to look: the provider, the variables, the outputs, and the backend each get their own file. Reusable modules go under modules/, and each environment gets its own directory under environments/. The cost is more files to open on a small project.
The layout below separates the root configuration, two modules, and three environments:
To check it, confirm that each file holds only the blocks its name announces, such as the backend block in backend.tf.
Name each resource for what it serves
Terraform addresses a resource by its type and its local name, as in azion_workload.api_gateway, and terraform state show and every reference use that address. A local name such as w1 tells nobody which workload it is. A local name such as api_gateway, with a name argument such as api-gateway-prod, says what the workload serves and in which environment. The cost is a naming convention that the team agrees on and keeps.
The first block shows the name to avoid, and the second the name to use:
To check it, run terraform state list and confirm that every address names the resource it manages.
Store the state in a remote backend
Terraform records what it created in a state file. Kept on one machine as terraform.tfstate, the state is out of reach for every other person and pipeline that runs the configuration. Each of them then plans against a different picture of the account. A remote backend keeps one copy of the state that every run reads and writes. The cost is infrastructure outside the configuration: the bucket and the lock table that the block names must exist for Terraform to use them.
The backend.tf file below stores the state in an s3 backend, encrypted, with a lock table:
To check it, run terraform state list from a second machine or from the pipeline, and confirm that it lists the same resources.
Lock the state during every run
A lock lets one run at a time change the state, so two applies started together cannot overwrite each other’s record. With the s3 backend, the lock lives in a DynamoDB table. The line dynamodb_table = "terraform-locks" in the backend block turns it on, as the full block in Store the state in a remote backend shows. The cost is one more resource outside the configuration, which must exist before the backend can use it.
To check it, confirm that dynamodb_table is set in the backend block of every environment.
Mark the token variable as sensitive
The provider takes an Azion personal token in its api_token argument. Terraform does not show the value of a variable marked sensitive in its output, so the token stays off the screen and out of pipeline logs. The variable that feeds the argument is declared like this:
The provider "azion" block then passes it on with api_token = var.api_token. The cost is a limit: marking the variable does not keep the value out of a .tfvars file, so keep that file out of the repository, as Keep secrets and state out of the repository shows.
To check it, run terraform plan and confirm that the token value does not appear in the output.
Keep secrets and state out of the repository
A file committed to a repository reaches everyone who can read the repository. The .gitignore file below keeps out the state files and their backups, every .tfvars file, the .terraform/ directory, and a secrets.tf file:
The cost is that variable values travel outside the repository, so each person and pipeline supplies them, as Pass the token through an environment variable shows.
To check it, run git status after terraform apply and confirm that it lists no .tfstate or .tfvars file.
Pass the token through an environment variable
An environment variable keeps the token in the shell or the pipeline that runs Terraform, outside every file. Terraform reads TF_VAR_api_token as the value of the api_token variable:
This route needs the sensitive api_token variable and api_token = var.api_token in the provider "azion" block. The provider also reads AZION_API_TOKEN directly, with no variable and an empty provider block. The CI/CD pipeline in Plan on every pull request and apply only from main uses that route. The cost is that the variable lives only as long as the shell session or the pipeline job, so each one sets it again.
To check it, run terraform plan in a fresh shell with the variable set, and confirm that it does not fail with personal token is required. For that error, refer to Troubleshoot the Terraform Provider.
Package repeated resources as modules
A module turns a group of resources into one block that takes inputs. Each environment then calls the same code with its own values instead of copying it. The cost is one more layer to read, and a change to the module reaches every configuration that calls it.
The module below builds a workload named from var.name and var.environment, and exposes its ID as the workload_id output:
The root main.tf calls the module once per workload, with its own values:
To check it, run terraform init and then terraform plan, and confirm that the plan creates a workload named api-prod.
Plan on every pull request and apply only from main
A pipeline runs the same commands for every change, so the plan is reviewed before anything reaches the account. In the GitHub Actions workflow below, every push and pull request against main runs terraform init and terraform plan. Only a run on main applies, with -auto-approve, because no one is there to answer the confirmation prompt. The plan and apply steps read the token from the AZION_API_TOKEN repository secret. The cost is a secret to manage in the repository settings, and an apply that runs without a second review once a change merges.
The workflow file holds the whole pipeline:
To check it, open a pull request and confirm that the workflow runs the plan step and skips the apply step.
Pin the provider version
terraform init installs the provider version that the required_providers block allows. An exact version keeps every run, on every machine and pipeline, on the release the configuration was written for. Provider v2.0 renamed the v1.x resources when it moved to Azion API v4, and a pin keeps a change of that kind out of a configuration until you migrate it. For that change, refer to Migrate from provider v1.x to v2.0.
The required_providers block below pins the provider to one release:
The cost is that a later release arrives only when you edit the pin and run terraform init -upgrade.
To check it, run terraform init and confirm that it installs aziontech/azion at version 2.0.0.
Export the IDs other configurations need as outputs
An output publishes a value after terraform apply, so another configuration, a script, or a person reads a workload or application ID instead of looking it up. A description says what the value is to anyone who reads the outputs later. The cost is a stable contract: whatever reads an output breaks when you rename or remove it.
The outputs below export the IDs of the workload and the application:
Inside a module, the same blocks go in its outputs.tf. For example, modules/application/outputs.tf exports application_id from azion_application_main_setting.this.id and application_name from azion_application_main_setting.this.name.
To check it, confirm that each output carries a description and reads a resource attribute, never a literal ID.
Reference resources instead of hardcoding IDs
A reference such as azion_application_main_setting.example.id passes the ID that Terraform knows once it creates the application, and it makes Terraform create the application first. A hardcoded ID such as "12345" belongs to one account, breaks when the application is created again with another ID, and gives Terraform no order to follow. The cost is that the referenced resource must be in the same configuration, or be read through a data source.
The first block shows the hardcoded ID to avoid, and the second the reference to use:
Because a reference already sets the order, a depends_on list that names the same resource repeats it. A deployment with workload_id = azion_workload.main.id needs no depends_on = [azion_workload.main]. For how references order resources, refer to How the Terraform Provider works.
To check it, search the configuration for quoted IDs in _id arguments, and replace each one with a reference or a data source.
Comment why each resource exists
A comment records what a resource is for and who relies on it, which neither its type nor its arguments say. The cost is upkeep: a comment that no longer matches its resource misleads more than no comment.
The comment below says which API the workload serves and in which environment:
To check it, read each commented block after a change and confirm that the comment still matches what the block manages.
Document each module in a README file
A README.md file in each module shows how to call it, which inputs it takes, and which outputs it returns. The person who calls the module then needs no look at its code. The cost is manual upkeep: update the README in the same change as the module’s variables and outputs.
The README below documents a workload module with one input and one output:
To check it, compare the Inputs and Outputs tables of each README with the variable and output blocks of its module.