Como usar GitOps para automatizar o Terraform
Em vez de usar pipelines de CI/CD ou Terraform Cloud, tente esta abordagem alternativa para automatizar o Terraform usando Flux e GitOps.
GitOps como fluxo de trabalho é perfeito para entrega de aplicativos, usado principalmente em ambientes Kubernetes, mas também é possível usar para infraestrutura. Em um cenário típico de GitOps, você pode querer considerar soluções como o Crossplane como uma alternativa nativa do Kubernetes, enquanto a maioria das infraestruturas tradicionais ainda são usadas com pipelines de CI/CD. Há vários benefícios em criar sua plataforma de implantação com o Kubernetes como base, mas também significa que mais pessoas precisariam ter esse conjunto específico de habilidades. Um dos benefícios de uma ferramenta de infraestrutura como código como o Terraform é que ela é fácil de aprender e não requer muito conhecimento especializado.
Quando minha equipe estava construindo nossos serviços de plataforma, queríamos que todos pudessem contribuir. A maioria, senão todos, de nossos engenheiros usa o Terraform diariamente. Eles sabem criar módulos Terraform que podem ser utilizados em diversos cenários e para diversos clientes. Embora existam várias maneiras de automatizar o Terraform, gostaríamos de utilizar um fluxo de trabalho GitOps adequado, tanto quanto possível.
Como funciona o controlador Terraform
Ao procurar alternativas para executar o Terraform usando Kubernetes, encontrei vários controladores e operadores, mas nenhum que eu achasse que tivesse tanto potencial quanto o tf-controller da Weaveworks. Já estamos usando o Flux como nossa ferramenta GitOps. O tf-controller funciona utilizando algumas das principais funcionalidades do Flux e possui um recurso personalizado para implantações do Terraform. O controlador de origem se encarrega de buscar nossos módulos, os controladores Kustomize aplicam os recursos do Terraform e, em seguida, o controlador ativa pods estáticos (chamados de executores) que executam seus comandos do Terraform.
O recurso Terraform é mais ou menos assim:
apiVersion: infra.contrib.fluxcd.io/v1alpha1
kind: Terraform
metadata:
name: helloworld
namespace: flux-system
spec:
interval: 1m
approvePlan: auto
path: ./terraform/module
sourceRef:
kind: GitRepository
name: helloworld
namespace: flux-system
Há algumas coisas a serem observadas nas especificações aqui. O intervalo na especificação controla a frequência com que o controlador inicia os pods do executor. Isso então executa um terraform plan
em seu módulo raiz, que é definido pelo parâmetro path.
Este recurso específico está configurado para aprovar automaticamente um plano. Isso significa que se houver uma diferença entre o plano e o estado atual do sistema de destino, um novo executor será executado para aplicar as alterações automaticamente. Isso torna o processo o mais “GitOps” possível, mas você pode desabilitar isso. Se você desativá-lo, terá que aprovar os planos manualmente. Você pode fazer isso usando a CLI do Terraform Controller ou atualizando seus manifestos com uma referência ao commit que deve ser aplicado. Para mais detalhes, consulte a documentação sobre aprovação manual.
O controlador tf utiliza o controlador de fonte do Flux. O atributo sourceRef
é usado para definir qual recurso de origem você deseja usar, assim como faria um recurso Flux Kustomization.
Implantações avançadas
Embora o exemplo acima funcione, não é o tipo de implantação que minha equipe normalmente faria. Ao não definir um armazenamento de back-end, o estado seria armazenado no cluster, o que é adequado para testes e desenvolvimento. Mas para produção, prefiro que o arquivo de estado seja armazenado em algum lugar fora do cluster. Não quero que isso seja definido diretamente no módulo raiz, pois quero reutilizar nossos módulos raiz em várias implantações. Isso significa que tenho que definir nosso back-end em nosso recurso Terraform.
Aqui está um exemplo de como configuro configurações de back-end personalizadas. Você pode encontrar todos os back-ends disponíveis nos documentos do Terraform:
apiVersion: infra.contrib.fluxcd.io/v1alpha1
kind: Terraform
metadata:
name: helloworld
namespace: flux-system
spec:
backendConfig:
customConfiguration: |
backend "azurerm" {
resource_group_name = "rg-terraform-mgmt"
storage_account_name = "stgextfstate"
container_name = "tfstate"
key = "helloworld.tfstate"
}
...
Armazenar o arquivo de estado fora do cluster significa que posso reimplantar nosso cluster. Mas então não há dependência de armazenamento. Não há necessidade de backup ou migração de estado. Assim que o novo cluster estiver ativo, ele executará os comandos no mesmo estado e eu voltarei aos negócios.
Outro movimento avançado são as dependências entre módulos. Às vezes, projetamos implantações como um foguete de dois estágios, onde uma implantação configura determinados recursos que a próxima utiliza. Nesses cenários, precisamos ter certeza de que nosso Terraform foi escrito de tal maneira que possamos gerar todos os dados necessários como entradas para o segundo módulo e garantir que o primeiro módulo seja executado com sucesso primeiro.
Esses dois exemplos são de código usado para demonstrar dependências, e todo o código pode ser encontrado em meu GitHub. Parte do código é omitida por questões de brevidade:
apiVersion: infra.contrib.fluxcd.io/v1alpha1
kind: Terraform
metadata:
name: shared-resources
namespace: flux-system
spec:
...
writeOutputsToSecret:
name: shared-resources-output
...
apiVersion: infra.contrib.fluxcd.io/v1alpha1
kind: Terraform
metadata:
name: workload01
namespace: flux-system
spec:
...
dependsOn:
- name: shared-resources
...
varsFrom:
- kind: Secret
name: shared-resources-output
...
Na implantação que chamo de recursos compartilhados, você vê que defini um segredo onde as saídas da implantação devem ser armazenadas. Neste caso, as saídas são as seguintes:
output "subnet_id" {
value = azurerm_virtual_network.base.subnet.*.id[0]
}
output "resource_group_name" {
value = azurerm_resource_group.base.name
}
Na implantação workload01, primeiro defino nossa dependência com o atributo dependsOn
, que garante que recursos compartilhados sejam executados com êxito antes de agendar < forte>carga de trabalho01. As saídas de recursos compartilhados são então usadas como entradas em workload01, e é por isso que quero esperar.
Por que não pipelines ou Terraform Cloud
A abordagem mais comum para automatizar o Terraform é usando pipelines de CI/CD ou Terraform Cloud. Usar pipelines para Terraform funciona bem, mas geralmente acaba sendo necessário copiar as definições de pipeline repetidamente. Existem soluções para isso, mas ao usar o tf-controller você tem uma abordagem muito mais declarativa para definir como deseja que suas implantações sejam, em vez de definir as etapas de maneira imperativa.
O Terraform Cloud introduziu muitos recursos que se sobrepõem ao uso do fluxo de trabalho GitOps, mas o uso do tf-controller não exclui você do uso do Terraform Cloud. Você poderia usar o Terraform Cloud como backend para sua implantação, automatizando apenas as execuções por meio do tf-controller.
A razão pela qual minha equipe usa essa abordagem é que já implantamos aplicativos usando GitOps e temos muito mais flexibilidade sobre como podemos oferecer esses recursos como um serviço. Podemos controlar nossa implementação por meio de APIs, tornando o autoatendimento mais acessível tanto para nossos operadores quanto para usuários finais. Os detalhes em torno de nossa abordagem de plataforma são um tópico tão importante que teremos que voltar a eles em seu próprio artigo.
Este artigo foi publicado originalmente no blog do autor e republicado com permissão.