di

Resumen

En este tutorial se muestra el uso de una herramienta como Terraform para construir y modificar infraestructura cloud mediante código. Se estudiarán casos de uso en OpenStack, Google Cloud y Microsoft Azure.

Objetivos
  • Conocer el papel de la Infraestructura como código en la cultura DevOps.

  • Entender la utilidad y potencia de la Infraestructura como código.

  • Inicializar un proveedor cloud en Terraform.

  • Desplegar infraestructura en OpenStack, Google Cloud y Microsoft Azure.

  • Inicializar las máquinas virtuales en el momento de su creación.

Tip

Disponible el repositorio usado en este tutorial.

1. Introducción

Desde hace un tiempo se oye hablar mucho de DevOps. Se trata de una fusión que combina las áreas de:

  • Desarrollo

  • Operaciones

  • Control de calidad

Es una extensión natural de metodologías Agile y es habitual el uso de los principios CAMS, cuyas siglas vienen de:

  • Cultura relacionada con comunicación humana, procesos y herramientas

  • Automatización de procesos

  • Monitorización

  • Sharing feedback, buenas prácticas y conocimiento

En DevOps son habituales las prácticas siguientes:

  • Planificación ágil

  • Despliegue continuo (CI/CD). La subida de cambios al repositorio de código desencadena la ejecución de pruebas automatizadas que finalmente realizan el despliegue de los cambios tras superarse las pruebas.

  • Infraestructura como código (Infrastructure as Code). Se trata del desarrollo de scripts para las tareas de despliegue y gestión de la infraestructura

  • Contenedorización. Combinada con la Infraestructura como código, permite el despliegue instantáneo de aplicaciones en contenedores.

  • Microservicios. Desarrollo de aplicaciones como un conjunto de servicios independientes. Cada servicio se despliega de forma independiente lo que facilita la escalabilidad y la actualización de los servicios.

  • Infraestructura cloud. Favorece el despliegue, la disponibilidad y la automatización.

En esta asignatura ya hemos tratado las prácticas de Infraestructura cloud y de Contenedorización. En este tema nos centraremos en la Infraestructura como código, a la que podríamos caracterizar de esta forma:

  • Uso de scripts para configurar de forma automática el entorno (redes, máquinas virtuales, volúmenes, …) con independencia de su estado inicial.

  • Versionado y desarrollo colaborativo del código de la infraestructura mediante sistemas de control de versiones.

  • Infraestructura repetible.

  • Evita errores humanos.

  • Se especifica el estado de lo que se quiere.

Actualmente, existen varias propuestas para hacer Infraestructura como código. Hay algunas que son específicas de un proveedor, como AWS CloudFormation y Azure Resource Manager. Otras son más genéricas y permiten trabajar con varios proveedores, como Terraform, Pulumi o Ansible.

En este tema estudiaremos Terraform, una herramienta para construir, modificar y versionar infraestructura de forma segura y eficiente.

La cultura DevOps

Si esto te parece interesante, puedes dedicar unos minutos a la lectura de estos documentos breves sobre DevOps. Presentan de forma clara y sencilla términos como DevOps, Integración continua, Microservicios, …​

2. Terraform

Terraform es una herramienta para construir, modificar y versionar infraestructura de forma segura y eficiente. Es un proyecto Open Source desarrollado por HashiCorp, surgido en 2014. Genera un plan de ejecución (preview) indicando qué hará para conseguir el estado deseado. Si hay cambios en la configuración, Terraform detecta los cambios y crea un plan incremental para alcanzar el nuevo estado.

2.1. Instalación

La instalación de Terraform es muy sencilla. Se descarga como un binario que hay que descoprimir. Luego se coloca en un directorio incluido en el PATH del sistema. Probamos su funcionamiento desde la terminal con terraform

Usage: terraform [global options] <subcommand> [args]

The available commands for execution are listed below.
The primary workflow commands are given first, followed by
less common or more advanced commands.

Main commands:
  init          Prepare your working directory for other commands
  validate      Check whether the configuration is valid
  plan          Show changes required by the current configuration
  apply         Create or update infrastructure
  destroy       Destroy previously-created infrastructure

All other commands:
  console       Try Terraform expressions at an interactive command prompt
  fmt           Reformat your configuration in the standard style
  force-unlock  Release a stuck lock on the current workspace
  get           Install or upgrade remote Terraform modules
  graph         Generate a Graphviz graph of the steps in an operation
  import        Associate existing infrastructure with a Terraform resource
  login         Obtain and save credentials for a remote host
  logout        Remove locally-stored credentials for a remote host
  metadata      Metadata related commands
  output        Show output values from your root module
  providers     Show the providers required for this configuration
  refresh       Update the state to match remote systems
  show          Show the current state or a saved plan
  state         Advanced state management
  taint         Mark a resource instance as not fully functional
  test          Experimental support for module integration testing
  untaint       Remove the 'tainted' state from a resource instance
  version       Show the current Terraform version
  workspace     Workspace management

Global options (use these before the subcommand, if any):
  -chdir=DIR    Switch to a different working directory before executing the
                given subcommand.
  -help         Show this help output, or the help for a specified subcommand.
  -version      An alias for the "version" subcommand.

2.2. Sintaxis de los archivos

Hashicorp usa su propio lenguaje de configuración para la descripción de la infraestructura.

Los archivos Terraform se pueden escribir en dos formatos:

  • HashiCorp Configuration Language (HCL). La extensión de los archivos es .tf

  • JSON. La extensión de los archivos es .tf.json

El formato preferido es HCL, ya que es más legible y fácil de escribir. No obstante, el lenguaje HCL es un poco complicado y puede ser confuso al principio, especialmente si se quieren hacer bucles o condicionales.

Note

Pulumi, una herramienta similar a Terraform, permite escribir la configuración en varios lenguajes de programación como Python, TypeScript, Go, …​ Sin embargo, Terraform es más popular y tiene una comunidad más grande. Esto, unido a que el estado en Terraform se almacena en local de forma predeterminada, mientras que en Pulumi se almacena en la nube, hace que Pulumi pueda despertar recelos en entornos corporativos.

Puedes obtener más información en el tutorial Infraestructura como código con Pulumi.

2.3. Recursos y módulos

El objetivo de Terraform es declarar recursos. Todas las características del lenguaje giran en torno a hacer que la definición de recursos sea más flexible y conveniente.

Los recursos puede agruparse en módulos, que crean una unidad de configuración de nivel más alto. Un recurso describe un objeto básico de infraestructura, mientras que un módulo describe un conjunto de objetos y sus relaciones para crear un sistema mayor.

Example 1. Ejemplo de un recurso para crear en OpenStack una IP flotante de la red externa
resource "openstack_networking_floatingip_v2" "tf_vm_ip" {
  pool = "externa"
}

Una configuración Terraform consta de un módulo raíz donde comienza la evaluación. El módulo puede contener módulos hijo que se van llamando unos a otros. La configuración más sencilla de módulo contendría sólo un archivo .tf (main.tf) aunque se recomienda una organización como la siguiente:

  • main.tf: Configuración de lo recursos del módulo

  • providers.tf: Proveedor de los recursos del módulo

  • variables.tf : Variables de entrada

  • terraform.tfvars: Valores de las variables de entrada

  • output.tf: Variables de salida

Tip

El archivo terraform.tfvars es opcional. Se usa para definir valores de variables de entrada. Si no se usa, se pueden definir las variables en el archivo variables.tf. Sin embargo, es una buena práctica usar terraform.tfvars para separar la configuración de la declaración de variables y dejar variables.tf para la declaración de variables. Además, de cara al control de versiones, se facilita la gestión de las variables de entorno, añadiendo el archivo terraform.tfvars al .gitignore. Esto evita que se suban al repositorio valores sensibles como contraseñas o claves de acceso.

Ejemplo de organización:

├── README.md
├── main.tf
├── providers.tf
├── variables.tf
├── terraform.tfvars
├── outputs.tf
├── ...
├── modules/
│   ├── moduleA/
│   │   ├── README.md
│   │   ├── main.tf
│   │   ├── providers.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   ├── moduleB/
│   ├── .../

2.4. Providers

Terraform puede crear stacks de infraestructura en varios proveedores. Por ejemplo, una configuración podría crear infraestructura en Google Cloud Platform y en OpenStack-DI.

Hay gran cantidad de proveedores Terraform, tanto oficiales, mantenidos por Hashicorp, (AWS, Azure, Google Cloud Platform, Heroku, Kubernetes, MongoDB Atlas, OpenStack, VMware Cloud, VMware vSphere, …​) como de la comunidad y terceros (OpenShift, Trello, Telegram, …​)

2.5. Variables

2.5.1. Variables de entrada

Las variables de entrada se usan como parámetros para los módulos. Se crean mediante bloques variable

variable "openstack_user_name" {
    type = string
    description = "The username for the Tenant."
    default  = "mtorres" (1)
}

variable "security_groups" {
    type    = list(string)
    default = ["default"]
}
  1. Valor por defecto. Esto es opcional y se usa si no se especifica un valor en el archivo terraform.tfvars.

Las variables se usan siguiendo esta sintaxis var.<variable>.

provider "openstack" {
  user_name   = var.openstack_user_name (1)
  ....
}
  1. Uso de la variable openstack_user_name

Más información sobre la declaración, uso de variables y constructores de tipos en la documentación oficial.

2.5.2. Configuración de variables

Las variables se pueden configurar de varias formas:

  • En el archivo variables.tf con un valor por defecto.

  • En el archivo terraform.tfvars con un valor específico.

Declaración de variables sin valor por defecto

Por ejemplo, si define una variable user_name en variables.tf, se puede configurar en terraform.tfvars con un valor específico.

Archivo variables.tf
variable "user_name" {
  type        = string
  description = "The username for the Tenant."
}
Archivo terraform.tfvars
user_name = "mtorres"
Declaración de variables con valor por defecto

Si se define una variable user_name en variables.tf con un valor por defecto, se puede configurar en terraform.tfvars con un valor específico o dejar el valor por defecto.

Archivo variables.tf
variable "user_name" {
  type        = string
  description = "The username for the Tenant."
  default     = "mtorres" (1)
}
  1. Valor por defecto

