In this post, we will explain what the terraform init
command is used for, what it does, and when to run it. We will explore the options available and give an example of when to use it in automation using Azure DevOps.
What is Terraform init?
After writing your Terraform code or cloning your existing Terraform code from source control, the terraform init
command is the first step you should take to prepare your working directory.
Terraform init
is a CLI command that initializes the Terraform environment by installing the necessary provider plugins and modules and setting up the configuration according to your code. Terraform init
enables you to run further commands like terraform plan
and terraform apply
.
How does Terraform init work?
In order to prepare the working directory for use with Terraform, the terraform init
command performs the following steps:
Backend initialization
Child module installation
Plugin installation
We will look at each of these in more detail in this article.
Quick usage examples: Terraform init command flags
If you are looking for some quick examples on how to use the terraform init
command, some of the more common usage flags are listed below. Later in the article, we will deep dive into some of these and provide examples.
Quick usage examples
terraform init
- Initialize the working directory, install required provider plugins and modules, and set up the backend.
terraform init -lock=false
- Initialize the working directory; don't hold a state lock during backend migration.
terraform init -input=false
- Initialize the working directory and disable interactive prompts.
terraform init -migrate-state
- Reconfigure a backend and attempt to migrate any existing Terraform state.
terraform init -upgrade
- Ensure you're using the latest compatible versions of your providers
terraform init -reconfigure
- Use the -reconfigure flag to force Terraform to forget any previous configuration and reinitialize.
terraform init -get=false
- Disable downloading modules for this configuration
terraform init -plugin-dir=/path/to/custom/plugins
- Point Terraform to custom or manually downloaded provider plugins
terraform init -input
- Provide values for required input variables during initialization
How to initialize a Terraform file - example configuration files
The example files we will use in this article will create a specified number of groups in Azure AD.
1. Set main.tf file
These files are contained in a directory called az-ad-group. Within it, I have my Terraform configuration files, named main.tf
, variables.tf
and terraform.tfvars
, as well as a .gitignore
file, which will specify which file extensions within the folder the git source control should ignore.
I have a subfolder (or module) within this which holds a main.tf
and variables.tf
file.
Lastly, the azure-pipeline.yml
file specifies the pipeline settings to run terraform init
and terraform plan
.
provider "azurerm" {
features {}
}
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">=2.95.0"
}
azuread = {
source = "hashicorp/azuread"
}
}
backend "azurerm" {
resource_group_name = "tf-rg"
storage_account_name = "jacktfstatesa"
container_name = "terraform"
key = "adgroups.tfstate"
}
}
module "ad_group" {
source = "./ad_group"
ad_group_names = var.ad_group_names
}
Note that the main.tf file contains the required_providers
and backend
blocks. I also call a module called ad_group
.
Since this article focuses on the terraform init
command, and everything relevant to that command is shown in the above code, I will not publish the rest of the files here, but they can be found in the GitHub repository should you wish to try the example out yourself or delve deeper into the setup.
2. Initialize backend
My main.tf
file is configured to use a storage account I have set up in Azure to store the Terraform state file. This is known as the 'backend'.
When terraform init
runs, it will first attempt to initialize the backend.
backend "azurerm" {
resource_group_name = "tf-rg"
storage_account_name = "jacktfstatesa"
container_name = "terraform"
key = "adgroups.tfstate"
}
Before that can happen successfully I will need to login to Azure using the Azure CLI. If I run it without first authenticating, Terraform complains that the resource group containing the storage account cannot be found.
To login to Azure, use the following commands. The --tenant
flag does not need to be specified if you have only one Azure AD tenant linked to your login. If you have multiple tenants linked to your login, you should specify this to avoid confusion.
az login --tenant <tenant ID>
az account set --subscription <subscription ID>
Once authenticated successfully, run the terraform init
command again:
This time we see the backend was initialized successfully.
In the storage account in Azure, in my terraform container, I can see that the adgroups.tfstate
file has been created successfully.
Any changes to the backend configuration will be detected by Terraform. When terraform init
is run, Terraform will throw an error alerting you to the change.
In this scenario, there are two options:
terraform init -migrate-state
-- To reconfigure the backend and attempt to migrate any existing state.terraform init -reconfigure
-- To reconfigure a backend while ignoring any saved configuration.
💡 You might also like:
3. Initialize the Terraform child module
A Terraform module is a container for multiple resources that are used together.
In my example code, my main.tf
calls my child module ad_group
.
module "ad_group" {
source = "./ad_group"
ad_group_names = var.ad_group_names
}
When terraform init
is run we can see it being installed:
If you have any module
blocks defined in your configuration files, these will be installed when executing the init command. The location of the source code for the referenced modules is defined in the source
argument of the module block. All modules require a source
argument.
The source
of a module block can point to a local module, or a remote source such as a module held in a central repository, GitHub, Bitbucket, or a module in the public terraform registry. If the code is remote, it will be downloaded from the source specified to a local directory so it can be used by other Terraform commands.
After adding, removing, or modifying module blocks, terraform init
must be run again in order for Terraform to make the necessary adjustments that have been defined in the configuration.
Re-running the terraform init
command will not amend any modules that have already been installed if the configuration of those modules has not been changed. It will, however, install any modules added since the last time terraform init
was executed. To force all modules to be re-installed, the -upgrade
flag can be specified.
Child module installation can also be skipped by using the -get=false
flag, this should only be used if the child modules have already been installed and no additions, deletions, or changes to the modules have been made. Note that Terraform builds an internal 'module tree' when terraform init
is run, and if this is not complete, then other steps performed by terraform init
will not complete successfully. It is, for this reason, the -get=false
flag should be used with caution.
4. Initialize the Terraform plugin
Most Terraform providers are published separately from Terraform as plugins. There are hundreds of available providers which allow Terraform to be used with different services. Common providers include azurerm
for Microsoft Azure, azuread
for Azure Active Directory, and aws
for Amazon Web Services. A full list of available providers can be browsed using the Terraform registry.
During terraform init
the registry is searched for those providers that are specified directly in the configuration files but also searches for providers that are not specified directly (indirect). They will be automatically found, downloaded, and installed if they are present in the registry.
As well as searching the public Terraform registry for providers, it is also possible to use a third-party registry, by specifying the -plugin-dir=PATH
flag.
If you do not wish to use these options, a provider_installation
block can be configured in your code to override Terraforms default provider installation behavior, allowing the use of a local mirror for some or all of your required providers.
When I run terraform init
with my example code, I see the following output:
Terraform searches the registry for the required azuread
and azurerm
providers and installs the appropriate versions as specified in my configuration files.
If I do not specify the version for a particular provider, Terraform will look for the latest version. It is recommended to always *pin *the provider version as upgrades may cause unexpected behavior over time. For example, the azuread
v2.18.0 provider will have introduced many changes compared to v2.17.0.
Refer to the main.tf
file at the beginning of the article for an example of this at line 9, which pins the version of the azurerm
provider.
5. Create dependency lock file - terraform.lock.hcl
Once providers have been successfully installed, Terraform creates a dependency lock file and writes information about the providers to it. This file will be named .terraform.lock.hcl
and should ideally be committed to your repository in your version control system to ensure that the same provider versions are used when terraform init
is run again in the future.
Using my example configuration code, once terraform init
has been run, Terraform will advise that the .terraform.lock.hcl
file has been created.
The contents of the .terraform.lock.hcl
file will look like this:
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/azuread" {
version = "2.18.0"
hashes = [
"h1:cpppwljjeyqTnwNlQGHK+o1Jb5Tf4UAnJiInNkN6P38=",
"zh:04b2b5e09dedd45316cd47d21d2ed7e3cd7a4e5f3c8b6e8fba0a10e7eb2a1ca9",
"zh:232440381b60d918e0da0ea8e8a2e8a78a4fe1ae785b3f829f2f965464ab79a2",
"zh:688111a9cb8d9ffec2ccabacb27456d275bf1d404dd5f85e681715abbdd64654",
"zh:7f37b7be7859170f077c58e74be42b5571e364c52aac0a2df3a6a14fbe48d3c5",
"zh:a385743bfae40f6a01bf6662a3c7a71035113c33965e0dbf421997a015734d08",
"zh:a97b7430647d7b449441e5515f11a4d123f6d6a383a8fbca5c0a4086be407358",
"zh:be6d40d1431e8e71a96cce2099a259ef5a8dfb0e849817875d9ee4bb8cf59d40",
"zh:db3b541d90881d620111fdae0efe90d1e0972fc80a2b4346d4af8d96e1fc1195",
"zh:e6d9e0481f2bdc16ee69aa00001d9713282daccfbc621e0143c53d9f6dbdb537",
"zh:ee5b724ca78301057059eff18062fa88d8b24ac7b39f52eb17b8609634427ce0",
"zh:fdb169f89551f97f6b0bf90d61d5fda166a25cce6867ec16f63c3bfb4d90a0a2",
]
}
provider "registry.terraform.io/hashicorp/azurerm" {
version = "2.98.0"
constraints = ">= 2.95.0"
hashes = [
"h1:8Sg08lYcJC12Y8EH5oFfgBhIR9OhZFKF633NjOMjilY=",
"zh:025f656a6d3ecc30f7cc2279bc41969789987b405e3fa8a7c1eb5f74e3ee1140",
"zh:23c54b330678a16378156193d709bbddce3ba76ee827fd65fb751ce90790af9e",
"zh:2d28d359ce6881918bd6c03701f6ec4fd90215abfce9b863cfd3172e28c1acb3",
"zh:31df88584d39cf876fa45ff6de92e67e03814a0985d34c7671bd6989cda22af8",
"zh:36019109790b9a905770355e5bbb57b291a9689a8b9beac5751dcbdb1282d035",
"zh:5fb4a277331c459db9e1b150d79b7c7157a176ceca871195e81225e949141b72",
"zh:7ec304afa1b60dc84257a54cea68e97f85df3feb405d25a9226a4f593ed00744",
"zh:bac469f104b8ad2c8b5ddc88ddae3b0bc27ae5f9c2ccf03f14a001a5c3ed6ae1",
"zh:d860b0ec60a978fe3f08d695326e9051a61cd3f60786fc618a61fbdb5d6a4f15",
"zh:ebcb2911ee27587f63df7eff3836c9a206181a931357c6b9a380124be4241597",
"zh:f37fae57bf7d05c30fda6e5414ad5a4aad1b34d41a5f2465a864736f92ded1ac",
]
}
Notice that the versions are specified, as well as any version constraints. In my main.tf
file I specified a constraint for the azurerm
provider, but not my azuread
provider.
If you want to force Terraform to look for newer versions than those contained in the lock file, you can specify the --upgrade
option, an example of which can be found in the 'terraform init options' section of this article.
What is the difference between Terraform init and plan?
Both terraform init
and terraform plan
are among Terraform's foundational commands.
The init command prepares the Terraform working directory by installing all the necessary provider plugins, downloading modules, and setting up state storage. In contrast, the plan command generates an execution plan showing infrastructure changes without implementing them.
While init prepares the environment for Terraform, plan previews potential adjustments based on the defined configuration against the current state.
Do you need to run Terraform init before every Terraform plan?
terraform init
is the first command you should run in the workflow, however, if you know that no changes have been made to the modules, backend, or provider installations, you can go ahead and run terraform plan
without running terraform init
first. In automation, it is always best practice to put in a terraform init
stage first to make sure the modules, providers, and backend are always up-to-date as specified in your configuration files.
Is it safe to run Terraform init multiple times?
It is always safe to run terraform init
. It will never modify the configuration or destroy any resources. If it is run multiple times, it will simply update the working directory with the changes in the configuration. This will be required if the configuration changes include the addition of a new provider block or a change to the backend storage location, for example.
Running Terraform init in automation
terraform init
will be the first step in configuring your Terraform workflow in an automation pipeline. To set this up using Azure DevOps pipelines, we will use the azure-pipelines.yml
file contained in the example code repository.
stages:
- stage: Build
displayName: Terraform-Plan
jobs:
- job: TerraformPlan
displayName: Terraform-Plan
pool:
name: Private-Build-Agents
steps:
- checkout: self
- script: ls $(System.DefaultWorkingDirectory)
- task: TerraformInstaller@0
displayName: 'Use Terraform latest'
- task: TerraformCLI@0
displayName: Terraform-Init
inputs:
command: 'init'
workingDirectory: '$(System.DefaultWorkingDirectory)'
backendType: 'azurerm'
backendServiceArm: 'IAC Service Connection-Azure'
backendAzureRmResourceGroupName: 'tf-rg'
backendAzureRmStorageAccountName: 'jacktfstatesa'
backendAzureRmContainerName: 'terraform'
backendAzureRmKey: 'adgroups.tfstate'
- task: TerraformCLI@0
displayName: Terraform-Plan
inputs:
command: 'plan'
workingDirectory: '$(System.DefaultWorkingDirectory)'
environmentServiceName: 'RG Service Connection'
This file specifies the pipeline in YAML format. As the pipeline is defined as code rather than set up manually through the Azure DevOps GUI, it can be considered 'pipeline-as-code', whereas terraform configuration files themselves would be considered 'infrastructure-as-code'.
The pipeline file specifies two stages, the terraform init
stage and a terraform plan
stage.
The terraform init
stage is specified starting at line 15. Note the backend details are specified here. They can also be specified in the main.tf
configuration file, but if they were committed from the file, the pipeline would still run using the values specified in the azure-pipelines.yml
file.
During the pipeline run, Azure DevOps will run the Terraform CLI task with the Azure DevOps plugin which it will set up during the 'initialize job' phase.
In some cases where you have lots of providers specified and want to avoid them being installed repeatedly on each pipeline run by terraform init
, you may wish to make the providers available locally. This is possible and is covered in this advanced 'Terraform in automation' tutorial.
Terraform init options
There are multiple options that can be used with the terraform init
command that can be viewed using the --help
flag. Below are some examples of the more useful and commonly used options.
1. terraform init -backend=false
Terraform init backend=false
lets you disable backend or Terraform Cloud initialization for this configuration and use what what was previously initialized instead. aliases: -cloud=false
.
As you might infer, this option does not create a local backend
2. terraform init -backend-config=path
When you use terraform init -backend-config=path
, your configuration will be merged with that in the configuration file's 'backend' block. This option allows you to dynamically configure the backend by passing in a file path or key/value pairs during the initialization process, separating potentially sensitive values from your main Terraform code.
3. terraform init -force-copy
terraform init -force-copy
is used to suppress prompts about copying state data when initializing a new state backend. It allows you to initialize a new backend and migrate the state data from the previous backend automatically, without any interactive prompts for confirmation. This enables smoother backend switching in automated workflows. However, it can potentially cause data loss if the state is being copied incorrectly.
4. terraform init -from-module=SOURCE
With terraform -init from-module=SOURCE
you can initialize the current working directory with the contents of the specified module source.
5. terraform init -get=false
terraform init -get=false
initialize a Terraform working directory by installing providers but skipping automatically downloading any declared modules. This can be useful in certain workflows where you want more control over when modules are downloaded or updated
If the working directory has been previously initialized without any further changes, this can be used to prevent child modules from being re-downloaded.
6. terraform init -input=false
terraform init -input=false
command is used to initialize the Terraform working directory without prompting for any input.
This is useful in automation pipelines where the run may simply hang and time out waiting for input. For example, if your code had an error that prompted for a value for a missing variable, the pipeline would prompt for this rather than erroring and finishing the run.
7. terraform init -lock=false
terraform init -lock=false
disables the locking of the state file during the initialization step. When Terraform runs, it locks the state file so it cannot be modified by other processes.
If a Terraform run ends unexpectedly (crashes or is canceled mid-run), or multiple pipelines have been running simultaneously using the same state file, then the state file remains locked. On the next run, you may see any error as pictured below, showing that the 'state blob is already locked'.
Error message: State blob is already locked
The easiest fix for this issue is to navigate to the storage account and then to the container in the Azure portal that holds the state file. The blob will show as 'Leased' under the leased state column. Select the state file and hit the 'break lease' button.
Lease state on the state file blob should then show as 'broken', and you can kick off the next Terraform run.
You can also use Azure CLI to do this, e.g.
az storage blob lease break -b terraform.tfstate -c myAzureStorageAccountContainerName --account-name "myAzureStorageAccountName" --account-key "myAzureStorageAccountAccessKey"
Or, using Terraform you can force the unlock, (get the LockID from the error) e.g.
terraform force-unlock <LockId>
You can also specify the lock=false
option which as mentioned might be dangerous if others might be concurrently running against the same state file, so it is better to fix the issue using the steps above.
8. terraform init -lock-timeout=<duration>
With terraform init -lock-timeout=<duration>
you can specify the maximum time Terraform should wait to acquire a lock on the state file during initialization.
9. terraform init -no-color
terraform init -no-color
initializes the Terraform working directory as usual, but with all color codes removed from the command output.
For example, when using Terraform in an Azure DevOps release pipeline, you may notice that the encoding on the console output has annoying characters displayed. This is caused by Terraform attempting to output colors to the console to signify additions, deleting, changes, etc. Azure DevOps can't handle this. Examples are as follows:
10. terraform init -plugin-dir
You can use terraform init
with the -plugin-dir
option to specify an alternate directory for Terraform to install and look for provider plugins*.*
You can use pre-installed plugins by either putting them in the same directory as the Terraform binary or by setting the -plugin-dir
flag. This might be desirable when you have connectivity issues or restrictions to https://releases.hashicorp.com.
11. terraform init -upgrade
terraform init -upgrade
is useful when any pinned provider versions have been changed and you want to force Terraform to search for the latest version.
For example, if I pin my provider version for azuread
to version 2.17.0, on running terraform init
for the first time, this is the version that will be installed.
azuread = {
source = "hashicorp/azuread"
version = "=2.17.0"
}
If I then proceed to remove the version constraint from the azuread
provider:
azuread = {
source = "hashicorp/azuread"
}
Then re-run terraform init
you will notice that Terraform reports that it will use the previously installed version (still version 2.17.0) rather than searching again for the latest version.
When re-runnig with the upgrade flag, you will notice that Terraform searches for the latest version of the azuread
provider (v2.18.0 at the time of writing). Notice that any modules in the configuration have also been upgraded.
12. terraform init -migrate-state
terraform init -migrate-state
lets you migrate your Terraform state data from one backend to another when changing backend configurations
13. terraform init -reconfigure
terraform init -reconfigure
option is used to reconfigure the backend for an existing Terraform working directory while ignoring any previously saved configuration.
14. terraform init -ignore-remote-version
terraform init -ignore-remote-version
command is a rarely used option that lets you override checking that the local and remote Terraform versions agree when using the remote backend, allowing an operation to proceed even when there is a version mismatch.
Key points
The terraform init
command is the first command you should use to prepare the working directory. terraform init
specifically performs the following actions:
Backend Initialization
Child Module Installation
Plugin Installation
It is always safe to run terraform init
. It can be used in automation and comes with a myriad of options that can be used to further control its behavior.
Note: New versions of Terraform will be placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that will expand on Terraform's existing concepts and offerings. It is a viable alternative to HashiCorp's Terraform, being forked from Terraform version 1.5.6.
Don't forget to take a look at how Spacelift helps you manage the complexities and compliance challenges of using Terraform. It brings with it a GitOps flow, so your infrastructure repository is synced with your Terraform Stacks, and pull requests show you a preview of what they're planning to change. It also has an extensive selection of policies, which lets you automate compliance checks and build complex multi-stack workflows. You may also check how initialization policies work with Spacelift.
Written by Jack Roper and Flavius Dinu.