Tips on Hashicorp Terraform.
It is an open-source tool for provisioning and managing cloud infrastructure as code (IaC). It codifies infrastructure in configuration files that describe the desired state for your topology. Terraform enables the management of any infrastructure - such as public clouds, private clouds, and SaaS services by using Terraform providers. Each provider adds a set of resource types and/or data sources that Terraform can manage.
Side note
terraform apply
, do the following:temp1=$(mktemp) && temp_from=$(mktemp) && temp_to=$(mktemp) && \
terraform plan | grep "will be" > "$temp1" && \
grep destroyed "$temp1" | awk '{print $3}' | sed "s/.*/'&'/" > "$temp_from" && \
grep created "$temp1" | awk '{print $3}' | sed "s/.*/'&'/" > "$temp_to" && \
paste -d' ' "$temp_from" "$temp_to" | sed "s;^;terraform state mv -dry-run ;" && \
rm "$temp1" "$temp_from" "$temp_to"
terraform state mv -dry-run From To
line that is printed above to make 100% sure it’s what you want.-dry-run
in each line and re-run to move the resource.terraform plan
and a terraform apply
to confirm everything is good.First, you’ll need to confirm the resource can actually be moved via the portal, by actually attempting to do the move.
If you were able to move it, now you can proceed by getting a list of all respective IDs from the TF state:
terraform state pull | grep "<RG-NAME>"
<LIST of IDs>
You’ll also need to list their addresses with terraform state list
Next, carefully remove each of the respective resources using their addresses, for example:
terraform state rm azurerm_dns_zone.mydomain
terraform state rm azurerm_dns_a_record.mydomain-apex
terraform state rm azurerm_dns_cname_record.mydomain-www
Now import the new ones, using the IDs from the LIST of IDs command above and changing the resource group nane, for example:
terraform import azurerm_dns_zone.mydomain "/subscriptions/<UUID>/resourceGroups/<NEW-RG-NAME>/providers/Microsoft.Network/dnsZones/mydomain.com"
terraform import azurerm_dns_a_record.mydomain-apex "/subscriptions/<UUID>/resourceGroups/<NEW-RG-NAME>/providers/Microsoft.Network/dnsZones/mydomain.com/A/@"
terraform import azurerm_dns_cname_record.mydomain-www "/subscriptions/<UUID>/resourceGroups/<NEW-RG-NAME>/providers/Microsoft.Network/dnsZones/mydomain.com/CNAME/www"
Below ASCII diagram depicts the typical Terraform workflow:
Update code
┌───────────────┐
▼ │
┌────────┐ ┌────────┐ ┌────┴────┐ ┌──────────┐
│ │ │ │ │ │ │ │
│ init ├────►│ plan │ │ apply ├────►│ destroy │
│ │ │ │ │ │ │ │
└────────┘ └───┬────┘ └─────────┘ └──────────┘
│ ▲
└───────────────┘
Deploy infra
Install Terraform and Azure CLI on macOs:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
brew install azure-cli
main.tf
and all other Infrastructure-as-Code Terraform configuration filesazuretf-sp00
az-terraform-management
and make above SP a member of itexport ARM_SUBSCRIPTION_ID="<azure_subscription_id>"
export ARM_TENANT_ID="<azure_subscription_tenant_id>"
export ARM_CLIENT_ID="<service_principal_appid>"
export ARM_CLIENT_SECRET="<service_principal_password>"
WARNING! Exposing an SP secret as a BASH variable is poor security, and should only be done for testing. For a production system you would do this in a more secure fashion, typically using a secrets manager such as HashiCorp Vault, Azure Key Vault, or even Github repository secrets.
Manually via the Azure portal create an Azure Storage Account (SA) to have Terraform store its “state” there. Or you can do so from the shell terminal, using Azure CLI tool :
az group create --name mytfstates --location eastus
az storage account create --resource-group mytfstates --name mytf --sku Standard_LRS --encryption-services blob
az storage container create --name mytfstate --account-name mytf
az storage account keys list --resource-group mytfstates --account-name mytf --query '[0].value' -o tsv
export ARM_ACCESS_KEY="use_above_storage_account_container_access_key"
main.tf
with these recommended initial values:# main.tf
terraform {
required_version = ">= 1.3.2"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0.2"
}
}
backend "azurerm" {
resource_group_name = "mytfstates"
storage_account_name = "mytf"
container_name = "mytfstate"
key = "terraform.tfstate"
}
}
provider "azurerm" {
skip_provider_registration = true
features {
}
}
rg-myaz01.tf
:
resource "azurerm_resource_group" "my-test-rg" {
name = "my-test-rg01"
location = "eastus"
}
terraform plan
to see what TF will doterraform apply
to do it (that is, deploy above new test resource group)*.tf
files in this working repo when you do scan/applyazuretf-sp00
doesn’t have the right Azure privilegesaz-terraform-management
and at the proper scope within your tenantterraform import
to import existing resources into the TF stateimport
does NOT create a configuration file. In fact, in order to import an existing resource, you MUST first create a minimalist config file for that resource, that TF needs to reference the importing functionterraform state list
to locate the imported resource namesterraform state show <imported-resource-type.name>
to show full detailsmy-rbac-role.tf
:# my-rbac-role.tf
# Minimalist file for existing "my-rbac-role" to be imported
data "azurerm_management_group" "primary" {
# To reference an existing management group where the role is defined
name = "<UUID>"
}
resource "azurerm_role_definition" "my-rbac-role" {
name = "my-rbac-role"
scope = data.azurerm_management_group.primary.id
}
Next, run the following:
terraform import azurerm_role_definition.my-rbac-role-def "/providers/Microsoft.Authorization/roleDefinitions/<UUID>|/providers/Microsoft.Management/managementGroups/<UUID>"
Similarly, for an existing role assignment you would create a similar minimalist file then do:
terraform import azurerm_role_assignment.my-rbac-role-assign "/providers/Microsoft.Management/managementGroups/<UUID>/providers/Microsoft.Authorization/roleAssignments/<UUID>"
References:
In general, the typical options when using Terraform are the following: 1. Deploy locally via Terraform CLI 2. Deploy using Terraform Cloud/Enterprise 3. Deploy using Azure DevOps 4. Deploy using GitHub Actions
Option one is really just to test Terraform. The others are a little more serious when managing a cloud environment.
Here we will focus on Option #4 (see https://jloudon.com/cloud/Using-GitHub-Actions-and-Terraform-for-IaC-Automation/), which will entail: - Source control : GitHub private repository - Workflow automation : GitHub Actions and HashiCorp’s GitHub Action (setup-terraform) - Infrastructure as code : Terraform - Terraform remote backend : Azure Storage Account container - Target cloud environment : Microsoft Azure
One can certainly use a different SCM, or a differnt cloud, so above depend on your choices
Intead of creating service principal TF-AzureRM-Policy as mentioned in above article, let’s just use the same SP and AZ group mentioned in previous section (azuretf-sp00
and az-terraform-management
)
Assign AZ group az-terraform-management
the role “Resource Policy Contributor”
Integrate TF state into Github Action workflow:
.github
with subfolder workflows
in the git repo(Needs clean up)
It’s wise to limit and isolate the scope and roles of the security context under which Terraform operates. This reduces the potential blast-radius if the Terraform credentials may happen to be compromised.
At some point you’ll need to decide whether you will only have a single state file (local or remote). A single state file works fine for very small setups, but can quickly become cumbersome for larger infrastructure. Particularly if there are many different teams submitting changes to an environment and the state gets locked while each change is being applied. That is when having multiple states or workspaces will come in handy. It gives an organization more flexibility by allowing changes to be independently deployed without locking their separate workflows.
When working with Terraform there will come a point where you will need to decide whether to use a mono-repo versus a multi-repo IaC structure for your source code repository. The common analogy is the monolithic project type as opposed to the micro-services project design. There’s no right or wrong in this. The answer will depend on different factors, such as how tightly-coupled your project and the development and operation teams are.
The typical mono-repo repository structure looks like the following:
terraform-azure-core/
├── main.tf
├── variables.tf
├── outputs.tf
├── README.md
├── modules/
│ ├── terraform-azure-dir-group/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── terraform-azure-dns-zone/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── terraform-azure-vm/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── examples/
│ ├── example1/
│ │ ├── main.tf
│ │ ├── terraform.auto.tfvars
│ │ └── outputs.tf
│ └── example2/
│ ├── main.tf
│ ├── terraform.auto.tfvars
│ └── outputs.tf
└── tests/
├── test_main.tf
└── test_terraform.auto.tfvars
The alternative is a multi-repo structure where each of the submodules under above modules
subdirectory would instead reside in their own repository.
main.tf
file would look like something like this:# main.tf
# terraform-managed-infra
# This is the calling module defining a specific Azure infrastructure to be
# managed, which leverages the terraform-azure-core.
module "azure_core" {
# This module would manage ALL Azure resources in the infra, and they would
# all be defined in the terraform.auto.tfvars file.
source = "github.com/myorg/terraform-azure-core?ref=1.0.0"
# "?ref-1.0.0" specifies the GH tag or branch of the repo
# source = "./modules/vm" # For local directory testing
# version = "1.0.0" # This is only needed if you're referencing a
# Terraform Cloud or Terraform Enterprise registry as in:
# source = "<REGISTRY_URL/<organization>/<provider>/<module name>"
# for example "spacelift.io/your-organization/vpc-module-name/aws"
dir_groups = var.dir_groups # Defined in the terraform.auto.tfvars
dns_zonnes = var.dns_zones
resource_n = var.var_n # And so on
}
This infrastructure module would then call the single terraform-azure-core module to manage the Azure infrastructure.
main.tf
file would look like something like this:# main.tf
# terraform-managed-infra
# This is the calling module defining a specific Azure infrastructure to be managed,
# which leverages all the different terraform-azure-X submodules.
module "azure_dir_groups" {
# This module would manage all directory groups in the infra as defined in
# variable 'dir_groups' in terraform.auto.tfvars
source = "github.com/myorg/terraform-azure-dir-group?ref=1.0.0"
# Above module sits in a separate repo in Github
dir_groups = var.dir_groups
}
module "azure_dns_zones" {
# This module would manage all DNS zones in the infra as defined in variable
# 'dns_zones' in terraform.auto.tfvars
source = "github.com/myorg/terraform-azure-dns-zones" # Defaults to main branch
# Above module sits in a separate repo in Github
dns_zonnes = var.dns_zones
}
# Other modules can be defined below to manage other resources with their respective separate module