Terraform, infraestructura como código declarativo
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:
- recurso: cualquier cosa que exista en la nube (o fuera) sobre la que Terraform tenga control de creación, modificación y destrucción
- datos: puntos donde podemos obtener información para nuestro código.
- salidas: valores generados durante la ejecución del plan y que pueden ser de interés (direcciones IP, contraseñas, ...)
- código: aquí es donde vamos a escribir la infraestructura que necesitamos (máquinas virtuales, clústeres de Kubernetes, balanceadores de carga, discos, ...). Representa el estado óptimo del sistema.
- estado: aquí Terraform almacena la infraestructura real, con mucha más información que la que existe en el código. Además le sirve a Terraform para acordarse entre ejecuciones de la infraestructura que controla
- proveedores: los recursos y los datos necesitan de un plugin que conecte la nube con Terraform. Terraform contiene multitud de proveedores: Azure, AWS, Google Cloud, Netlify, OpenStack, Kubernetes, Let's Encrypt, Helm, Digital Ocean, OVH, Alibabba Cloud, Oracle Cloud, PostgreSQL, Triton, VMware vSphere, Heroku, Linode, Packet, 1&1 y muchos más. Adicionalmente, existen proveedores creados por terceros.
- plan: se trata del paso en cual Terraform compara el estado con el código y encuentra diferencias entre el estado real y el óptimo. Opcionalmente el estado se puede actualizar antes (lo hace por defecto) para tener un estado lo más real posible. Cuando tengamos un plan lo podemos aplicar y entonces Terraform modificará la infraestructura.
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.