Archivo terraform.tfvars
user_name = "mtorres"

Si se define una variable en variables.tf con un valor por defecto y no se configura en terraform.tfvars, se usará el valor por defecto. En cambio, si se configura en terraform.tfvars, se usará el valor específico, independientemente del valor por defecto.

2.5.3. Variables de entorno

Terraform permite el uso de variables de entorno para la configuración. Se definen con la sintaxis TF_VAR_<variable>.

Por ejemplo, si se define una variable PASSWORD en Terraform, se puede acceder a ella en la shell como TF_VAR_PASSWORD. Terraform la reconocerá como PASSWORD.

$ export TF_VAR_PASSWORD=xxxx

Posterirmente, se accede a la variable en Terraform como var.PASSWORD.

provider "openstack" {
  user_name   = var.openstack_user_name
  tenant_name = var.openstack_tenant_name
  password    = var.PASSWORD (1)
  auth_url    = var.openstack_auth_url
}
  1. Uso de la variable

La variable PASSWORD no tiene por qué estar definida en el archivo variables.tf. Terraform la reconocerá como PASSWORD. Además, Terraform no la incluirá en el archivo de estado. Esto es muy útil para almacenar valores sensibles como contraseñas o claves de acceso.

Warning

Configurar contraseñas en variables de entorno es una buena práctica de seguridad. Por contra, almacenar contraseñas en archivos de configuración es una mala práctica, ya que si se suben al repositorio de código quedan expuestas y además se almacenan en el archivo de estado de Terraform, lo que puede ser un problema de seguridad.

2.5.4. Variables de salida

Las variables de salida se usan para pasar valores a otros módulos o para mostrar en el CLI un resultado tras un despliegue con terraform apply.

Las variables de salida se definen con bloques output y un identificador único. Normalmente, toman como valor una expresión (p.e. una IP generada para una instancia creada).

output tf_vm_Floating_IP {
  value      = openstack_networking_floatingip_v2.tf_vm_ip.address (1)
  depends_on = [openstack_networking_floatingip_v2.tf_vm_ip] (2)
}
  1. Expresión que devuelve la dirección IP de un recurso previamente creado.

  2. Argumento opcional que establece una dependencia con un recurso creado.

2.6. Archivo de estado

Terraform guarda la información de la infraestructura creada en un archivo de estado Terraform (terraform.tfstate). Este archivo se usa al ejecutar los comandos terraform plan o terraform apply para determinar los cambios a aplicar. Gracias a esto se puede:

  • Seguir la pista de los cambios en la infraestructura

  • Actualizar sólo los componentes necesarios

  • Eliminar componentes

Una característica muy interesante de Terraform es la idempotencia, así como la facilidad para aplicar cambios. Si volvemos a ejecutar un despliegue con terraform apply y no ha habido cambios en los archivos de configuración tras el último despliegue (cuyo estado quedó almacenado en el archivo .tfstate), el despliegue quedará intacto. Es decir, no se volverá a crear infraestructura repetida, ni se reemplazará la infraestructura creada por una nueva si no hay cambios en los archivos de configuración.

Sin embargo, si modificamos la configuración modificando los archivos Terraform estaremos indicando un nuevo estado al que queremos llegar. En este caso, al aplicar terraform apply sí se desplegarán los cambios realizados en la configuración. Sin embargo, sólo se desplegarán los recursos correspondientes a los cambios realizados, manteniendo intacta la configuración no modificada.

Atención al archivo de estado

El archivo de estado puede contener información sensible por lo que debe quedar excluido en el sistema de control de versiones.

Tip

Recuerda incluir el archivo de estado en .gitignore.

Además, el estado local no funciona bien en un entorno colaborativo, ya que la ejecución local almacenaría el estado en cada equipo local y no coincidirá con el estado almacenado en otro equipo de otro miembro. Si dos o más personas necesitan ejecutar la configuración Terraform, se necesita almacenar el archivo en un lugar remoto común a fin de evitar errores y no dañar la infraestructura existente.

Más información sobre estado remoto y configuración de backends.

Note

Google Cloud Storage ofrece soporte para el almacenamiento del estado de Terraform con la opción de bloqueo. Crea un segmento (bucket) y activa el versionado de objetos para recuperación de estados anteriores ante errores accidentales.

Terraform también permite usar una base de datos PostgreSQL para el almacenamiento del estado con la opción de bloqueo. Aprovisiona una máquina virtual con SQL o usa un servicio de PostgreSQL en la nube para el almacenamiento de estado en PostgreSQL.

Actualmente. Terraform da una lista bastante amplia de backends para almacenamiento de estado

2.7. Gestión de la infraestructura

Normalmente, estos son los pasos que se deben seguir para construir, mantener y eliminar una infraestructura con Terraform.

  1. Inicializar el directorio del proyecto Terraform (terraform init). El comando descarga todos los componentes necesarios, incluyendo módulos y plugins. La inicialización crea un archivo .terraform en el directorio de trabajo con los plugins necesarios. La información necesaria sobre los plugins y proveedores a descargar se suele encontrar en el archivo providers.tf.

  2. Crear un plan de ejecución (terraform plan). El comando determina las acciones necesarias para alcanzar el estado deseado especificado en los archivos de configuración (p.e. main.tf).

  3. Crear o modificar la infraestructura (terraform apply). Terraform es idempotente. Al usar este comando sólo se despliegan los recursos correspondientes a los cambios que se hayan realizado en los archivos de configuración (p.e. main.tf), sin volver a crear lo que ya existe y no se ha modificado. Para esto, Terraform se basa en lo almacenado en los archivos de estado, que guardan la información de la infraestructura creada en el último despliegue.

  4. Mostrar las variables de salida de un despliegue (terraform output).

  5. Eliminar la infraestructura (terraform destroy). Se usa para eliminar la infraestructura creada.

Note

Es posible que en algún momento se produzca un fallo en un despliegue. Por ejemplo, se realiza un despliegue de una infraestructura y se produce un error por falta de recursos. En una situación como esta, Terraform no puede deshacer los cambios realizados y quizá no pueda eliminar los recursos creados. En este caso, se puede usar el comando terraform refresh para actualizar el estado de la infraestructura con la información real de los recursos creados. Esto reconciliará el estado de la infraestructura con la información real de los recursos creados. Posteriormente, se puede usar terraform destroy para eliminar la infraestructura.

3. Despliegue en OpenStack

El provider OpenStack permite crear configuraciones Terraform para desplegar infraestructura en OpenStack. Entre los recursos que podemos gestionar están:

  • Instancias

  • Credenciales

  • Imágenes

  • Redes

  • Almacenamiento de bloques

  • Almacenamiento NFS

  • Balanceadores de carga

3.1. Configuración del provider

Para usarlo hay que configurar sus parámetros de acceso (p.e. usuario, proyecto, endpoint, …​). Lo haremos en un archivo providers.tf. El archivo providers.tf se usa para definir y configurar los proveedores de los recursos del módulo.

El archivo providers.tf
terraform {
  required_version = ">= 0.14.0"
  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 1.53.0"
    }
  }
}

provider "openstack" {
  user_name   = var.openstack_user_name
  tenant_name = var.openstack_tenant_name
  password    = var.PASSWORD (1)
  auth_url    = var.openstack_auth_url
}
  1. La contraseña se accede a través de la variable de entorno TF_VAR_PASSWORD para evitar almacenarla en el archivo de configuración y en el archivo de estado. Esto es una buena práctica de seguridad.

Se usan las variables definidas en el archivo variables.tf

variable "openstack_user_name" {
    description = "The username for the Tenant."
    default  = "your-openstack-user"
}

variable "PASSWORD" {
    description = "The user password."
}

variable "openstack_tenant_name" {
    description = "The name of the Tenant."
    default  = "your-openstack-project"
}

variable "openstack_auth_url" {
    description = "The endpoint url to connect to OpenStack."
    default  = "https://openstack.di.ual.es:5000/v3"
}

variable "openstack_keypair" {
    description = "The keypair to be used."
    default  = "your-openstack-keypair-name"
}
Uso de variables de entorno

Para evitar introducir datos sensibles en los archivos de configuración y evitar que queden expuestos en el sistema de control de versiones es buena práctica configurar valores sensibles en variables de entorno.

El convenio de Terraform es que definamos en la shell las variables precedidas de TF_VAR_. Por ejemplo, definimos una variable de entorno TF_VAR_PASSWORD que será accedida por Terraform como PASSWORD.

Table 1. Nomemclatura de variables de entorno
Variable de entorno Variable Terraform

TF_VAR_PASSWORD

PASSWORD

Seguiremos estos pasos:

  1. Configurar la variables en la shell

    $ export TF_VAR_PASSWORD=xxxx
  2. Cargar la variable en Terraform

    Archivo variables.tf
    ...
    variable "PASSWORD" {} (1)
    ...
    1. La variable de entorno TF_VAR_PASSWORD es reconocida en Terraform como PASSWORD

  3. Usar la variable en Terraform

    Archivo providers.tf
    provider "openstack" {
      user_name   = var.openstack_user_name
      tenant_name = var.openstack_tenant_name
      password    = var.PASSWORD (1)
      auth_url    = var.openstack_auth_url
    }
    1. Uso de la variable

3.2. Inicializar el provider

Para inicializar ejecutar terraform init.

terraform init

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of terraform-provider-openstack/openstack from the dependency lock file
- Using previously-installed terraform-provider-openstack/openstack v1.53.0

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
(base) MacBook-Pro-de-Manuel:00-pruebas-carga manolo$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding terraform-provider-openstack/openstack versions matching "~> 1.53.0"...
- Installing terraform-provider-openstack/openstack v1.53.0...
- Installed terraform-provider-openstack/openstack v1.53.0 (self-signed, key ID 4F80527A391BEFD2)

Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

...

Terraform has been successfully initialized!

...

Esto creará una carpeta .terraform con en plugin de OpenStack instalado y disponible para ser usado en el proyecto. También crea un archivo .terraform.lock.hcl que registra las selecciones de proveedores realizadas. Este archivo se debe incluir en el repositorio de control de versiones para garantizar que Terraform haga las mismas selecciones por defecto cuando se ejecute terraform init en el futuro.

