Como organizar sua infra como código com o Terraform

Alex Baptista
Accenture Digital Product Dev
8 min readAug 14, 2019

--

O objetivo da “Infraestrutura como código (Infra-as-code — IaC)” é dar visibilidade do estado da infraestrutura para todos os envolvidos, como operações e desenvolvimento, por exemplo. Essa técnica é considerada uma boa prática para criar e evoluir ambientes e eu, particularmente, considero essencial a adoção em qualquer modelo de infraestrutura, seja cloud computing, on-premisses ou qualquer outro.

Neste post, vamos mostrar como usar o “Terraform” como ferramenta para o “Infra-as-code”. Acredito que atualmente essa é a melhor ferramenta para lidar com múltiplos cloud providers, como Google Cloud, AWS e etc., e tem uma notação bem amigável (HCL — semelhante ao JSON), com aprendizado simples.

Em times multidisciplinares, é comum que o “DevOps” seja o principal responsável pela infraestrutura e manutenção do “Infra-as-code”. Em alguns casos, ele até torna o ambiente em uma “caixa-preta” para os desenvolvedores, que se limitam a dizer: “Olha, sei que tem uma automação aí, mas nem mexo”.

A idea aqui é que iniciantes no “Terraform” aprendam a manter seu código reaproveitável, seguro e colaborativo para os demais integrantes do time, e ao mesmo tempo demonstrar que o DevOps não precisa (nem deve) ser o único detentor do conhecimento da infraestrutura como código.

Se ainda não conhece ou nunca ouviu falar no Terraform, você pode dar uma olhada neste post aqui, no Blog da Concrete. Pronto para começar? Bora lá!

Ferramentas

Para esta demonstração vamos utilizar as ferramentas:

Estruturando o “Terraform” para múltiplos usuários

Vamos imaginar que você já tenha um modelo inicial de código Terraform, mas precisa adotar alguns mecanismos de operação para múltiplos usuários para evitar ações indesejadas, repetições de código e concorrência de operação. Para isso, vamos usar alguns recursos da AWS.

O primeiro passo é versionar este código em um repositório (Github, Bitbucket ou outro). Isso é importante para que adotemos um “workflow” de trabalho mais adiante. Então, se ainda não o fez, faça neste momento.

Configure um Back-end

Depois da execução, o Terraform gera um arquivo de estado de ambiente (.tfstate), e quando não há um back-end configurado este arquivo é gerado de forma local, ou seja, inviabiliza a utilização por um time. Ou seja, ter um back-end é um pré-requisito para trabalhar com múltiplos usuários.

O arquivo .tfstate é importantíssimo para que você consiga realizar evoluções no ambiente. Em caso de perda, o terraform simplesmente não tem como identificar e listar os recursos provisionados para considerar o que deve ser criado, destruído e/ou modificado.

Bucket S3 (com DynamoDB)

A configuração de um bucket AWS S3 é bem simples. Basta criar um bucket seguindo as recomendações de segurança (por exemplo não-público e com uma criptografia padrão AES256 ou AWS KMS). A configuração é basicamente assim:

terraform {
backend "s3" {
bucket = "<bucket name>"
key = "terraform.tfstate"
region = "us-east-1"
encrypt = true
}
}

Essa configuração é opcional, mas é interessante para evitar a concorrência de uso do “back-end”, o que evita comportamentos inesperados durante a execução no Terraform. É bem simples:

terraform {
backend "s3" {
bucket = "<bucket name>"
key = "terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock-dynamo"
}
}

Crie workspaces

Além de otimizar o fluxo de trabalho, criar workspaces facilita a evolução do “infra-as-code”, evita repetição de código e mantém a equivalência entre os ambientes produtivos e não-produtivos. Por exemplo, ao invés de manter uma estrutura semelhante a esta:

$ terraform workspace new <ambiente>

Utilizamos o mesmo código, mas com um ambiente “alvo”, simplificando o “chaveamento”:

$ terraform workspace list
$ terraform workspace select <ambiente>

Nesta altura, já temos um “back-end” e o “workspace” configurados. Agora precisamos protegê-los, ou fazer com que um “workspace” de produção não seja deletado. Se você quiser saber os detalhes da política (AWS S3) é só olhar esta documentação.

Utilize módulos

Como em uma linguagem de programação qualquer, módulos são basicamente “funções”, que processam um ou alguns “resources” em conjunto e evita a repetição de código.

