Geschreven door Ton Swart

Metadata driven Terraform deployments

DevOps6 minuten leestijd

Wat ooit is begonnen als een hackathon bij DHL, één van onze klanten bij CINQ ICT, is nu een framework aan het worden om Azure resources aan te maken en te beheren. Het idee was om de Infrastructure as Code weg te abstraheren bij de ontwikkel teams, maar om de resources via configuratie files (bijv. YAML) aan te kunnen maken. Deze manier van werken zou ook een eerste stap kunnen zijn naar een vorm van self-service.

Infrastructure As Code (IaC)

Terraform is een van de meest gebruikte tools om infrastructuur in te richten vanuit code (IaC). Het gebruikt hiervoor een taal genaamd HCL (Hashicorp Configuration Language). In deze blogpost onderzoeken we hoe we het aanmaken van resources kunnen configureren in YAML files en wat de voordelen hiervan zijn.

Waarom YAML?

Een van de beste eigenschappen van YAML is de afwezigheid van “syntax overhead”. Hiermee kunt je key value pairs overzichtelijk opschrijven. Laten we eens kijken naar een vergelijking van bepaalde HCL-code en YAML.

HCL

locals {
  storage_accounts = [
    {
      name                     = "storageaccountname"
      account_tier             = "Standard"
      account_replication_type = "ZRS"
      access_tier              = "Hot"
      containers               = [
        {
          name                  = "vhds"
          container_access_type = "auto"
        }
      ]
    }
  ]
}

YAML

storage_accounts:  - name: storageaccountname
    account_tier: Standard
    account_replication_type: ZRS
    access_tier: Hot
    containers:
      - name: vhds
        container_access_type: auto

Zoals je kunt zien, is het verschil in aantal regels behoorlijk groot. Natuurlijk zal dit veranderen zodra we wat HCL-code toevoegen om de YAML-configuratie te importeren, maar dit wordt snel groter als de infrastructuur groeit.

Inlezen van de YAML files in Terraform

Het laden en converteren van een YAML-bestand naar HCL is heel eenvoudig. Je kunt het op één regel doen met behulp van de yamldecode en file functies:

locals {
  config = yamldecode(file("config.yaml"))
}

Na het inlezen van alle yaml files is er een Terraform map beschikbaar met alle resource-types en de in de yaml geconfigureerde properties.

config = {
  "keyvaults" = [
    {...},
    {...}
  ]
  "storage_accounts" = [
    {...},
    {...}
  ]
}

Orchestration en for_each loops

De "router" module is de orchestratie module en de spin in het web die meerdere resources en/of resources-typen aan kan maken. De router module leest alle yaml files in en roept voor iedere resource-type de betreffende resource module aan. Als er in de yaml file meerdere resources van hetzelfde type worden aangemaakt dan zal middels een for_each loop de resource module meerdere malen worden aangeroepen.

module "keyvaults" {
  for_each = {
    for resource in lookup(config, "keyvaults", []) :
    resource.name => resource
  }
  source = "../../microsoft.keyvault/vaults"
  keyvault_info = each.value
}

Resource module

Om de resource module folder structuur te geven, gebruiken we het resource-type als folder naam.

modules/
├── internal
│   ├── global
│   ├── router
├── datastax
│   └── astradb
├── microsoft.cache
│   └── redis
├── microsoft.datafactory
│   └── factories
├── microsoft.dbforpostgresql
│   └── flexibleservers
│       ├── databases
├── microsoft.eventhub
│   └── namespaces
│       └── eventhub
├── microsoft.keyvault
│   └── vaults
├── microsoft.sql
│   └── servers
│       └── databases
├── microsoft.storage
│   └── storageaccounts
├── microsoft.synapse
│   ├── spark_pool
│   └── workspaces
└── microsoft.virtualmachine

De resource module zal de daadwerkelijk resource aanmaken en bevat alle validatie regels op de input parameters die noodzakelijk zijn. Verder zal de module verplichte parameters afdwingen en "waarden van gezond verstand" gebruiken voor niet verplichte niet opgegeven properties.

variable "storageaccount_info" {
  type = object({
    name                          = string
    account_tier                  = string
    account_replication_type      = string
    access_tier                   = string
    is_hns_enabled                = optional(bool, false)

    containers = optional(list(object({
      name                  = string
      container_access_type = optional(string, "private")
    })), [])

    tags        = optional(map(any), {})

    validation {
      condition     = contains(["Standard", "Premium"], var.sa_info.account_tier)
      error_message = "Invalid account tier. Valid options are 'Standard' and 'Premium'."
    }

    validation {
      condition     = contains(["LRS", "GRS", "RAGRS", "ZRS", "GZRS", "RAGZRS"], var.sa_info.account_replication_type)
      error_message = "Invalid account replication type. Valid options are 'LRS', 'GRS', 'RAGRS', 'ZRS', 'GZRS' and 'RAGZRS'."
    }

    validation {
      condition     = contains(["Hot", "Cool"], var.sa_info.access_tier)
      error_message = "Invalid access tier. Valid options are 'Hot' and 'Cool'."
    }
}

Ook zal de resource module best practices implementeren (denk bijv. aan privacy-by-design principes zoals private endpoints, encryptie, logging) zonder dat dat in de yaml files is aangegeven. De maker van de yaml file hoeft alleen maar de meest voor de hand liggende properties in te vullen zoals bijv. de naam van de resource en de uitvoering (SKU, pricing tier).

Samenvatting

YAML-bestanden zijn eenvoudiger dan Terraform locals en/of tfvars-bestanden voor het beheren van niet-triviale Terraform-configuraties. Omdat development teams nu zelf YAML files kunnen aanmaken is dit een eerste voorzichtige stap naar een vorm van self-service.

Vervolgstappen

Binnen de DevOps-unit van CINQ ICT gaan we dit jaar een Terraform hackathon organiseren om te kijken of we dit concept nog verder kunnen uitwerken. Zo heeft iedere resource module een scope van alleen zichzelf en heeft dus geen weet van het bestaan van meerdere en/of andere resource-typen.

Een belangrijk onderwerp van de hackathon zal dan ook zijn hoe we een integratie laag tussen de verschillende modulen kunnen realiseren. Denk daarbij bijv. aan hoe koppelen we een keyvault aan een Kubernetes namespace, of een storage account aan een Synapse Workspace, hoe gaan we om met firewall rules? Zonder daarbij de code in de modules heel complex te maken.