Terraform is the go-to choice for Infrastructure as Code (IaC) due to its simplicity and effectiveness. You have the flexibility to create the initial setup in various ways. In this guide, I'm presenting an approach that is both adaptable and production-ready, seamlessly integrating with CI/CD pipelines.
Files
main.tf
- main file with resource definition
resource "azurerm_resource_group" "rg" { name = var.name location = var.location tags = merge(var.tags, local.default_tags) }
provider.tf
- place to specify all version required
terraform { required_version = ">= 1.5" required_providers { azurerm = { source = "hashicorp/azurerm" version = ">= 3.90.0" } } backend "azurerm" {} } provider "azurerm" { features {} }
Having backend "azurerm" {}
it allows to provide backend credentials as a environment variables during execution
variables.tf
- Definition of interface - input variables (not values)
variable "name" { description = "The name of the resource group" type = string } variable "location" { description = "The location of the resource group" type = string } variable "tags" { description = "A map of tags to add to the resource group" type = map(string) }
variables.auto.tfvars
- values of the variables defined in variables.tf
file
name = "rg1" location = "eastus" tags = { environment = "dev" }
All *.auto.tfvars
files are being loaded during execution without using -var-file flag
output.tf
- definition of output interface
output "name" { value = azurerm_resource_group.rg.name description = "The name of the resource group" }
locals.tf
- local default values or local functions to make main.tf
file more human readable
locals { default_tags = { manneged_by = "Terraform" costcenter = "12345" } }
.env
- credential to connect to backend and manage provisioner's resources.
export ARM_CLIENT_ID="23a023..." export ARM_CLIENT_SECRET="NumaYEPkxG..." export ARM_TENANT_ID="2c29-..." export ARM_SUBSCRIPTION_ID="bfff..." export TF_STATE_BLOB_ACCOUNT_NAME="sa" export TF_STATE_BLOB_CONTAINER_NAME="terraform" export TF_STATE_BLOB_RESOURCE_GROUP_NAME="rg1" export TF_STATE_BLOB_KEY="test.tfstate" export ARM_ACCESS_KEY="access_key"
This approach offers the following advantages:
- .env can be replace in any time, this allow as to use another environment
- many file can be easly track in git
- update of provisioner version taking place in separate file which doesn't touch the resources configuration
- backend/provisioner variables can be set during execution, as a consequence, it allows to use that code in CI/CD pipelines
Execution
SPN
At the beginning, SPN with appropriate role (contributor) needs to be created
# Login to Azure az login # Create a new Service Principal az ad sp create-for-rbac --name <name>
This command will output JSON with the appId
, displayName
, name
, password
, and tenant
.
# Cole assigment az role assignment create --assignee <app_id> --role Contributor --subscription <subscription_id>
Backend
Second point is Backend - you will store a state file there
# Resource Group az group create --name <resource-group-name> --location <location> # Storage account az storage account create --name <storage-account-name> --resource-group <resource-group-name> --location <location> --sku Standard_LRS # get storage account key az storage account keys list --account-name <storage-account-name> --resource-group <resource-group-name> --query '[0].value' --output tsv # Container az storage container create --name <container-name> --account-name <storage-account-name> --account-key <account-key>
.env file
For local development is useful to have that file. You can find that information in previous paragraphs
Terraform
Execution is typical
# Install provisioners terraform init # dry run terraform plan # Provisioning terraform apply
Summary of initial setup
The provided instructions give a solid starting point for diving into Terraform, with practical examples guiding users through file setup, backend configuration, and execution of essential commands.
Read more
Pulumi - Terraform's competitor