Actualización de la configuración

Con el paso del tiempo, puede que haya que actualizar la configuración de Terraform. La actualuización comprendería módulos, plugins y proveedores. Para ello, ejecutar terraform init -upgrade en el directorio del proyecto.

3.3. Despliegue de una instancia

La creación de una instancia se realiza con openstack_compute_instance_v2.

A continuación, crearemos una instancia denominada tf_vm. Cada recurso creado en Terraform se identifica con un nombre. En este caso, el nombre del recurso es tf_vm. Es el nombre que se use en resource, no el nombre asignado en name, es el que referencia al objeto resource creado. Esto permite tratar el recurso creado (p.e. para asignarle una dirección IP flotante, para conectarle un volumen, …​).

En el ejemplo siguiente se ilustra la creación de una máquina virtual, una dirección IP flotante (openstack_networking_floatingip_v2) y la asignación de la IP flotante a la máquina virtual creada (openstack_compute_floatingip_associate_v2).

#Crear nodo tf_vm
resource "openstack_compute_instance_v2" "tf_vm" {(1)
  name              = "tf_vm"
  image_name        = "jammy"
  availability_zone = "nova"
  flavor_name       = "medium"
  key_pair          = var.openstack_keypair
  security_groups   = ["default"]
  network {
    name = var.openstack_network_name (2)
  }
}

resource "openstack_networking_floatingip_v2" "tf_vm_ip" { (3)
  pool = "ext-net"
}

resource "openstack_compute_floatingip_associate_v2" "tf_vm_ip" { (4)
  floating_ip = openstack_networking_floatingip_v2.tf_vm_ip.address (5)
  instance_id = openstack_compute_instance_v2.tf_vm.id (6)
}

output tf_vm_Floating_IP {
  value      = openstack_networking_floatingip_v2.tf_vm_ip.address (7)
  depends_on = [openstack_networking_floatingip_v2.tf_vm_ip] (8)
}
  1. Creación de un recurso instancia (máquina virtual) en OpenStack. El objeto recurso creado es asignado a la variable tf_vm.

  2. Red a la que se conectará la instancia creada. Usamos una variable de entrada almacenada en variables.tf con el nombre de la red.

  3. Creación de un recurso dirección IP flotante. El objeto recurso creado es asignado a la variable tf_vm_ip.

  4. Asociación de la IP flotante a la instancia

  5. Acceso a la dirección del recurso IP flotante creado

  6. Acceso al id la instancia creada

  7. Acceso a la dirección del recurso IP flotante creado

  8. Esperar a que esté creado el recurso de la IP flotante

La creación de la instancia, igual que los demás recursos, tiene un configuración específica. En este caso, se crea una instancia con las siguientes características destacadas:

  • Nombre tf_vm

  • Imagen jammy. Así es como se conoce a la imagen de Ubuntu 22.04 en OpenStack-DI.

  • Zona de disponibilidad nova. Es el nombre de la zona de disponibilidad en OpenStack-DI. Una zona de disponibilidad es un conjunto de recursos de cómputo y almacenamiento que se encuentran en un solo centro de datos o en varios centros de datos cercanos.

  • Sabor medium. Es el tamaño de la instancia. En OpenStack-DI, el tamaño medium es una instancia con 2 vCPUs y 4 GB de RAM.

Para desplegar la infraestructura, ejecutar terraform apply. Terraform mostrará un resumen de los cambios a realizar y pedirá confirmación para aplicarlos. Si la variable de entorno TF_VAR_PASSWORD no está definida, Terraform la solicitará. Tras confirmar, Terraform creará la infraestructura. Como resultado, se mostrará la dirección IP flotante asignada a la instancia creada.

La figura siguiente ilustra la instancia creada en OpenStack-DI con la dirección IP flotante asignada.

Terraform OpenStack instance

Si ya no necesitamos la infraestructura creada, podemos eliminarla con terraform destroy. Terraform mostrará un resumen de los cambios a realizar y pedirá confirmación para aplicarlos. Tras confirmar, Terraform eliminará la infraestructura.

3.4. Modificar el despliegue

La modificación de un despliegue se realiza modificando los archivos de configuración Terraform y ejecutando terraform apply. Terraform detectará los cambios y mostrará un resumen de los cambios a realizar. Tras confirmar, Terraform aplicará los cambios.

A modo de ilustración, este ejemplo muestra cómo aplicar cambios a una configuración desplegada previamente. En este caso se trata de:

resource "openstack_compute_instance_v2" "tf_vm" {
  name              = "tf_vm"
  image_name        = "jammy"
  availability_zone = "nova"
  flavor_name       = "large" (1)
  key_pair          = var.openstack_keypair
  security_groups   = ["default"]
  network {
    name = var.openstack_network_name
  }
}

...

resource "openstack_blockstorage_volume_v3" "tf_vol" { (2)
  name        = "tf_vol"
  description = "first test volume"
  size        = 1 (3)
}

resource "openstack_compute_volume_attach_v2" "va_1" { (4)
  instance_id = "${openstack_compute_instance_v2.tf_vm.id}" (5)
  volume_id   = "${openstack_blockstorage_volume_v3.tf_vol.id}" (6)
}
  1. Modificación del sabor de la imagen

  2. Creación de un recurso volumen

  3. Especificación del tamaño del volumen

  4. Conexión del volumen a la instancia

  5. Acceso al id la instancia

  6. Acceso al id del volumen creado

Al ejecutar con terraform apply, Terraform nos informará de los cambios detectados y de la nueva configuración. La nueva configuración se aplicará si confirmamos la operación. Una vez aplicados desplegados los cambios, los recursos creados se mostrarán en el panel de control de OpenStack-DI, mostrando la instancia modificada y el volumen creado y conectado a la instancia. La figura siguiente ilustra el volumen creado y conectado a la instancia.

Terraform OpenStack volume

3.5. Ejecutar un script de inicialización

Una característica muy interesante en el despliegue de una instancia es la posibilidad de ejecutar un script de inicialización durante su creación. Esto permite la creación de instancias con paquetes instalados y configurados.

Terraform permite esta operación en OpenStack pasando un script en el parámetro user_data al crear la instancia.

Note

Si se modifica el valor de user_data se creará un nuevo servidor si se usa terraform apply.

A continuación se muestra un script install_mysql.sh que configura una base de datos MySQL inicializada con una base de datos de ejemplo. El script realiza las siguientes operaciones:

  • Actualizar el repositorio de paquetes.

  • Instalar un servidor MySQL con el password my_password.

  • Descargar un archivo con un script SQL para inicializar una base de datos de ejemplo.

  • Ejecutar el archivo SQL para inicializar la base de datos. La inicialización consiste en la creación de una base de datos denominada SG (Sporting Goods), la creación de una tabla denominada s_customers, la inserción de datos en la tabla y la creación de un usuario SG con permisos sobre la base de datos.

  • Modificar el archivo de configuración de MySQL (mysqld.cnf) para que admita conexiones desde cualquier lugar.

El script install_mysql.sh
#!/bin/bash

sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password password my_password'
sudo debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password my_password'
sudo apt update
sudo apt -y install mysql-server
wget https://gist.githubusercontent.com/ualmtorres/f8d0e5ea79a0e570f495087724288c6d/raw/0a894b23466bb6eea520a05559372e148e6e5803/sginit.sql -O /home/ubuntu/sginit.sql
mysql -h "localhost" -u "root" "-pmy_password" < "/home/ubuntu/sginit.sql"

sudo sed -i 's/127.0.0.1/0.0.0.0/g' /etc/mysql/mysql.conf.d/mysqld.cnf
sudo service mysql restart

Para crear la instancia con Terraform basta con crear el recurso pasando a la propiedad user_data el nombre y la ruta del script de inicialización. En este caso, se supone que el script de inicialización está en el mismo directorio que el script Terraform.

#Crear nodo mysql
resource "openstack_compute_instance_v2" "mysql" {
  name              = "mysql"
  image_name        = "jammy"
  availability_zone = "nova"
  flavor_name       = "medium"
  key_pair          = var.openstack_keypair
  security_groups   = ["default"]
  network {
    name = var.openstack_network_name
  }
  user_data = file("install_mysql.sh") (1)
}
  1. Pasar el script de inicialización de la instancia

Tras ejecutar terraform apply, Terraform creará la instancia con el script de inicialización. El script se ejecutará durante la creación de la instancia. La instancia creada tendrá un servidor MySQL instalado y configurado con la base de datos SG inicializada.

3.6. Archivos de plantilla

Una característica muy interesante de Terraform es la posibilidad de definir scripts con contenido dinámico. Se trata de archivos que interpolan el valor de variables generadas durante el proceso de despliegue.

El procedimiento es el siguiente:

  • Generar variables de salida

  • Crear archivos de plantilla con extensión .tpl que obtengan los valores de dichas variables con la sintaxis siguiente ${nombre-variable}.

  • Interpolar mediante la función templatefile donde sea necesario los archivos plantilla con la sintaxis siguiente data.template_file.objeto-template-file.rendered.

Para ilustrar su uso:

  • Crearemos una plantilla que obtenga (también llamado interpole) la dirección IP de un servidor MySQL creado en el despliegue (almacenada en una variable output). Dicha variable se usará para definir una variable de entorno en la instancia definida y para cambiar las variables de entorno de Apache.

  • Crearemos una instancia inicializada con el archivo de la plantilla. La instancia será un servidor web inicializado con una aplicación PHP sencilla. La aplicación usará la variable de entorno inicializada por el script. La variable de entorno contiene la dirección IP del servidor MySQL al que accede la aplicación para mostrar sus datos.

Archivo plantilla install_appserver.tpl
#!/bin/bash
echo "export MYSQL_SERVER=${mysql_ip}" >> /home/ubuntu/.profile (1)

sudo apt-get update
sudo apt-get install -y apache2 php php-mysql libapache2-mod-php php-mcrypt
sudo chgrp -R www-data /var/www
sudo chmod -R 775 /var/www
sudo chmod -R g+s /var/www
sudo useradd -G www-data ubuntu
sudo chown -R ubuntu /var/www/

