Terraform nos permite describir, evaluar y aplicar cambios en nuestra infraestructura mantenida como código (infrastructure as code).
Creado por Hashicorp y liberado como open source, Terraform se une a productazos de esta misma compañía como Vagrant, Vault, Nomad o Consul, los cuales destacan historicamente por su calidad y flexibilidad.
Introducción
Para iniciarte en Terraform, un buen punto de partida puede ser la charla "Terraform: Managing your Infrastructure as code" que los chicos de @FlyWireEng impartieron en la Software Craftsmanship de Barcelona:
En ella podemos ver en detalle el flujo básico de trabajo con Terraform utilizando uno de sus providers de Cloud como es el de AWS. En cualquier caso, la lista de los providers soportados es simplemente impresionante, ya que uno de sus objetivos principales es el de ser capaz de conectar y coordinar cualquier entorno ya sea Cloud como on-premises. En esta lista de providers, entre otros, podemos encontrar: Azure, Bitbucket, Cloudflare, DigitalOcean, DNSimple, Docker, Google Cloud, Kubernetes, OpenStack, OVH, PostgreSQL y muchos más.
Terraform y su provider de AWS
Para que te sea más sencillo iniciarte en Terraform, vamos a crear un proyecto nuevo basado en el provider de AWS. Crearemos así un directorio hello-terraform-aws
y dentro, generaremos la descripción inicial de la infraestructura en el fichero main.tf
con el siguiente contenido:
resource "aws_s3_bucket" "mycompany-database-dumps" {
bucket = "mycompany-database-dumps"
acl = "private"
}
Como podéis ver, estas lineas describen un bucket en el servicio de S3 de AWS. Ahora que ya tenemos alguna descripción que hace uso de uno de los providers de Terraform, es buen momento para inicializar nuestro proyecto y que Terraform descargue los providers necesarios ejecutando el comando terraform init
:
$ terraform init
Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (1.6.0)...
The following providers do not have any version constraints in configuration,
so the latest version was installed.
To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.
* provider.aws: version = "~> 1.6"
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.
Como podemos ver por el output del comando terraform init
, el siguiente paso es ver el impacto de nuestra configuración con el comando terraform plan
. El comando plan
permite que Terraform evalue la infraestructura descrita en main.tf
y la compare con el estado actual de nuestra cuenta en AWS, mostrando un resumen de aquellas acciones que se aplicarían si finalmente ejecutamos la definición.
Si lo ejecutamos en nuestro caso, al no existir todavía ningún bucket con el nombre mycompany-database-dumps
, Terraform nos mostrará el siguiente resultado:
$ terraform plan
provider.aws.region
The region where AWS operations will take place. Examples
are us-east-1, us-west-2, etc.
Default: us-east-1
Enter a value: eu-west-1
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.
------------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ aws_s3_bucket.mycompany-database-dumps
id:
acceleration_status:
acl: "private"
arn:
bucket: "mycompany-database-dumps"
bucket_domain_name:
force_destroy: "false"
hosted_zone_id:
region:
request_payer:
versioning.#:
website_domain:
website_endpoint:
Plan: 1 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.
Como podemos ver al principio del comando, Terraform nos va a preguntar todo aquello que desconoce al no haber sido especificado en el main.tf
. Es el caso por ejemplo de la región de AWS. Si no queremos estar poniendola todo el rato, lo mejor es añadir esta definición al principio del main.tf
:
provider "aws" {
access_key = "XXX"
secret_key = "XXX"
region = "eu-central-1"
}
Lo siguiente es fijarnos en el resumen del final del comando, el cual nos indica que al ser un bucket nuevo, el plan será crear un recurso, cambiar 0 y destruir 0. Vamos pues a ello!!
Para aplicar los cambios descritos, utilizaremos el comando terraform apply
:
$ terraform apply
provider.aws.region
The region where AWS operations will take place. Examples
are us-east-1, us-west-2, etc.
Default: us-east-1
Enter a value: eu-west-1
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
+ aws_s3_bucket.mycompany-database-dumps
id:
acceleration_status:
acl: "private"
arn:
bucket: "mycompany-database-dumps"
bucket_domain_name:
force_destroy: "false"
hosted_zone_id:
region:
request_payer:
versioning.#:
website_domain:
website_endpoint:
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_s3_bucket.mycompany-database-dumps: Creating...
acceleration_status: "" => ""
acl: "" => "private"
arn: "" => ""
bucket: "" => "mycompany-database-dumps"
bucket_domain_name: "" => ""
force_destroy: "" => "false"
hosted_zone_id: "" => ""
region: "" => ""
request_payer: "" => ""
versioning.#: "" => ""
website_domain: "" => ""
website_endpoint: "" => ""
aws_s3_bucket.mycompany-database-dumps: Creation complete after 5s (ID: mycompany-database-dumps)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Y listo!! Ya tenemos nuestros recursos definidos y sincronizados con nuestra infraestructura. Si nos queda alguna duda, sólo tenemos que volver a ejectuar terraform plan
para ver que no hay cambios a realizar y que todo está sincronizado:
$ terraform plan
provider.aws.region
The region where AWS operations will take place. Examples
are us-east-1, us-west-2, etc.
Default: us-east-1
Enter a value: eu-west-1
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_s3_bucket.mycompany-database-dumps: Refreshing state... (ID: mycompany-database-dumps)
------------------------------------------------------------------------
No changes. Infrastructure is up-to-date.
This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.
Ingeniería inversa de nuestra infraestructura
Para completar esta introducción y si ya tienes una infraestructura existente, como es nuestro caso, una buena opción para aprender con ejemplos es terraformar tu infraestructura y ver el resultado. Con esto quiero decir generar el fichero de descripción de Terraform a partir de la infraestructura ya existe, de forma que al intentar ejectuar terraform plan
, nos diga que no hay cambios a aplicar.
Para ello, en la parte de Terraform debemos contar tanto con el script de definición main.tf
como con el estado actual o descripción completa de todos los elementos de la infraestructura que ya se han consolidado. Esta definición detallada la guarda Terraform en el fichero terraform.tfstate
y se conoce como state
(estado actual de sincronización).
Así pues y para conseguir este objetivo, el primer impulso es examinar el comando terraform import
, pero como podemos ver, todavía no está listo para ser utilizado según la documentación oficial, ya que aunque permite importar configuración al state
, no nos genera su descripción de recursos:
The current implementation of Terraform import can only import resources into the state. It does not generate configuration. A future version of Terraform will fully generate configuration, significantly simplifying this process.
Una alternativa pues para generar ambos ficheros es utilizar Terraforming, el cual se encuentra disponible como contenedor Docker.
Suponiendo que ya tenemos una cuenta en AWS con varias instancias corriendo en EC2, una forma de recuperar el estado actual y "terraformar" así nuestra infraestructura como código sería producir antes de nada el fichero de definición de recursos (en esta caso lo generaremos automáticamente examinando la infraestructura existente en AWS):
docker run --rm --name terraforming \
-e AWS_ACCESS_KEY_ID=XXX \
-e AWS_SECRET_ACCESS_KEY=XXX \
-e AWS_DEFAULT_REGION=eu-west-1 \
quay.io/dtan4/terraforming:latest terraforming ec2 > main.tf
Como podemos ver, pasar las credenciales de acceso a AWS en este caso es necesario, ya que estas no se encuentran definidas en el contenedor Docker. Por otra parte, lo único que queda pues es ejecutar el comando "terraforming" pasándole el servicio sobre el que lo aplicaremos de entre todos los soportados en AWS (asg, dbpg, dbsg, dbsn, ec2, ecc, ecsn, elb, iamg, iamgm, iamgp, iamip, iamp iamr, iamrp, iamu, iamup, nacl, r53r, r53z, rds, rt, rta, s3, sg, sn y vpc).
Finalmente, para dejar a Terraform con el state
correcto, debemos generar también el terraform.tfstate
. En caso contrario, nos estaría pidiendo aplicar cambios que ya tenemos consolidados en nuestra infraestura al desconocer que ya se aplicaron en su momento. Para ello podemos volver a invocar a terraforming
, esta vez con la opción --tfstate
:
docker run --rm --name terraforming
-e AWS_ACCESS_KEY_ID=XXX \
-e AWS_SECRET_ACCESS_KEY=XXX \
-e AWS_DEFAULT_REGION=eu-west-1 \
quay.io/dtan4/terraforming:latest terraforming ec2 --tfstate > terraform.tfstate
Si ahora ejecutamos terraform plan
, nos debería decir que todo está aplicado y correcto como en el caso del ejemplo del bucket :)
Conclusión
Terraform nos permite de una forma declarativa construir y mantener la definición de nuestra infraestructura como si de código se tratase. Su potencia y capacidad de adaptación a múltiples entornos y necesidades hace que se especialmente interesante en entornos muy dinámicos, cambiantes y con distintos entornos de ejecución (on-premises, cloud o híbrido).