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.
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.
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.
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" = [
{...},
{...}
]
}
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
}
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).
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.
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.