sudo rm /var/www/html/index.html
wget https://gist.githubusercontent.com/ualmtorres/1c833f9b471fa7351e2725731596f45e/raw/a66b26d90b5f75c3a37cfe12a2370b57d2768132/sginit.php -O /var/www/html/index.php

echo "export MYSQL_SERVER=${mysql_ip}" >> /etc/apache2/envvars (2)
sudo service apache2 restart
  1. Inicialización de una variable de entorno con el valor de la variable mysql_ip.

  2. Inicialización de una variable de entorno Apache con el valor de la variable mysql_ip.

Creación del recurso con el script de inicialización interpolado
#Crear nodo appserver
resource "openstack_compute_instance_v2" "appserver" {
  name              = "appserver"
  image_name        = "Ubuntu 16.04 LTS"
  availability_zone = "nova"
  flavor_name       = "medium"
  key_pair          = "mtorres_ual"
  security_groups   = ["default"]
  network {
    name = "desarrollo-net"
  }

    user_data = templatefile("${path.module}/install_appserver.tpl", { mysql_ip = openstack_compute_instance_v2.mysql.network.0.fixed_ip_v4 }) (1)

  depends_on = [openstack_compute_instance_v2.mysql]

}
  1. Interpolación del archivo plantilla

El proceso de interpolación con la función templatefile se realiza en el momento de la creación de la instancia. Terraform sustituye las variables de la plantilla por los valores de las variables de salida generadas durante el despliegue. La función toma dos argumentos: la ruta del archivo plantilla y un mapa con las variables a interpolar.

3.7. Ejemplo completo

En este apartado crearemos un escenario más complejo que combine creación de recursos de red e instancias aprovisionadas durante su creación.

Se trata de crear lo siguiente:

  • Red denominada desarrollo-net. Contendrá una subred denominada desarrollo-subnet con direcciones 10.2.0.0./24 y estos servidores DNS: 150.214.156.2 8.8.8.8.

  • Router denominado desarrollo-router que conecte la red exterior ext-net con la red desarrollo-net creada anteriormente.

  • Un servidor MySQL inicializado con el script install_mysql.sh

  • Un servidor Web con PHP inicializado con el script install_appserver.tpl

La figura siguiente ilustra el diagrama de la infraestructura.

EjemploCompleto

Tras finalizar el despliegue tendremos la configuración de red realizada, un servidor MySQL con una base de datos inicializada y servidor web con aplicación PHP de catálogo de productos desplegada. Terraform nos informará con las variables de salida.

Apply complete! Resources: 10 added, 0 changed, 0 destroyed.

Outputs:

Appserver_Floating_IP = 192.168.68.112
MySQL_Floating_IP = 192.168.68.135

Si accedemos a la dirección IP del servidor web veremos la aplicación de catálogo mostrando los productos almacenados en la base de datos.

SGApp

4. Despliegue en Google Cloud

El provider Google Cloud permite crear configuraciones Terraform para desplegar configuraciones en el gran conjunto de servicios de Google Cloud. Entre los recursos que podemos gestionar están:

  • Infraestructura (Instancias, Imágenes, Redes, …​)

  • App Engine

  • Bases de datos (Cloud SQL, Big Query, Firebase, …​)

  • Kubernetes

  • Cloud Storage

  • …​

4.1. Crear una clave para la Cuenta de servicio

  • Seleccionar el proyecto Google Cloud.

  • En el menú de navegación seleccionar IAM y administración | Cuentas de servicio.

  • Seleccionar Crear cuenta de servicio.

  • Darle un nombre (p.e. terraform)

  • Seleccionar Crear y continuar.

  • En el paso Otorga a esta cuenta de servicio acceso al proyecto del asistente, seleccionar el rol Proyecto → Editor.

  • Pulsar el botón Listo. No es necesario configurar nada más en este asistente.

  • Editar la Cuenta de servicio. En la sección Claves seleccionar Agregar clave | Crear clave nueva.

  • Dejar JSON en el tipo de clave..

  • Seleccionar Crear. A continuación se descargará a nuestro equipo la clave privada.

  • En el menú de navegación seleccionar IAM y adminsitración | IAM, en la pestaña de Permisos localizar la cuenta de servicio creada para terraform y pulsar sobre Editar cuenta principal.

  • Pulsar sobre Agregar otro rol. Seleccionar Servicios de red - Administrador de extensiones del servicio.

  • Guardar los cambios.

4.2. Configuración del provider

Para usarlo hay que configurar sus parámetros de acceso. Lo haremos en un archivo providers.tf

El archivo providers.tf
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "6.5.0"
    }
  }
}

provider "google" {
  credentials = file("../gcp-identity.json") (1)

  project = var.gcp-project
  region  = "us-central1"
  zone    = "us-central1-c"
}
  1. Ruta al archivo de credenciales de la cuenta de servicio descargadas en el paso anterior.

Se usan las variables definidas en el archivo variables.tf

variable "gcp-username" {
  description = "GCP user name"
  default     = "mtorres"
}

variable "gcp-project" {
  description = "GCP project"
  default     = "cc2025-mtorres"
}

variable "gcp-network" {
  description = "GCP network"
  default     = "terraform-network"
}

4.3. Inicializar el provider

Para inicializar ejecutar terraform init.

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/google versions matching "6.5.0"...
- Installing hashicorp/google v6.5.0...
- Installed hashicorp/google v6.5.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Esto creará una carpeta .terraform con en plugin de Google Cloud instalado y disponible para ser usado en el proyecto. También crea un archivo .terraform.lock.hcl que registra las selecciones de proveedores realizadas. Este archivo se debe incluir en el repositorio de control de versiones para garantizar que Terraform haga las mismas selecciones por defecto cuando se ejecute terraform init en el futuro.

4.4. Configuración de la red

Para crear una red en Google Cloud usaremos el recurso google_compute_network. En el siguiente ejemplo se crea una red denominada terraform-network.

resource "google_compute_network" "vpc_network" {
  name = var.gcp-network
}

También crearemos las reglas de firewall para permitir el tráfico de entrada y salida en la red. Para ello usaremos el recurso google_compute_firewall. En este ejemplo veremos cómo añadir una regla ICMP que permita el tráfico PING desde cualquier origen, una regla SSH que permita el tráfico SSH desde cualquier origen y una regla que permite todo el tráfico interno. El tráfico interno lo entenderemos dentro de la región us-central1 con máscara de red 10.128.0.0/20.

resource "google_compute_network" "vpc_network" {
  name = var.gcp-network
}

resource "google_compute_firewall" "firewall-icmp" {
  name    = "terraform-allow-icmp"
  network = google_compute_network.vpc_network.name

  allow {
    protocol = "icmp"
  }

  source_ranges = ["0.0.0.0/0"]
}

resource "google_compute_firewall" "firewall-ssh" {
  name    = "terraform-allow-ssh"
  network = google_compute_network.vpc_network.name

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  source_ranges = ["0.0.0.0/0"]
}

resource "google_compute_firewall" "firewall-internal" {
  name    = "terraform-allow-internal"
  network = google_compute_network.vpc_network.name

  allow {
    protocol = "tcp"
    ports    = ["0-65535"]
  }

  allow {
    protocol = "udp"
    ports    = ["0-65535"]
  }

  allow {
    protocol = "icmp"
  }

  source_ranges = ["10.128.0.0/20"]
}

4.5. Despliegue de una instancia

La creación de una instancia se realiza con google_compute_instance.

A continuación, crearemos una instancia denominada tf-vm. El nombre que se use en resource, no el nombre asignado en name, es el que referencia al objeto resource creado. Esto permite tratar el recurso creado (p.e. para asignarle una dirección IP externa, para conectarle un volumen, …​).

En el ejemplo siguiente se ilustra la creación de una máquina virtual con una dirección IP efímera.

Note

De forma predeterminada, si no se indica ninguna dirección IP fija, Google Cloud creará una efímera para la máquina virtual.

resource "google_compute_instance" "tf-vm" { (1)
  name         = "tf-vm"
  zone         = "us-central1-c"
  machine_type = "n1-standard-1"
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }

  # Add SSH access to the Compute Engine instance
  metadata = {
    ssh-keys = "${var.gcp-username}:${file("~/.ssh/id_rsa.pub")}"
  }

  # Startup script
  # metadata_startup_script = "${file("update-docker.sh")}"

  network_interface { (2)
    network    = var.gcp-network
    subnetwork = var.gcp-network

    access_config {} (3)
  }
}

output "tf-vm-internal-ip" { (4)
  value      = google_compute_instance.tf-vm.network_interface.0.network_ip
  depends_on = [google_compute_instance.tf-vm]
}

output "tf-vm-ephemeral-ip" { (5)
  value      = google_compute_instance.tf-vm.network_interface.0.access_config.0.nat_ip
  depends_on = [google_compute_instance.tf-vm]
}
  1. Creación de un recurso instancia (máquina virtual) en Google Cloud. El objeto recurso creado es asignado a la variable tf-vm.

  2. Red a la que se conectará la instancia creada.

  3. Dejar access_config sin configurar hará que se genere una dirección IP efímera.

  4. Dirección IP interna de la instancia

  5. Dirección IP efímera de la instancia

4.6. Modificar el despliegue

A modo de ilustración este ejemplo muestra cómo aplicar cambios a una configuración desplegada previamente. En este caso se trata de:

resource "google_compute_instance" "tf-vm" {
  name         = "tf-vm"
  zone         = "us-central1-c"
  machine_type = "n1-standard-2" (1)
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }
...
resource "google_compute_disk" "tf-disk" { (2)
  name = "tf-disk"
  type = "pd-ssd" (3)
  size = 1 (4)
}

resource "google_compute_attached_disk" "attached-tf-disk" {(5)
  disk     = google_compute_disk.tf-disk.id (6)
  instance = google_compute_instance.tf-vm.id (7)
}
  1. Modificación del tamaño de la imagen

  2. Creación de un recurso volumen

  3. Tipo SSD

  4. Especificación del tamaño del volumen

  5. Conexión del volumen a la instancia

  6. Acceso al id del volumen creado

  7. Acceso al id de la instancia