Por exemplo, para provisionar um recurso da AWS (Lambda), que não é necessariamente um recurso isolado, é bem provável que você dependa de permissões (IAM) e estímulos (SNS), e normalmente é uma estrutura semelhante a esta:

├── terraform-lambda
│ ├── hello_lambda.py
│ ├── iam.tf
│ ├── lambda.tf
│ ├── outputs.tf
│ └── vars.tf
  • hello_lambda.py — script exemplo em Python para o lambda.
  • iam.tf — script Terraform para provisionar um AWS Policy e Role para uso no Lambda.
  • lambda.tf — script Terraform para provisionar o lambda.
  • outputs.tf — script que determina a saída de informações pós-execução (Ex: ARN do lambda)
  • vars.tf — arquivo com variáveis para execução no Terraform.

Se você observar bem este exemplo de código para Lambda (lambda.tf), pode ver que um lambda denominado “hello_lambda” foi provisionado.

# Specify the provider and access details
provider "aws" {
region = "${var.aws_region}"
}
provider "archive" {}data "archive_file" "zip" {
type = "zip"
source_file = "hello_lambda.py"
output_path = "hello_lambda.zip"
}
resource "aws_lambda_function" "lambda" {
function_name = "hello_lambda"
filename = "${data.archive_file.zip.output_path}"
source_code_hash = "${data.archive_file.zip.output_base64sha256}"
role = "${aws_iam_role.iam_for_lambda.arn}"
handler = "hello_lambda.lambda_handler"
runtime = "python3.6"
environment {
variables = {
greeting = "Hello"
}
}
}

“Mas e se eu precisar provisionar alguns Lambdas, com diferentes características, alguns com estímulos, outros sem e assim por diante?”

Com a modularização dos recursos é possível atribuir parâmetros em sua “função”, evitando repetições de código. Para isso, inicialmente teríamos que modificar a estrutura atual do Terraform para um modelo semelhante a este:

└── terraform-lambda-as-module
├── main.tf
├── modules
│ └── lambda
│ ├── function
│ │ └── hello_lambda.py
│ └── lambda.tf
├── provider.tf
└── vars.tf
  • (NEW) main.tf — arquivo principal que será usado para o input de dados para o módulo.
  • (NEW) modules/lambda/* — todos os arquivos utilizados pelo lambda serão movidos para este diretório para modularização.
  • vars.tf — arquivo com variáveis para execução no Terraform.
  • provider.tf — arquivo com configurações do Cloud Provider utilizado, neste caso, AWS.

No novo arquivo “main.tf” só está declarado o módulo com a sua localização “source”, e os dados do “vars.tf” estão sendo interpolados.

module "lambda" {
source = "modules/lambda"
LAMBDA_SETTINGS = "${var.HELLO_LAMBDA["settings"]}"
LAMBDA_VARIABLES = "${var.HELLO_LAMBDA["variables"]}"
}

O “vars.tf” também mudou, agora possui as informações do lambda para serem interpoladas:

variable "HELLO_LAMBDA" {
type = "map"
default = {
settings = {
function_name = "hello_lambda"
handler = "hello_lambda.lambda_handler"
runtime = "python3.6"
source_file = "hello_lambda.py"
}
variables = {
greeting = "Hello"
}
}
}

Repare que houve uma mudança no “lambda.tf” no exemplo anterior. As opções do lambda estavam diretamente configuradas nos “resources”, e neste caso os dados estão sendo interpolados. É por isso que existem duas variáveis do tipo “chave/valor” (LAMBDA_VARIABLES/LAMBDA_SETTINGS), pois estamos convertendo o “lambda.tf” para operar como um “módulo” reaproveitável:

variable "LAMBDA_VARIABLES" {
type = "map"
}
variable "LAMBDA_SETTINGS" {
type = "map"
}
data "archive_file" "zip" {
type = "zip"
source_file = "${path.module}/function/${var.LAMBDA_SETTINGS["source_file"]}"
output_path = "${path.module}/function/${var.LAMBDA_SETTINGS["function_name"]}.zip"
}
resource "aws_lambda_function" "lambda" {
function_name = "${var.LAMBDA_SETTINGS["function_name"]}"
filename = "${data.archive_file.zip.output_path}"
source_code_hash = "${data.archive_file.zip.output_base64sha256}"
role = "${aws_iam_role.iam_for_lambda.arn}"
handler = "${var.LAMBDA_SETTINGS["handler"]}"
runtime = "${var.LAMBDA_SETTINGS["runtime"]}"
environment {
variables = "${var.LAMBDA_VARIABLES}"
}
}
data "aws_iam_policy_document" "policy" {
statement {
sid = ""
effect = "Allow"
principals {
identifiers = ["lambda.amazonaws.com"]
type = "Service"
}
actions = ["sts:AssumeRole"]
}
}
resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda_${var.LAMBDA_SETTINGS["function_name"]}"
assume_role_policy = "${data.aws_iam_policy_document.policy.json}"
}

Ou seja, se precisarmos criar múltiplos lambdas, é possível usar o mesmo módulo, apenas interpolando dados diferentes!

Com o uso avançado dos módulos, o próximo passo é o versionamento deles, o que vai tornar possível a utilização entre diferentes projetos Terraform. Se você quiser saber mais é só clicar neste link.

Obs: se você tiver interesse, o exemplo está disponível no GitHub.

Declare as versões utilizadas em módulos externos

Recentemente o Terraform passou por uma grande atualização da versão 11 (que foi a que usei neste post) para 12. São novas features, correções de bugs e mudanças em alguns casos de sintaxe. Se você for usar módulos de terceiros ou oficiais, indique qual é a versão compatível com o seu projeto:

module "redshift" {
version = "1.6.0"
source = "terraform-aws-modules/redshift/aws"

Assim você economiza algum tempo de troubleshooting, como tentando identificar porque o Terraform parou de funcionar do “dia para noite”, ou se em algum momento você precisou excluir o diretório “.terraform”.

Dados sensíveis

Ao usar o Terraform, tenha MUITO cuidado com dados sensíveis. Seguem algumas dicas:

  • Usando o .tfstate local as informações ficam em texto plano, mas usando um S3 como backend remoto é possível aplicar criptografia em repouso (KMS ou AES256);
  • Use “Dummy” password (se for realmente necessário);
  • É possível integrar o Terraform ao Vault;
  • Cuidado ao expor credenciais de Cloud Providers (ex: AWS) no código. use variáveis de ambiente;
  • GitCrypt pode ser usado, por exemplo, no vars.tf.

Equalizando conhecimento com demais integrantes do time

Talvez nem todos do seu time tenham domínio da utilização do Terraform. Essa é uma boa hora para passar conhecimento, seja com alguns tutoriais ou com documentações. Aqui tem algumas recomendações:

Documentações

Ferramentas

Adote um fluxo de desenvolvimento (Gitflow)

Esse é o passo de definição do “workflow” de desenvolvimento para a evolução da infraestrutura.

Fonte: https://twitter.com/sheriffjackson

Utilize uma ferramenta para Continuous Integration/Deployment

Como Gitlab, Travis e outras. No caso do GitLab CE existe um template (.gitlab-ci.yml) para a execução da Pipeline que utiliza o Terraform como container (Docker):

# This file is a template, and might need editing before it works on your project.
# Official image for Hashicorp's Terraform. It uses light image which is Alpine
# based as it is much lighter.
#
# Entrypoint is also needed as image by default set `terraform` binary as an
# entrypoint.
image:
name: hashicorp/terraform:light
entrypoint:
- '/usr/bin/env'
- 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
# Default output file for Terraform plan
variables:
PLAN: plan.tfplan
cache:
paths:
- .terraform
before_script:
- terraform --version
- terraform init
stages:
- validate
- build
- deploy
validate:
stage: validate
script:
- terraform validate
plan:
stage: build
script:
- terraform plan -out=$PLAN
artifacts:
name: plan
paths:
- $PLAN
# Separate apply job for manual launching Terraform as it can be destructive
# action.
apply:
stage: deploy
environment:
name: production
script:
- terraform apply -input=false $PLAN
dependencies:
- plan
when: manual
only:
- master

Fonte: Template Oficial GitLab CI

Automatize testes

Existe a possibilidade de incluir testes unitários e de aceitação do código Terraform desenvolvido utilizando a linguagem “Go”, e também algumas ferramentas de auxílio de execução de testes Terratest e “sandbox” Terragrunt. Abaixo seguem os links para as documentações:

Dicas finais

O Terraform ainda tem recursos para validação de sintaxe e até mesmo de indentação de código. Use e abuse durante o desenvolvimento!

  • Validação de sintaxe
$ terraform validate
  • Formatação/indentação de código
$ terraform fmt

Mais links e referências

E é isso! Ficou alguma dúvida ou tem algo a contribuir? Deixe um comentário e vamos conversar. Até a próxima!

--

--