Tips

Terraform

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

Module Definitions

  1. A Terraform module is a container for multiple resources that are used together, allowing for reuse, simplification, and management of Terraform configurations.
  2. One thing to remember about a terraform module is that it should be for a composite of resources, not a single resource.

Moving Resources in Terraform State

  1. First you have to update your infrastructure files, but before running terraform apply, do the following:
  2. Below command will print of list ot lines to check for moving desired resources:
    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"
    
  3. Carefully inspect and run each terraform state mv -dry-run From To line that is printed above to make 100% sure it’s what you want.
  4. Remove the -dry-run in each line and re-run to move the resource.
  5. After doing all the moves, you can do a terraform plan and a terraform apply to confirm everything is good.

Moving Terraform-managed Azure resources from one Resource Group to another (OLD)

See https://stackoverflow.com/questions/69283041/terraform-move-entire-resource-group-to-new-azure-subscription

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"

Terraform Workflow

Below ASCII diagram depicts the typical Terraform workflow:

                         Update code
                        ┌───────────────┐
                        ▼               │
     ┌────────┐     ┌────────┐     ┌────┴────┐     ┌──────────┐
     │        │     │        │     │         │     │          │
     │  init  ├────►│  plan  │     │  apply  ├────►│ destroy  │
     │        │     │        │     │         │     │          │
     └────────┘     └───┬────┘     └─────────┘     └──────────┘
                        │               ▲
                        └───────────────┘
                         Deploy infra

Manage Azure With Terraform

export 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>"
    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

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 {
  }
}
# 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
}

Manage Terraform with Github Actions

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:

(Needs clean up)

Extent of Terraform Privileges

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.

Single vs Multi-states

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.

Mono vs Multi-repo

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.

  1. For a mono repo setup the 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.

  1. For a multi repo setup the 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

See also