Al ejecutar con terraform apply, Terraform nos informará de los cambios detectados y de la nueva configuración. La nueva configuración se aplicará si confirmamos la operación. Una vez aplicados desplegados los cambios, los recursos creados se mostrarán en el panel de control de Google Cloud, mostrando la instancia modificada y el volumen creado y conectado a la instancia. Si nos conectamos a la instancia con ssh podremos ver el volumen creado con lsblk.

$ lsblk
NAME    MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda       8:0    0   10G  0 disk (1)
├─sda1    8:1    0  9.9G  0 part /
├─sda14   8:14   0    3M  0 part
└─sda15   8:15   0  124M  0 part /boot/efi
sdb       8:16   0    1G  0 disk (2)
  1. Disco de la instancia

  2. Volúmen creado y conectado a la instancia

4.7. Creación de una instancia con una dirección IP estática

De forma predeterminada, Google Cloud crea una dirección IP efímera para las instancias. Si queremos una dirección IP estática, debemos crearla y asignarla a la instancia. Para ello, usaremos el recurso google_compute_address. En el siguiente ejemplo se crea una dirección IP estática denominada tf-vm-ip. A la hora de crear la instancia, una forma de asignar la dirección IP estática creada es a través de la configuración access_config de la tarjeta de red de la instancia.

resource "google_compute_address" "tf-vm-ip" { (1)
  name = "ipv4-address-tf-vm"
}

resource "google_compute_instance" "tf-vm" { (2)
  name         = "tf-vm"
  machine_type = "n1-standard-1"
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }

...

  network_interface {
    network    = var.gcp-network
    subnetwork = var.gcp-network

    access_config {
      nat_ip = google_compute_address.tf-vm-ip.address (3)
    }
  }
}

output "tf-vm-ip" { (4)
  value      = google_compute_address.tf-vm-ip.address
  depends_on = [google_compute_instance.tf-vm]
}
  1. Creación de un recurso dirección IP estática

  2. Creación de un recurso instancia (máquina virtual) en Google Cloud

  3. Asignación de la dirección IP estática a la instancia

  4. Dirección IP estática de la instancia

4.8. Ejecutar un script de inicialización

Una característica muy interesante en el despliegue de una instancia es la posibilidad de ejecutar un script de inicialización durante su creación. Esto permite la creación de instancias con paquetes instalados y configurados.

Terraform permite esta operación en GCP pasando un script en el parámetro metadata_startup_script al crear la instancia.

Note

Si se modifica el valor de metadata_startup_script se creará un nuevo servidor si se usa terraform apply.

En este apartado veremos cómo crear una instancia Ubuntu aprovisionada con Docker. Además, la instancia se inicializará con un archivo docker-compose.yml que despliega dos contenedores: un contenedor MySQL con una base de datos inicializada y otro contenedor con una aplicación PHP que muestra un catálogo de productos almacenados en el contenedor MySQL.

Note

El script de instalación es válido para Ubuntu. Si se usan otras otras distribuciones Linux será necesario adaptar el script de instalación a las peculiaridades de la distribución utilizada.

La aplicación deberá ser accesible en Internet. Por tanto, hay que definir una regla en el cortafuegos que permita la comunicación HTTP. La regla tendrá una etiqueta asociada. Las instancias que deseen aplicar la regla incluirán la etiqueta correspondiente en su definición.

El archivo network-firewall.tf
# allow http traffic
resource "google_compute_firewall" "allow-http" {
  name    = "tf-fw-allow-http" (1)
  network = var.gcp-network (2)
  allow {
    protocol = "tcp"
    ports    = ["80"] (3)
  }
  target_tags   = ["http"] (4)
  source_ranges = ["0.0.0.0/0"] (5)
}
  1. Nombre de la regla del firewall

  2. Red a la que se aplica la regla definida

  3. Puerto abierto

  4. Etiqueta para poder usar la regla

  5. Rango de direcciones IP permitidas. En este caso, cualquier dirección IP

El archivo main.tf
resource "google_compute_instance" "tf-vm" {
  name         = "tf-vm"
  zone         = "us-central1-c"
  machine_type = "n1-standard-1"
  boot_disk {
    initialize_params {
      image = "ubuntu-os-cloud/ubuntu-2204-lts" (1)
    }
  }

  # Add SSH access to the Compute Engine instance
  metadata = {
    ssh-keys = "${var.gcp-username}:${file("~/.ssh/id_rsa.pub")}"
  }

  # Add http tag to the instance to identify it in the firewall rule
  tags = ["http"] (2)

  # Startup script
  metadata_startup_script = file("setup-docker.sh") (3)

  network_interface {
    network    = var.gcp-network
    subnetwork = var.gcp-network

    access_config {}
  }
}

output "tf-vm-internal-ip" {
  value      = google_compute_instance.tf-vm.network_interface.0.network_ip
  depends_on = [google_compute_instance.tf-vm]
}

output "tf-vm-ephemeral-ip" {
  value      = google_compute_instance.tf-vm.network_interface.0.access_config.0.nat_ip
  depends_on = [google_compute_instance.tf-vm]
}
  1. Imagen de la instancia

  2. Etiqueta para identificar la instancia en la regla del cortafuegos

  3. Script de inicialización de la instancia

El script setup-docker.sh de inicialización de la instancia
#!/bin/bash

echo "Instalando Docker"

# Add Docker's official GPG key:
apt-get update
apt-get install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  tee /etc/apt/sources.list.d/docker.list > /dev/null
apt-get update

apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable docker

git clone https://github.com/ualmtorres/docker_customer_catalog.git (1)
cd docker_customer_catalog
docker compose up -d (2)

exit 0
  1. Clonado del repositorio con el archivo de despliegue, la aplicación y el script de inicialización de la base de datos

  2. Despliegue del entorno (Base de datos + Aplicación)

Para crear la instancia con Terraform basta con crear el recurso pasando a la propiedad metadata_startup_script el nombre y la ruta del script de inicialización. En este caso, se supone que el script de inicialización está en el mismo directorio que el script Terraform.

La figura siguiente ilustra el resultado tras unos minutos que se necesitan para la creación e inicialización de la instancia y despliegue de la base de datos y la aplicación de catálogo.

CustomerCatalog
Note

Si estamos interesados en mostrar el log de arranque de la instancia para comprobar que el script de inicialización se ha ejecutado correctamente, podemos hacerlo desde la propia instancia ejecutando sudo journalctl -u google-startup-scripts.service.

5. Despliegue en Microsoft Azure

El provider Microsoft Azure permite crear configuraciones Terraform para desplegar configuraciones en el gran conjunto de servicios de Azure. Entre los recursos que podemos gestionar están:

  • Infraestructura (Instancias, Imágenes, Redes, …​)

  • App Service

  • Bases de datos (SQL, CosmosDB, …​)

  • Kubernetes

  • Storage

  • …​

5.1. Autenticación en Azure

Para autenticarse en Azure, Terraform necesita que se haya iniciado la sesión con el CLI de Azure y proporcionar las credenciales de la suscripción y del proyecto en Azure. Las credenciales se obtendrán a través del CLI de Azure.

Note

Azure CLI es una herramienta de línea de comandos que proporciona una experiencia unificada para administrar los servicios de Azure. Para instalarlo, seguir las instrucciones en Instalación de la CLI de Azure.

Para obtener las credenciales necesarias, seguir los siguientes pasos:

  1. Iniciar sesión en Azure con az login. Esto abrirá un navegador para autenticarse en Azure. Tras la autenticación, se mostrará un mensaje de confirmación en la terminal y devolverá los datos de la cuenta.

    Retrieving tenants and subscriptions for the selection...
    
    [Tenant and subscription selection]
    
    No     Subscription name    Subscription ID                       Tenant
    -----  -------------------  ------------------------------------  ----------------------
    [1] *  Azure for Students   00000000-0000-0000-0000-000000000000  University of XXXXXXX
  2. Seleccionar la suscripción y el proyecto con la que se desea trabajar. El listado aparecerá numerado. Introdcir el número correspondiente a la suscripción y al proyecto.

  3. Obtener los detalles de la suscripción con az account show.

    {
      "environmentName": "AzureCloud",
      "homeTenantId": "00000000-0000-0000-0000-000000000000",
      "id": "00000000-0000-0000-0000-000000000000", (1)
      "isDefault": true,
      "managedByTenants": [],
      "name": "Azure for Students",
      "state": "Enabled",
      "tenantDefaultDomain": "students.uxxxxxx.es",
      "tenantDisplayName": "University of XXXXXXX",
      "tenantId": "00000000-0000-0000-0000-000000000000", (2)
      "user": {
        "name": "robertsmith@ual.es",
        "type": "user"
      }
    }
    1. ID de la suscripción

    2. ID del proyecto

Los datos que necesitamos para configurar el provider de Azure en Terraform son los que hemos destacado en el listado anterior:

  • subscription_id: ID de la suscripción.

  • tenant_id: ID del proyecto.

5.2. Configuración del provider

Para usarlo hay que configurar sus parámetros de acceso. Lo haremos en un archivo providers.tf

El archivo providers.tf
# We strongly recommend using the required_providers block to set the
# Azure Provider source and version being used
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "=4.1.0"
    }
  }
}

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {}

  subscription_id = var.azure-subscription
  tenant_id       = var.azure-tenant

}

Se usan las variables definidas en el archivo variables.tf

variable "azure-subscription" {
  description = "Azure subscription id"
}

variable "azure-tenant" {
  description = "Azure tenant id"
}

variable "azure-resource-group" {
  description = "Azure resource group name"
}

variable "azure-location" {
  description = "Azure location"
}

Los valores de las variables se pueden definir en un archivo terraform.tfvars

El archivo terraform.tfvars
azure-subscription = "00000000-0000-0000-0000-000000000000"
azure-tenant       = "00000000-0000-0000-0000-000000000000"
azure-resource-group = "tf-resource-group"
azure-location = "France Central"
Important

El archivo terraform.tfvars no debe ser incluido en el control de versiones. Contiene información sensible.

5.3. Inicializar el provider

