Infraestructura como Código (V): Terraform vs Ansible

Publicado por Alejandro Nieto el

DevOpsInfraestructura como CódigoTerraformAnsible

En los post anteriores hemos hablado de tratar la infraestructura como código, agilizar su creación y todas las ventajas que ello supone. Posteriormente hemos puesto en práctica varios ejemplos utilizando Terraform, pero es posible que muchos de vosotros os preguntéis qué os aporta Terraform frente a Ansible, si ya tenéis conocimiento en ésta última. Éste es el propósito de este post.

Configuration Management & Infrastructure as Code

Ansible (y otras como Chef, Puppet, ...) nació como una herramienta de "Configuration Management", esto quiere decir que está diseñada para tareas como instalar y administrar software o configurar servidores existentes. Esto no quiere decir que Ansible no sea capaz de manejar infraestructura como código, de hecho sí lo es.

Con Terraform sólo nos hacemos cargo del aprovisionamiento de infraestructura y dejamos la configuración de ésta en manos de otras herramientas (como Ansible).

Ahora mismo estaréis pensando que con Ansible podéis solucionar ambas problemáticas: el aprovisionamiento de infraestructura y su configuración, pero más adelante veréis las diferencias entre ambas herramientas, tales como la administración del estado de nuestra infraestructura.

Estilo & Lenguaje

Dentro del ámbito de herramientas DevOps podemos distinguir las herramientas que utilizan un estilo declarativo y las que tienen un estilo procedimental.

Si se utiliza un estilo procedimental es necesario indicar los pasos exactos a seguir en nuestro código, como por ejemplo Ansible, en el que se definen una serie de tareas consecutivas.

Si utilizamos una herramienta con estilo declarativo solo es necesario indicar el resultado que deseamos con nuestro código. En este caso, Ansible utiliza un estilo procedimental y Terraform un estilo declarativo.

Caso práctico

Vamos a aplicar algunos ejemplos prácticos en los que se verá la información que nos presentan ambas herramientas al planificar un cambio y cómo llevan a cabo estos cambios.

Ansible

Para crear un VPC de AWS con Ansible necesitamos el siguiente bloque (asumiendo que el provider ya esté configurado):

# ansible-aws.yml

- hosts: localhost
  connection: local
  gather_facts: False

  vars:
    cidr_block: 10.10.0.0/16

  tasks:
    - name: create a VPC with dedicated tenancy and a couple of tags
      ec2_vpc_net:
        name: enmilocalfunciona
        cidr_block: "{{ cidr_block }}"
        region: eu-west-1

En este caso le decimos a Ansible que cree una VPC con el cidr_block 10.10.0.0/16 y ejecutamos la receta:

$ ansible-playbook ansible-aws.yml
 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'


PLAY [localhost] **************************************************************************************************************************************************************************************************

TASK [create a VPC with dedicated tenancy and a couple of tags] ***************************************************************************************************************************************************  
changed: [localhost]

PLAY RECAP ********************************************************************************************************************************************************************************************************  
localhost                  : ok=1    changed=1    unreachable=0    failed=0  

Imaginad que queremos cambiar un argumento del recurso que hemos creado, en este caso se puede cambiar el cidr_block:

# ansible-aws.yml

- hosts: localhost
  connection: local
  gather_facts: False

  vars:
    cidr_block: 10.20.0.0/16

  tasks:
    - name: create a VPC with dedicated tenancy and a couple of tags
      ec2_vpc_net:
        name: enmilocalfunciona
        cidr_block: "{{ cidr_block }}"
        region: eu-west-1

Si queremos obtener información sobre los cambios a realizar por Ansible podemos obtenerlos así:

$ ansible-playbook ansible-aws.yml --check
 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'


PLAY [localhost] **************************************************************************************************************************************************************************************************

TASK [create a VPC with dedicated tenancy and a couple of tags] ***************************************************************************************************************************************************  
changed: [localhost]

PLAY RECAP ********************************************************************************************************************************************************************************************************  
localhost                  : ok=1    changed=1    unreachable=0    failed=0  

Como podéis observar, la única información que nos da es que va a haber un cambio, pero no nos dice dónde ni qué va a cambiar.

Si ejecutamos el playbook veremos que lo que hace es crear una nueva VPC con el cidr_block 10.20.0.0/16 sin modificar la que creamos anteriormente.

vpc-aws
Terraform

Vamos a realizar exactamente lo mismo con Terraform. Definir un recurso (en este caso VPC) y ejecutarlo:

resource "aws_vpc" "enmilocalfunciona" {  
  cidr_block = "10.10.0.0/16"
  enable_dns_hostnames = true
  tags {
    Name = "enmilocalfunciona"
  }
}

Para ejecutar con Terraform, primero se debe crear un plan, el cual nos muestra información sobre lo que se va a ejecutar. Este plan quedará almacenado en un fichero, en este caso enmilocal.tfplan:

$ terraform plan -var-file='aws.tfvars' -out='enmilocal.tfplan'
...
  + create
Terraform will perform the following actions:

  + aws_vpc.enmilocalfunciona
      id:                               <computed>
      arn:                              <computed>
      assign_generated_ipv6_cidr_block: "false"
      cidr_block:                       "10.10.0.0/16"
      ...
      tags.%:                           "1"
      tags.Name:                        "enmilocalfunciona"


Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

This plan was saved to: enmilocal.tfplan

To perform exactly these actions, run the following command to apply:  
    terraform apply "enmilocal.tfplan"

