Adrianistán

Terraform, infraestructura como código declarativo

17/09/2019

Como alguno de los lectores ya sabrá, he empezado a trabajar este verano en Telefónica como becario. El proyecto donde estoy es 100% cloud y para ello usamos muchas herramientas. Hoy os vengo a hablar de Terraform, una herramienta que nos permite declarar la infraestructura como código y de forma declarativa.

¿Por qué?

Antiguamente, cuando se desplegaban servicios, o una de dos: o se usaban servidores físicos, que se tenían que comprar, instalar, mantener, ... o se usaba un hosting compartido, el cuál para ciertas cosas puede estar bien, pero tiene muchas limitaciones. Hoy en día, gracias a los proveedores cloud, podemos alquilar infraestructura bajo demanda. Terraform es una herramienta para dejar por escrito toda la infraestructura en la nube que necesita nuestro servicio y crearla/modificarla según modifiquemos los archivos. Además es declarativo, lo que quiere decir que tenemos que expresarnos en código según lo que queremos obtener, no cómo, por tanto también cumple el papel de documentación. Piensa en Terraform como en los planos de un edificio. Nosotros definimos las vigas, paredes, tuberías sobre el papel y los obreros se encargan de construirlo. Terraform es eso, nosotros definimos máquinas, bases de datos y el programa se encarga de construir la infraestructura en la nube.

Funcionamiento interno

Terraform tiene varios componentes que vamos a definir primero:

Una máquina virtual sencilla

Para este ejemplo voy a usar Microsoft Azure, pero se pueden usar otros proveedores similares haciendo los cambios adecuados. Cualquier estudiante puede pedir el GitHub Education Pack que regala 100$ para gastar en Azure.


provider "azurerm" {
  version = "~> 1.28.0"
}

provider "random" {}

resource "random_string" "username" {
  length = 12
  special = false
}

resource "random_password" "password" {
  length = 12
  special = true
}

resource "azurerm_resource_group" "main" {
  name     = "blog"
  location = "France Central"
}

resource "azurerm_public_ip" "main" {
  name                = "blog-ip"
  location            = "${azurerm_resource_group.main.location}"
  resource_group_name = "${azurerm_resource_group.main.name}"
  allocation_method   = "Static"
}

resource "azurerm_virtual_network" "main" {
  name                = "blog-network"
  address_space       = ["10.0.0.0/16"]
  location            = "${azurerm_resource_group.main.location}"
  resource_group_name = "${azurerm_resource_group.main.name}"
}

resource "azurerm_subnet" "internal" {
  name                 = "blog-internal"
  resource_group_name  = "${azurerm_resource_group.main.name}"
  virtual_network_name = "${azurerm_virtual_network.main.name}"
  address_prefix       = "10.0.2.0/24"
}

resource "azurerm_network_interface" "main" {
  name                = "blog-nic"
  location            = "${azurerm_resource_group.main.location}"
  resource_group_name = "${azurerm_resource_group.main.name}"

  ip_configuration {
    name                          = "conf1"
    subnet_id                     = "${azurerm_subnet.internal.id}"
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id = "${azurerm_public_ip.main.id}"
  }
}

resource "azurerm_virtual_machine" "main" {
  name                  = "blog-vm"
  location              = "${azurerm_resource_group.main.location}"
  resource_group_name   = "${azurerm_resource_group.main.name}"
  network_interface_ids = ["${azurerm_network_interface.main.id}"]
  vm_size               = "Standard_DS1_v2"

  storage_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }
  storage_os_disk {
    name              = "disk-os-1"
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "Standard_LRS"
  }
  os_profile {
    computer_name  = "blog"
    admin_username = "${random_string.username.result}"
    admin_password = "${random_password.password.result}"
  }
  os_profile_linux_config {
    disable_password_authentication = false
  }
}

output "login_information" {
  value = "${random_string.username.result}:${random_password.password.result}:${azurerm_public_ip.main.ip_address}"
}

 

El código es sencillo y declarativo. Definimos dos proveedores (azurerm y random) para los recursos que vamos a definir. Los recursos se identifican por el tipo de recurso y por un identificador único. Luego definimos usuario y contraseña aleatorios para la máquina, un grupo de recursos de Azure, una dirección IP pública, la infraestructura de red y finalmente una máquina virtual con Ubuntu. En output mostramos la IP pública de la máquina, su usuario y contraseña, para poder conectarnos. Aquí podríamos pasar a usar otras herramientas como Ansible para instalar todo lo necesario en la máquina virtual.

Aquí hemos visto el uso de referencias, con el símbolo del dólar. Estas referencias le permiten a Terraform construir/destruir la infraestructura en el orden correcto. Por defecto Terraform intenta realizar todas las operaciones en paralelo, salvo que necesite una información que venga de otro recurso. En ese caso, tiene que ser creado con éxito para poder proceder a la creación del siguiente. Si no podemos usar referencias, podemos usar depends_on.

Hagamos un terraform apply para realizar un plan y aplicarlo. Además veremos las salidas definidas.

Y vemos como ha sido creado en Azure con éxito.

Ahora si modificamos algo desde la web y volvemos a ejecutar terraform apply, se detectará que el estado real es diferente al óptimo y tratará de revertir el cambio. También si modificamos los ficheros se intentará modificar el entorno real. 

Variables y bucles

Terraform admite datos externos y bucles. Vamos a verlo. Con variable podemos introducir datos desde variables de entorno, la CLI o un fichero .tfvars. Estas variables pueden tener un valor por defecto. Las variables se definen con -var="var_name=var_value", con las variables de entorno TF_VAR_var_name=var_value y el fichero terraform.tfvars.

Los bucles se pueden realizar con count, si cada elemento no tiene identidad (por ejemplo, el número de réplicas de una VM igual) o con for_each si cada elemento debe tener identidad (por ejemplo, una VM para cada país).


provider "azurerm" {
  version = "~> 1.28.0"
}

variable "name" {
  default = "adrianistan"
}

variable "country" {
  default = [
    "es",
    "ar"
  ]
}

resource "azurerm_resource_group" "main" {
  name     = "blog"
  location = "France Central"
}

resource "azurerm_app_service_plan" "main" {
  name                = "main-appserviceplan"
  location            = "${azurerm_resource_group.main.location}"
  resource_group_name = "${azurerm_resource_group.main.name}"
  kind                = "Linux"
  reserved            = true

  sku {
    tier = "Basic"
    size = "B1"
  }
}

resource "azurerm_app_service" "main" {
  for_each            = toset(var.country)
  name                = "${var.name}-${each.value}"
  location            = "${azurerm_resource_group.main.location}"
  resource_group_name = "${azurerm_resource_group.main.name}"
  app_service_plan_id = "${azurerm_app_service_plan.main.id}"

  site_config {
    linux_fx_version = "DOCKER|nginx:1.17.3"
  }
}

output "web" {
value = "${formatlist("%s", [for o in azurerm_app_service.main : o.default_site_hostname])}"
}

En este ejemplo usamos un bucle for_each para generar dos Azure App Services (cargados con nginx), uno para España y otro para Argentina.

Comprobamos como funciona:

Espero que esta breve introducción a Terraform os haya resultado interesante. Se trata de un lenguaje sencillo, declarativo y donde la mayor parte de nuestros problemas vendrán de conocer la documentación de cada proveedor al dedillo.

 

Tags: programacion terraform tutorial declarativa