Para inicializar ejecutar terraform init.

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/azurerm versions matching "4.1.0"...
- Installing hashicorp/azurerm v4.1.0...
- Installed hashicorp/azurerm v4.1.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Esto creará una carpeta .terraform con en plugin de Azure instalado y disponible para ser usado en el proyecto. También crea un archivo .terraform.lock.hcl que registra las selecciones de proveedores realizadas. Este archivo se debe incluir en el repositorio de control de versiones para garantizar que Terraform haga las mismas selecciones por defecto cuando se ejecute terraform init en el futuro.

5.4. Creación de un grupo de recursos

Un grupo de recursos es un contenedor que mantiene los recursos relacionados para una solución de Azure. Los recursos pueden incluir instancias, bases de datos, redes, etc. Los recursos de un grupo de recursos pueden ser administrados, eliminados o actualizados en conjunto. Para crear un grupo de recursos usaremos el recurso azurerm_resource_group.

En el siguiente ejemplo se crea un grupo de recursos denominado tf-resource-group en la región France Central, que es la región que hemos definido en el archivo terraform.tfvars.

resource "azurerm_resource_group" "tf-resource-group" {
  name     = var.azure-resource-group
  location = var.azure-location
}

Desplegando con terraform apply se creará el grupo de recursos en Azure.

5.5. Configuración de la red

La configuración de una red en Azure pasa por la creación de una red virtual y una subred. Es posible crear la subred directamente en la creación de la red virtual. Sin embargo, es recomendable crear la red y la subred por separado para tener un mayor control sobre la configuración de la red. Posteriormente, necesitaremos crear una interfaz de red para conectar la instancia a la red. Esta interfaz de red deberá estar conectada a la subred. Por tanto, necesitamos que la subred sea un recurso independiente para poder usarlo en la creación de la interfaz de red. Así que, resumiendo, crearemos la red y la subred por separado.

Para crear una red en Azure usaremos el recurso azurerm_virtual_network. Para crear una subred usaremos el recurso azurerm_subnet. En el siguiente ejemplo se crea una red denominada tf-net y una subred denominada tf-subnet, ambas en la región France Central con el rangos de direcciones 10.0.0.0/24.

resource "azurerm_virtual_network" "tf-net" {
  name                = var.azure-net-name
  location            = azurerm_resource_group.tf-resource-group.location
  resource_group_name = azurerm_resource_group.tf-resource-group.name
  address_space       = var.azure-address-space
  dns_servers         = var.azure-dns-servers
}

resource "azurerm_subnet" "tf-subnet" {
  name                 = var.azure-subnet-name
  resource_group_name  = azurerm_resource_group.tf-resource-group.name
  virtual_network_name = azurerm_virtual_network.tf-net.name
  address_prefixes     = var.azure-subnet-prefixes
}

El archivo variables.tf tendrá que ser modificado para incluir las variables necesarias para la creación de la red.

...
# Nuevo contenido

variable "azure-net-name" {
  description = "Azure virtual net name"
}

variable "azure-address-space" {
  description = "Azure address space"
  type        = list(string)
}

variable "azure-dns-servers" {
  description = "Azure DNS servers"
  type        = list(string)
}

variable "azure-subnet-name" {
  description = "Azure subnet name"
}

variable "azure-subnet-prefixes" {
  description = "Azure subnet prefixes"
  type        = list(string)
}

El archivo terraform.tfvars tendrá que ser modificado para incluir los valores de las variables necesarias para la creación de la red.

...
# Nuevo contenido

azure-address-space        = ["10.0.0.0/24"]
azure-dns-servers          = ["8.8.8.8"]
azure-subnet-name          = "tf-subnet"
azure-subnet-prefixes      = ["10.0.0.0/24"]

Desplegando con terraform apply se crearán en Azure la red y la subred.

5.6. Creación de una instancia

En esta sección vamos a crear una instancia en Azure configurada en el arranque con un servidor web Apache. Además, crearemos un disco de datos que se conectará a la instancia. La instancia se creará en la red creada anteriormente. En Azure, la conexión de una instancia a la red se realizar a través de un recurso denominado interfaz de red. Por tanto, necesitaremos crear una interfaz de red para conectar la instancia a la red. En cuanto al acceso a la instancia, se permitirá el acceso a través del puerto 22 para SSH y del puerto 80 para HTTP. Esto exige crear un grupo de seguridad de red que permita el tráfico a través de estos puertos. Además, habrá que crear una dirección IP pública y asignarla a la instancia. A continuación se muestran los pasos a seguir para crear la instancia;

  1. Crear un grupo de seguridad de red que permita el tráfico a través de los puertos 22 y 80.

  2. Crear una dirección IP pública.

  3. Crear una interfaz de red conectada a la red y configurada con la dirección IP pública.

  4. Aplicar el grupo de seguridad de red a la interfaz de red.

  5. Crear la instancia conectada a la interfaz de red. La instancia se inicializará con un script de arranque que instalará y configurará el servidor web Apache.

  6. Crear un disco de datos y conectarlo a la instancia.

5.6.1. Creación de un grupo de seguridad de red

Un grupo de seguridad de red es un conjunto de reglas que permiten o deniegan el tráfico de red a las instancias conectadas a la red. Para crear un grupo de seguridad de red usaremos el recurso azurerm_network_security_group. En el siguiente ejemplo se crea un grupo de seguridad de red denominado tf-nsg que permite el tráfico a través de los puertos 22 y 80.

...
# Nuevo contenido