En un entorno real, estos parámetros deberían ser definidos como variables y trabajar con éstas. En este caso cambiaremos el cidr_block por 10.20.0.0/16 en código:

resource "aws_vpc" "enmilocalfunciona" {  
  cidr_block = "10.20.0.0/16"
  enable_dns_hostnames = true
  tags {
    Name = "enmilocalfunciona"
  }
}
$ terraform plan -var-file='aws.tfvars' -out='enmilocal.tfplan'
...
-/+ destroy and then create replacement

Terraform will perform the following actions:

-/+ aws_vpc.enmilocalfunciona (new resource required)
      id:                               "vpc-012a080f9f1f2d6cd" => <computed> (forces new resource)
      arn:                              "arn:aws:ec2:eu-west-1:104102502792:vpc/vpc-012a080f9f1f2d6cd" => <computed>
      assign_generated_ipv6_cidr_block: "false" => "false"
      cidr_block:                       "10.10.0.0/16" => "10.20.0.0/16" (forces new resource)
...

Plan: 1 to add, 0 to change, 1 to destroy.

------------------------------------------------------------------------

This plan was saved to: enmilocal.tfplan

To perform exactly these actions, run the following command to apply:  
    terraform apply "enmilocal.tfplan"

Al ser lenguaje declarativo y controlar el estado de nuestra infraestructura, Terraform entiende que queremos que la VPC tenga un cidrblock diferente y no que creemos otra VPC adicional con el cidrblock que le hemos indicado, que es lo que ejecuta Ansible.

Al aplicar el plan:

 terraform apply "enmilocal.tfplan"
aws_vpc.enmilocalfunciona: Creating...  
  arn:                              "" => "<computed>"
  assign_generated_ipv6_cidr_block: "" => "false"
  cidr_block:                       "" => "10.10.0.0/16"
...
  enable_dns_hostnames:             "" => "true"
  enable_dns_support:               "" => "true"
  instance_tenancy:                 "" => "default"
  ipv6_association_id:              "" => "<computed>"
  ipv6_cidr_block:                  "" => "<computed>"
  main_route_table_id:              "" => "<computed>"
  owner_id:                         "" => "<computed>"
  tags.%:                           "" => "1"
  tags.Name:                        "" => "enmilocalfunciona"
aws_vpc.enmilocalfunciona: Creation complete after 4s (ID: vpc-0f7ad36df5258bbaa)

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

Si posteriormente queremos cambiar el recurso, por ejemplo, el cidr, nos indican los cambios que van a ser llevados a cabo:

terraform plan -var-file='aws.tfvars' -out='enmilocal.tfplan'  
Refreshing Terraform state in-memory prior to plan...  
The refreshed state will be used to calculate this plan, but will not be  
persisted to local or remote state storage.

aws_vpc.enmilocalfunciona: Refreshing state... (ID: vpc-0f7ad36df5258bbaa)

------------------------------------------------------------------------

An execution plan has been generated and is shown below.  
Resource actions are indicated with the following symbols:  
-/+ destroy and then create replacement

Terraform will perform the following actions:

-/+ aws_vpc.enmilocalfunciona (new resource required)
      id:                               "vpc-0f7ad36df5258bbaa" => <computed> (forces new resource)
      arn:                              "arn:aws:ec2:eu-west-1:104102502792:vpc/vpc-0f7ad36df5258bbaa" => <computed>
      assign_generated_ipv6_cidr_block: "false" => "false"
      cidr_block:                       "10.10.0.0/16" => "10.20.0.0/16" (forces new resource)
...
      tags.Name:                        "enmilocalfunciona" => "enmilocalfunciona"


Plan: 1 to add, 0 to change, 1 to destroy.

------------------------------------------------------------------------

This plan was saved to: enmilocal.tfplan

To perform exactly these actions, run the following command to apply:  
    terraform apply "enmilocal.tfplan"

Podéis apreciar en la línea cidr_block cuál va a ser el cambio llevado a cabo, y si para llevarlo a cabo es necesario destruirlo y construirlo de nuevo:

cidr_block:                       "10.10.0.0/16" => "10.20.0.0/16" (forces new resource)  

Aplicamos de nuevo el plan actualizado y observamos cómo el recurso es destruido y creado de nuevo:

aws_vpc.enmilocalfunciona: Destroying... (ID: vpc-0f7ad36df5258bbaa)  
aws_vpc.enmilocalfunciona: Destruction complete after 0s  
aws_vpc.enmilocalfunciona: Creating...  
  arn:                              "" => "<computed>"
  assign_generated_ipv6_cidr_block: "" => "false"
  cidr_block:                       "" => "10.20.0.0/16"
...
  tags.Name:                        "" => "enmilocalfunciona"
aws_vpc.enmilocalfunciona: Creation complete after 4s (ID: vpc-0ca954b8c15912e69)

Apply complete! Resources: 1 added, 0 changed, 1 destroyed.  
➜  enmilocalfunciona 

Conclusión

Ambas herramientas son potentes y fáciles de aprender. Si ya conocéis Ansible o queréis tener todo el código unificado (Configuration Management e IaC) es posible que os resulte más atractivo Ansible. Si no conocéis ninguna de las herramientas, queréis separar responsabilidades y obtener más información, aplicar GitOps, etc., es posible que sea Terraform la que os resulte más atractiva. La elección es vuestra pero Terraform es una herramienta más completa en cuanto a Infraestructura como Código.

Si te ha gustado, ¡síguenos en Twitter para estar al día de próximos posts!