resource "azurerm_network_security_group" "tf-nsg" {
  name                = "tf-nsg"
  location            = azurerm_resource_group.tf-resource-group.location
  resource_group_name = azurerm_resource_group.tf-resource-group.name

  security_rule {
    name                       = "SSH"
    priority                   = 1001
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  security_rule {
    name                       = "HTTP"
    priority                   = 1002
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "80"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

Desplegando con terraform apply se creará en Azure el grupo de seguridad de red.

5.6.2. Creación de una dirección IP pública

En Azure, una dirección IP pública puede ser estática o dinámica. Esta funcionalidad se configura a través de la propiedad sku. El sku básico es gratuito. La diferencia entre ambos es que el sku básico no permite la asignación de una dirección IP estática.

Para crear una dirección IP pública usaremos el recurso azurerm_public_ip. En el siguiente ejemplo se crea una dirección IP pública denominada tf-ip.

...
# Nuevo contenido

resource "azurerm_public_ip" "tf-web-server-ip" {
  name                = "tf-web-server-ip"
  location            = azurerm_resource_group.tf-resource-group.location
  resource_group_name = azurerm_resource_group.tf-resource-group.name
  allocation_method   = "Dynamic"
  sku                 = "Basic"
}

Desplegando con terraform apply se creará en Azure la dirección IP pública.

5.6.3. Creación de una interfaz de red

Para crear una interfaz de red usaremos el recurso azurerm_network_interface. En el siguiente ejemplo se crea una interfaz de red denominada tf-nic conectada a la red y configurada con la dirección IP pública. También se aplica el grupo de seguridad de red creado anteriormente.

...
# Nuevo contenido

resource "azurerm_network_interface" "tf-nic" {
  name                = "tf-nic"
  location            = azurerm_resource_group.tf-resource-group.location
  resource_group_name = azurerm_resource_group.tf-resource-group.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.tf-subnet.id
    private_ip_address_allocation = "Dynamic" (1)
    public_ip_address_id          = azurerm_public_ip.tf-web-server-ip.id (2)
  }
}

resource "azurerm_network_interface_security_group_association" "tf-nic-nsg" { (3)
  network_interface_id      = azurerm_network_interface.tf-nic.id
  network_security_group_id = azurerm_network_security_group.tf-nsg.id
}
  1. Asignación de una dirección IP dinámica

  2. Asignación de la dirección IP pública a la interfaz de red

  3. Asociación del grupo de seguridad de red a la interfaz de red

Desplegando con terraform apply se creará en Azure la interfaz de red.

5.6.4. Creación de la instancia

Para crear una instancia usaremos el recurso azurerm_linux_virtual_machine. En el siguiente ejemplo se crea una instancia denominada tf-vm conectada a la interfaz de red creada anteriormente. La instancia se inicializará con un script de arranque que instalará y configurará el servidor web Apache. Además, será necesario configurar el nombre de usuario, la clave pública de acceso por SSH y los datos de la imagen de la instancia, que en Azure es un poco diferente a la de otros proveedores.

...
# Nuevo contenido

resource "azurerm_linux_virtual_machine" "tf-web-server" {
  name                = "tf-web-server"
  resource_group_name = azurerm_resource_group.tf-resource-group.name
  location            = azurerm_resource_group.tf-resource-group.location
  size                = var.azure-vm-size
  admin_username      = var.azure-admin-username (1)
  network_interface_ids = [ (2)
    azurerm_network_interface.tf-nic.id,
  ]


  admin_ssh_key {
    username   = var.azure-admin-username (3)
    public_key = file("~/.ssh/id_rsa.pub") (4)
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = var.azure-storage-account-type
  }

  source_image_reference {
    publisher = var.azure-os-publisher (5)
    offer     = var.azure-os-offer (6)
    sku       = var.azure-os-sku (7)
    version   = var.azure-os-version (8)
  }

  user_data = base64encode(file("install-web-server.sh")) (9)

  tags = {
    web_server = "tf-web-server"
  }
}
  1. Nombre de usuario administrador de la instancia

  2. ID de la interfaz de red a la que se conectará la instancia

  3. Nombre de usuario para la clave pública de acceso por SSH

  4. Clave pública de acceso por SSH

  5. Publicador de la imagen de la instancia

  6. Nombre de la imagen de la instancia

  7. SKU de la imagen de la instancia. En Azure, el SKU de la imagen es el sistema operativo

  8. Versión de la imagen del sistema operativo

  9. Script de arranque de la instancia. El script hay que codificarlo en base64

La imagen de la instancia en Azure se define por el publicador, la oferta, el SKU y la versión. En el caso de la imagen de Ubuntu que usamos en el ejemplo, los valores son: publisher = "Canonical", offer = "ubuntu-24-04-lts", sku = "server", version = "latest". No obstante, este convenio puede variar en función de la imagen y de la versión que se desee usar. Este enlace muestra en forma de URN los valores de configuración de imágenes Ubuntu para Azure.

El archivo variables.tf tendrá que ser modificado para incluir las variables necesarias para la creación de la instancia.

...
# Nuevo contenido

variable "azure-vm-size" {
  description = "Azure VM size"
}

variable "azure-admin-username" {
  description = "Admin username"
}

variable "azure-storage-account-type" {
  description = "Storage account type"
  default     = "Standard_LRS"
}

variable "azure-os-publisher" {
  description = "Publisher of the image"
  default     = "Canonical"
}

variable "azure-os-offer" {
  description = "Offer of the image"
  default     = "0001-com-ubuntu-server-noble"
}

variable "azure-os-sku" {
  description = "SKU of the image"
  default     = "24_04-lts"
}

variable "azure-os-version" {
  description = "Version of the image"
  default     = "latest"
}

El archivo terraform.tfvars tendrá que ser modificado para incluir los valores de las variables necesarias para la creación de la instancia.

...
# Nuevo contenido

azure-vm-size              = "Standard_B1s"
azure-admin-username       = "mtorres"
azure-storage-account-type = "Standard_LRS"
azure-os-publisher         = "Canonical"
azure-os-offer             = "ubuntu-24_04-lts"
azure-os-sku               = "server"
azure-os-version           = "latest"

El script de arranque de la instancia install-web-server.sh instalará y configurará el servidor web Apache. A continuación se muestra el contenido del script.

El script install-web-server.sh
#!/bin/bash

apt-get update
apt-get install -y apache2
systemctl enable apache2
systemctl start apache2

echo "<h1>Welcome to Terraform Azure</h1>" > /var/www/html/index.html
Note

Se puede añadir un output para mostrar la dirección IP pública de la instancia. Para ello, añadir el siguiente código

output "tf-web-server-ip" {
  value      = azurerm_public_ip.tf-web-server-ip.ip_address
  depends_on = [azurerm_linux_virtual_machine.tf-web-server]
}

Desplegando con terraform apply se creará en Azure la instancia y tras unos minutos se podrá acceder a través de un navegador a la dirección IP pública asignada a la instancia mostrando el mensaje de bienvenida que hemos configurado en el script de arranque. La imagen siguiente muestra el resultado.

azure web server

5.6.5. Creación de un disco de datos

Azure ofrece gran flexibilidad en la creación de discos de datos. Se pueden crear discos de datos independientes o discos de datos que se conectan a la instancia. En este caso, crearemos un disco de datos gestionados que se conectará a la instancia. Para ello, usaremos el recurso azurerm_managed_disk. En el siguiente ejemplo se crea un disco de datos denominado tf-web-server-disk de 1GB de tamaño y posteriormente lo conectaremos a la instancia.

...
# Nuevo contenido

resource "azurerm_managed_disk" "tf-web-server-disk" {
  name                 = "tf-web-server-disk"
  location             = azurerm_resource_group.tf-resource-group.location
  resource_group_name  = azurerm_resource_group.tf-resource-group.name
  storage_account_type = var.azure-storage-account-type
  create_option        = "Empty"
  disk_size_gb         = 1 (1)
}

resource "azurerm_virtual_machine_data_disk_attachment" "tf-web-server-disk" {
  managed_disk_id    = azurerm_managed_disk.tf-web-server-disk.id
  virtual_machine_id = azurerm_linux_virtual_machine.tf-web-server.id
  lun                = 10 (2)
  caching            = "ReadWrite"
}
  1. Tamaño del disco en GB

  2. Número de unidad lógica (LUN) del disco. Este número debe ser único para cada disco conectado a la instancia. Por defecto, el valor es 0.

Desplegando con terraform apply se creará en Azure el disco de datos y se conectará a la instancia. Si nos conectamos a la instancia con ssh podremos ver el disco creado con lsblk.

6. Bucles en Terraform

Terraform tiene una forma particular de implementar bucles. Aquí veremos cómo hacerlo con contadores y con un bucle for. En ambos casos contamos con las variables declaradas en variables.tf e inicializadas en terraform.tfvars, como se muestra a continuación. Para ilustrar el uso de los bucles usaremos un ejemplo de creación de dos instancias. Los valores de creación de las instancias los inicializaremos en un array de objetos, donde cada objeto incluye el nombre de la instancia, el tipo de máquina y la imagen. El ejemplo lo implementaremos para Google Cloud Platform.

El archivo variables.tf
variable "gcp-username" {
  description = "GCP user name"
}

variable "gcp-project" {
  description = "GCP project"
}

variable "gcp-region" {
  description = "GCP region"

}

variable "gcp-zone" {
  description = "GCP zone"
}

variable "gcp-network" {
  description = "GCP network"
}

variable "gcp-subnetwork" {
  description = "GCP subnetwork"
}

variable "instances" {
  description = "Number of instances to create"
  type = list(object({
    name         = string,
    machine_type = string,
    image        = string
  }))
}
El archivo terraform.tfvars
gcp-username   = "your-username"
gcp-project    = "your-project"
gcp-region     = "us-central1"
gcp-zone       = "us-central1-c"
gcp-network    = "terraform-network"
gcp-subnetwork = "terraform-subnetwork"

instances = [{ (1)
  name         = "database"
  machine_type = "n1-standard-1"
  image        = "debian-cloud/debian-11"
  }, { (2)
  name         = "web-server"
  machine_type = "n1-standard-1"
  image        = "ubuntu-os-cloud/ubuntu-2204-lts"
  }
]
  1. Primera instancia. Nombre: database, Tipo de máquina: n1-standard-1, Imagen: debian-cloud/debian-11

  2. Segunda instancia. Nombre: web-server, Tipo de máquina: n1-standard-1, Imagen: ubuntu-os-cloud/ubuntu-2204-lts

El archivo providers.tf
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "6.5.0"
    }
  }
}

provider "google" {
  credentials = file("../gcp-identity.json")

  project = var.gcp-project
  region  = var.gcp-region
  zone    = var.gcp-zone
}

6.1. Bucles con count

Para este ejemplo usaremos un contador para crear varias instancias en GCP. La propiedad count se inicializa con la longitud del array de instancias. El índice del array se obtiene con count.index.

Archivo main.tf
resource "google_compute_instance" "instance" {
  count = length(var.instances) (1)

  name         = var.instances[count.index].name (2)
  machine_type = var.instances[count.index].flavor (3)
  boot_disk {
    initialize_params {
      image = var.instances[count.index].image (4)
    }
  }

  network_interface {
    network = google_compute_network.tf-net.self_link
  }
}
  1. La propiedad count se inicializa con la longitud del array instances

  2. El nombre de la instancia se obtiene del array instances con el índice count.index y la propiedad `name

  3. El sabor de la instancia se obtiene del array instances con el índice count.index y la propiedad `flavor

  4. La imagen de la instancia se obtiene del array instances con el índice count.index y la propiedad `image

En este caso, el recurso openstack_compute_instance_v2 se creará tantas veces como elementos tenga el array instances. El índice del array se obtiene con count.index.

Note

El ejemplo crea dos instancias, una para la base de datos y otra para el servidor web. Es sólo un ejemplo para ilustrar el uso de bucles en Terraform. Como se puede observar, no se realiza ninguna configuración adicional en las instancias. Para configurar las instancias, se puede configurar la propiedad metadata_startup_script con un script de inicialización. A modo de indicación, se podría añadir en las propiedades de cada instancia de los archivos de variables, el nombre del script de inicialización y el path del script.

Para obtener la dirección IP de las instancias creadas, usaremos output. En lugar de añadir una salida por cada instancia, usaremos un bucle for para recorrer las instancias y obtener sus direcciones IP. Sin embargo, output no soporta count. En su defecto, usaremos for para obtener las direcciones IP de las instancias y asignarlas a la propiedad value de output. Y en lugar de añadir el código de output en main.tf, lo añadiremos en un archivo independiente outputs.tf.

Archivo outputs.tf
output "tf-vm-internal-ip" {
  value = [
    for vm in google_compute_instance.tf-vm : vm.network_interface.0.network_ip (1)
  ]
  depends_on = [google_compute_instance.tf-vm]
}

output "tf-vm-ephemeral-ip" {
  value = [
    for vm in google_compute_instance.tf-vm : vm.network_interface.0.access_config.0.nat_ip (2)
  ]
  depends_on = [google_compute_instance.tf-vm]
}
  1. Dirección IP interna de las instancias. El bucle for recorre las instancias creadas y disponibles en google_compute_instance.tf-vm y obtiene la dirección IP interna de cada instancia.

  2. Dirección IP externa de las instancias. El bucle for recorre las instancias creadas y disponibles en google_compute_instance.tf-vm y obtiene la dirección IP externa de cada instancia.

Una vez aplicados los cambios con terraform apply, se crearán las instancias en GCP y se podrán obtener las direcciones IP de las instancias con output.

$ terraform apply

...

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

tf-vm-ephemeral-ip = [
  "34.69.211.150",
  "35.193.129.189",
]
tf-vm-internal-ip = [
  "10.128.0.11",
  "10.128.0.10",
]

Cuando ya no se necesiten las instancias, se pueden destruir con terraform destroy.

6.2. Bucles con for_each

En Terraform, el bucle for se usa para crear estructuras de datos, pero no se aplica para crear recursos. Por tanto, si queremos crear varios recursos en un bucle sin usar count necesitaremos otra construcción. Aquí es donde entra el uso de for_each. Veamos cómo usar for_each para crear las dos instancias del ejemplo anterior en GCP.

Para la preparación, necesitaremos modificar los archivos variables.tf y terraform.tfvars ya que for_each usa mapas en lugar de arrays. A continuación se muestran las modificaciones que hay que realizar.

El archivo variables.tf
...
variable "instances" {
  description = "Number of instances to create"
  type = map(object({
    name         = string,
    machine_type = string,
    image        = string
  }))
}
El archivo terraform.tfvars
...
instances = {
  database = {
    name         = "database"
    machine_type = "n1-standard-1"
    image        = "debian-cloud/debian-11"
  },
  web-server = {
    name         = "web-server"
    machine_type = "n1-standard-1"
    image        = "ubuntu-os-cloud/ubuntu-2204-lts"
  }
}

El archivo main.tf se modificará para usar for_each en lugar de count. La propiedad for_each se inicializa con el mapa de instancias. Una vez inicializado, each.key se podrá usar para obtener el nombre de la instancia (p.e. database, web-server) y each.value para obtener las propiedades de la instancia.

Archivo main.tf
resource "google_compute_instance" "instance" {
  for_each = var.instances (1)

  name         = each.value.name (2)
  zone         = var.gcp-zone
  machine_type = each.value.machine_type (3)
  boot_disk {
    initialize_params {
      image = each.value.image (4)
    }
  }

  # Startup script
  # metadata_startup_script = "${file("script.sh")}"

  network_interface {
    network    = var.gcp-network
    subnetwork = var.gcp-network

    access_config {}
  }
}
  1. La propiedad for_each se inicializa con el mapa de instancias

  2. El nombre de la instancia se obtiene con each.value.name

  3. El tipo de máquina se obtiene con each.value.machine_type

  4. La imagen de la instancia se obtiene con each.value.image

Una vez aplicados los cambios con terraform apply, se crearán las instancias en GCP y se podrán obtener las direcciones IP de las instancias con output.

$ terraform apply

...

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

tf-vm-ephemeral-ip = [
  "34.132.169.85",
  "34.69.211.150",
]
tf-vm-internal-ip = [
  "10.128.0.15",
  "10.128.0.14",
]

Cuando ya no se necesiten las instancias, se pueden destruir con terraform destroy.

7. Módulos en Terraform

En Terraform, los módulos son una manera de organizar, reutilizar y estructurar el código, lo cual permite crear y gestionar infraestructuras de una manera más eficiente y escalable. Un módulo en Terraform es un conjunto de archivos de configuración que se agrupan para realizar una función específica. Los módulos ayudan a dividir la infraestructura en componentes lógicos, que luego pueden ser reutilizados en otros proyectos o aplicaciones. Así, usaremos los módulos en estos casos:

  • Reutilización del Código: Los módulos permiten empaquetar configuraciones de recursos complejas en un solo bloque, que luego puede utilizarse en varios proyectos.

  • Mantenimiento Sencillo: Los módulos organizan el código, facilitando el mantenimiento y las actualizaciones en infraestructuras grandes y complejas.

  • Consistencia y Estandarización: Al usar un mismo módulo en diferentes entornos (desarrollo, pruebas y producción), se asegura que las configuraciones sean consistentes en todos los entornos.

7.1. Estructura básica de un módulo

Un módulo básico suele consistir en tres archivos principales:

  • main.tf: Define los recursos principales del módulo.

  • variables.tf: Declara las variables necesarias para parametrizar el módulo.

  • outputs.tf: Especifica los valores que el módulo devolverá al ser invocado.

Además, un módulo puede tener otros archivos o carpetas, como configuraciones adicionales, archivos de documentación (README.md), o scripts personalizados, según sea necesario.

7.2. Uso de módulos en un proyecto

Una vez definido el módulo, se puede llamar en el código de Terraform utilizando el bloque module. A continuación, se muestra un ejemplo:

...
module "web_server" {
  source = "./path/to/module" # Puede ser una ruta local o un repositorio remoto.
  name   = "web-server"       # Variables pasadas al módulo
  region = "us-west-1"
  ...
}
...

Al ejecutar este código, Terraform invoca el módulo, utiliza sus configuraciones y aplica las variables definidas al llamar al módulo.

7.3. Creación de un módulo para la creación de instancias en GCP

En este ejemplo, crearemos un módulo denominado instance_module para la creación de instancias en Google Cloud Platform. El módulo estará en una carpeta denominada modules y creará una instancia en GCP con un disco de datos y una dirección IP pública. El módulo se compondrá de los siguientes archivos:

  • main.tf: Definirá los recursos principales del módulo.

  • variables.tf: Declarará las variables necesarias para parametrizar el módulo.

  • outputs.tf: Especificará los valores que el módulo devolverá al ser invocado.

La estructura del proyecto será la siguiente:

.
├── main.tf
├── variables.tf
├── outputs.tf
├── terraform.tfvars
├── providers.tf
└── modules
    └── instance_module
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Veamos el código de los archivos del módulo. Comencemos especificando las variables necesarias en el archivo variables.tf.

El archivo modules/instance_module/variables.tf
variable "gcp-username" {
  description = "GCP user name"
}

variable "gcp-zone" {
  description = "GCP zone"
}

variable "gcp-network" {
  description = "GCP network"
}

variable "gcp-subnetwork" {
  description = "GCP subnetwork"
}

variable "instance-name" {
  description = "Name of the instance"
}

variable "machine-type" {
  description = "Machine type"
}

variable "image" {
  description = "Image"
}

A continuación, definimos los recursos principales del módulo en el archivo main.tf. Los valores de las variables se obtendrán en tiempo de ejecución de las variables con las que se llame al módulo.

El archivo modules/instance_module/main.tf
resource "google_compute_instance" "tf-vm" {
  name         = var.instance-name
  zone         = var.gcp-zone
  machine_type = var.machine-type
  boot_disk {
    initialize_params {
      image = var.image
    }
  }

  # Add SSH access to the Compute Engine instance
  metadata = {
    ssh-keys = "${var.gcp-username}:${file("~/.ssh/id_rsa.pub")}"
  }

  # Startup script
  # metadata_startup_script = "${file("script.sh")}"

  network_interface {
    network    = var.gcp-network
    subnetwork = var.gcp-network

    access_config {}
  }
}

Finalmente, especificamos en el archivo outputs.tf los valores que el módulo devolverá al ser llamado.

El archivo modules/instance_module/outputs.tf
output "tf-vm-internal-ip" { (1)
  value      = google_compute_instance.tf-vm.network_interface.0.network_ip
  depends_on = [google_compute_instance.tf-vm]
}

output "tf-vm-ephemeral-ip" { (2)
  value      = google_compute_instance.tf-vm.network_interface.0.access_config.0.nat_ip
  depends_on = [google_compute_instance.tf-vm]
}
  1. Dirección IP interna de la instancia. Este nombre es el que se usará para obtener el valor devuelto por el módulo.

  2. Dirección IP externa de la instancia. Este nombre es el que se usará para obtener el valor devuelto por el módulo.

7.4. Uso del módulo en el proyecto

Una vez definido el módulo, se puede llamar en el código de Terraform utilizando el bloque module. A continuación, se muestra un ejemplo de cómo llamar al módulo instance_module en el archivo main.tf. No obstante, también será necesario definir las variables necesarias en el archivo variables.tf y en el archivo terraform.tfvars, así como el proveedor de GCP en el archivo providers.tf y la salida de los valores del módulo en el archivo outputs.tf.

El archivo providers.tf
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "6.5.0"
    }
  }
}

provider "google" {
  credentials = file("./gcp-identity.json")

  project = var.gcp-project
  region  = var.gcp-region
  zone    = var.gcp-zone
}
El archivo variables.tf
variable "gcp-username" {
  description = "GCP user name"
}

variable "gcp-project" {
  description = "GCP project"
}

variable "gcp-region" {
  description = "GCP region"

}

variable "gcp-zone" {
  description = "GCP zone"
}

variable "gcp-network" {
  description = "GCP network"
}

variable "gcp-subnetwork" {
  description = "GCP subnetwork"
}

variable "instances" {
  description = "Number of instances to create"
  type = map(object({
    name         = string,
    machine_type = string,
    image        = string
  }))
}
El archivo terraform.tfvars
gcp-username   = "your-username"
gcp-project    = "your-project"
gcp-region     = "us-central1"
gcp-zone       = "us-central1-c"
gcp-network    = "terraform-network"
gcp-subnetwork = "terraform-subnetwork"

instances = {
  database = {
    name         = "database"
    machine_type = "n1-standard-1"
    image        = "debian-cloud/debian-11"
  },
  web-server = {
    name         = "web-server"
    machine_type = "n1-standard-1"
    image        = "ubuntu-os-cloud/ubuntu-2204-lts"
  }
}
El archivo main.tf
module "database" { (1)
  source         = "./instance-module" (2)
  instance-name  = var.instances.database.name (3)
  machine-type   = var.instances.database.machine_type
  image          = var.instances.database.image
  gcp-zone       = var.gcp-zone (4)
  gcp-username   = var.gcp-username
  gcp-network    = var.gcp-network
  gcp-subnetwork = var.gcp-subnetwork
}

module "web-server" {
  source         = "./instance-module"
  instance-name  = var.instances.web-server.name
  machine-type   = var.instances.web-server.machine_type
  image          = var.instances.web-server.image
  gcp-zone       = var.gcp-zone
  gcp-username   = var.gcp-username
  gcp-network    = var.gcp-network
  gcp-subnetwork = var.gcp-subnetwork
}
  1. Llamada al módulo instance-module

  2. Ruta al módulo

  3. Variables pasadas al módulo para la personalización de la instancia

  4. Variables genéricas necesarias para la creación de la instancia

El archivo outputs.tf
output "database_external_ip" {
  value = module.database.tf-vm-ephemeral-ip (1)
}

output "web-server_external_ip" {
  value = module.web-server.tf-vm-ephemeral-ip
}

output "database_internal_ip" {
  value = module.database.tf-vm-internal-ip (2)
}

output "web-server_internal_ip" {
  value = module.web-server.tf-vm-internal-ip
}
  1. Dirección IP externa de la instancia database obtenida de la salida del módulo

  2. Dirección IP interna de la instancia database obtenida de la salida del módulo

8. Recursos de interés

9. Conclusiones

En este tutorial hemos visto cómo usar Terraform para desplegar infraestructura en la nube en diferentes proveedores. Hemos visto cómo configurar Terraform para trabajar con los proveedores y cómo crear configuraciones para desplegar recursos en la nube. Hemos visto cómo crear instancias, redes, volúmenes, direcciones IP, etc. en los proveedores OpenStack, Google Cloud y Microsoft Azure. Básicamente, hemos desarrollado un ejemplo de preparación de la infraestructura de red y de despliegue de una instancia con un servidor web Apache en cada uno de los proveedores. Esto nos ha permitido ver las similitudes en la conceptualización en el proceso de despliegue de infraestructura en la nube en diferentes proveedores así como las diferencias en la configuración de los recursos en cada uno de ellos.