Infraestrutura Como Código Com Terraform

| Comments

Infraestrutura como Código (IAC – Infrastructure as Code)

Infraestrutura como código – IaC (ou infrastructure as code em inglês) – é o processo de gerenciamento e provisionamento de recursos de infraestrutura através de códigos ou arquivos de configuração que descrevem o estado desejado para tal infraestrutura ou recursos de infraestrutura. A principal característica de IaC é o uso de scripts ou definições declarativas ao invés de processos manuais, mas o termo é utilizado com mais frequência para promover abordagens declarativas. Como se tratam de arquivos de código, as definições podem ser armazenadas em um sistema de controle de versões, tal como o Git.

Abordagens IaC são comumente promovidas para computação em nuvem e, às vezes, são comercializadas como infraestrutura como serviço (infrastructure as a service, IaaS). IaC suporta IaaS, mas os dois conceitos não devem ser confundidos.

IaC e DevOps

IaC, ou Infraestrutura como Código, é um conceito bastante ligado à filosofia DevOps, visto que com práticas de implementação de uma infraestrutura baseada em códigos declarativos podemos aproximar as equipes de Operações e Desenvolvimento, fazendo com que os desenvolvedores tornem-se mais envolvidos nas configurações de máquinas ou recursos de infraestrutura como um todo, enquanto que os profissionais de Operações se envolvem mais cedo no processo de desenvolvimento. Além disso, agora ambas as equipes podem armazenar seu código em um mesmo ambiente, como por exemplo repositórios Git.

Infraestrutura como código mostrou-se uma excelente solução para livrar equipes de tarefas enfadonhas do cotidiano realizadas manualmente. Além de tomarem muito tempo e serem tarefas extremamente repetitivas, os corriqueiros processos manuais estão sujeitos a erros e podem colocar as operações em risco.

Algumas vantagens da utilização de IaC

  • Elimina tarefas repetitivas – Se você precisa criar 3 clusters Kubernetes em seu provedor de cloud (GCP ou AWS, por exemplo), você não precisa repetir os mesmos passos 3 vezes. Escreve um bloco de código que define a criação de um cluster e poderá aplicar este mesmo código quantas vezes forem necessárias;

  • Documentação simplificada – Não há necessidade de logar-se em um servidor ou provedor de cloud para tentar vasculhar tudo o que foi configurado (está tudo no código);

  • Reaproveitamento – Uma vez que tudo está codificado e separado em módulos, fica fácil reaproveitar módulos e código para futuras implementações;

  • Simples manutenção – Mudanças na configuração, versões, regras e demais definições podem ser implementadas e aplicadas rapidamente com pequenas alterações no código;

  • Versionamento – Ao abordar nossa infraestrutura como código passamos a ter diversos benefícios já rotineiros para desenvolvedores, como por exemplo a possibilidade de gerenciar nosso código em sistemas de versionamento como o Git, de forma a facilitar o trabalho em equipes, controle de versões, mudanças, etc;

  • Agilidade – Se preciso trocar a faixa de endereços IP de uma VPC ou subnet, alterar uma linha de código é muito mais rápido do que logar em uma dashboard, procurar tal recurso e alterar manualmente os valores desejados;

  • Possibilidade de soluções agnósticas – Em um mundo tecnológico que muda constantemente não são raras as ocasiões em que temos de mudar completamente nossa infraestrutura, seja deixando de usar servidores físicos para passar a utilizar VMs, ou migrando de VMs locais para a nuvem, ou de VMs na nuvem para containers, etc. Independente de qual seja o cenário de mudança, uma vez que sua infraestrutura está definida em código, dependendo das ferramentas escolhidas para codificar sua infra, o mesmo código poderia ser utilizado para um ambiente VMWare, AWS, Azure, GCP, etc, com poucas modificações. (Já citei agilidade e Simples manutenção, certo?!);

  • Fácil replicar – Em um ambiente não codificado ou automatizado, geralmente repetimos as mesmas configurações para criarmos ambientes distintos como Produção, Teste, Desenvolvimento, etc. Uma vez que sua infra está codificada, você aplica o mesmo código para criar quantos ambientes desejar;

  • Recuperação de Desastres ou Disaster Recovery – Desastres acontecem. Imagine um problema grande em sua infraestrutura. Em um cenário de virtualização, imagine que seu host VMWare simplesmente parou de funcionar pois seu disco queimou. Ou que o storage onde se encontravam as suas VMs simplesmente foi destruído. Ou, em um ambiente de cloud, imagine que sua senha de administrador da nuvem vazou e seu ambiente foi completamente excluído. Ou mesmo que o próprio data center ou região na qual se encontra a sua infra estrutura teve algum problema sério e toda a sua infra caiu. Claro, as boas práticas já pregam há muito tempo que sempre devemos ter backups de todos os servidores e sistemas, mas backups nada mais são do que arquivos. E a infraestrutura de fato? Você precisa ter uma infraestrutura ativa antes de conseguir restaurar backups, certo? Rede, firewall, VPCs, Clusters, Servidores, etc.. Se você possui toda a sua infraestrutura em código, recuperar tudo isso é tão simples quanto executar um único comando.

  • Planejamento (plan) e Testes – Práticas como planejamento e testes fazem parte (ou ao menos deveríam fazer) da rotina de praticamente qualquer desenvolvedor. Profissionais de infraestrutura sempre tiveram uma desvantagem em relação a isso, pois era complicado fazer testes de infraestrutura. Teste basicamente significava instalar exatamente a mesma infraestrutura em um ambiente isolado para testes. Ainda possível, porém pouco confiável e com altas chances de falhas, pois, por ser um processo manual e lento, não há qualquer garantia de que todos os mesmos passos serão executados no ambiente de produção tal como foram executados no ambiente de teste. Com infraestrutura como código, fica fácil utilizar-se das mesmas técnicas de planejamento e testes automatizados há muito utilizadas por desenvolvedores. Agora você consegue executar testes no código de sua infraestrutura que, literalmente, irão avaliar cada bloco de seu código e simular a execução de cada expressão ou descrição, dando-lhe assim uma visão geral sobre o que acontecerá, o que funcionará e o que falhará, garantindo uma integradade fiel entre teste e implantação em produção ou demais ambientes.

Lindo, não? Lembro de meus tempos de faculdade, quando costumava dizer a meus colegas que todos os profissionais de TI deveríam saber programar, independente de desejarem ou não trabalhar com programação. Na ocasião, em meados de 2006, a maioria deles dizia que isso era loucura. “Porque aprender a programar se vou trabalhar com infraestrutura?” Bom, tudo o que posso dizer hoje é: Bem vindos à era DevOps.

Quando falamos em infraestrutura como código, existem diversas ferramentas que trabalham em cima deste conceito, e muitas delas atuam juntas para englobar soluções mais completas, mas uma de minhas favoritas é o Terraform, da Hashicorp.

Escolha da ferramenta ideal para IAC

Com uma simples busca no Google pelo termo “ïnfraestrutura como código” ou “IAC”, será extremamente fácil encontrar diversas ferramentas, dentre as mais populares estão Chef, Puppet, Ansible, SaltStack, Terraform, CloudFormation, etc.

Um problema comum para quem inicia no mundo DevOps ou simplesmente deseja começar a utilizar infraestrutura como código é justamente a escolha. Qual a melhor ferramenta? Qual devo utilizar?

Eesta é uma dificuldade comum e inerante ao fato de se ter muitas opções. Se em uma sorveteria você só possui os sabores chocolate e baunilha, é extremamente simples optar por um, outro ou nenhum dos dois. No entanto, em uma sorveteria com 50 sabores, você provavelmente perderá alguns minutos apenas lendo todas as opções, do contrário irá apostar na sorte e escolher o primeiro que lhe parecer apetitoso, correndo o risco de descartar algum que não chegou a ver, mas que poderia ser muito melhor e refrescante para um dia quente como os do nordeste cearense.

O mais importante é sempre realizar uma pesquisa sobre pontos fortes e fracos de cada uma delas antes de tomar uma decisão, tendo em mente alguns aspectos:

  • A escolha não deve (ou não deveria) ser puramente pessoal. A melhor ferramenta dificilmente será a que você gostou mais de utilizar. A melhor ferramenta será a que melhor atende as necessidades do seu projeto ou negócio;

  • A análise deve ser feita em diversos aspectos, e não apenas em um ou dois. Supondo que você pesquise por exemplo quais possuem mais módulos gratuitos e quais delas possuem uma comunidade mais ativa na internet para dúvidas, mas esqueceu de ponderar o preço para ter acesso a suporte corporativo, você pode ter feito uma boa ou uma má escolha. Em caso de ser uma pequena empresa, com prazos relativamente longos para entregas de projetos e maior flexibilidade em termos de tempo fora do ar (downtime), o suporte corporativo pode não ser um fator decisivo. No entanto, em uma empresa de ambiente mais crítico, como um banco, demais sistemas financeiros, governamentais, etc., o suporte corporativo se torna um fator mais importante, portanto escolher uma ferramenta sem ponderar o valor de seu suporte corporativo acaba sendo um tiro no pé, o que reforça a ideia de que sempre devemos avaliar o máximo de aspectos possíveis e relevantes ao nosso projeto ou ambiente;

  • Outro fator fundamental e, em meu ver o mais importante, é entender que não necessariamente a escolha será exclusiva. Imagine que você precisa montar uma mesa que veio toda desmontada: tábuas, parafusos, gavetas, etc. Você tem algumas ferramentas a disposição, como chave de fendas, martelo, régua, serra, furadeira, etc. Você pode ser uma espécie de rambo e gostar de resolver as coisas com uma ferramenta só e, sinceramente, você pode até ser capaz de conseguir montar a mesa inteira apenas utilizando o martelo, parabéns por isso. Mas será que essa é a forma mais eficiente de resolver o problema? Será que não seria mais rápido e organizado utilizando um martelo e uma chave de fendas? Afinal, temos parafusos também, certo?!

Conforme descrito acima, o ideal é sempre avaliar o projeto ou ambiente no qual se irá trabalhar, sem a necessidade de escolher apenas uma ferramenta.

Sim, Chef, Puppet e Ansible são ferramentas de Infraestrutura como Código, no entanto elas possuem tarefas mais específicas, nas quais possuem mais desempenho, como por exemplo o gerenciamento de configurações, para não listar todas as suas funções.

E, claro, o CloudFormation é uma excelente ferramenta da Amazon para criação de infraestrutura como código, no entanto ela fica restrita ao ambiente de cloud da Amazon, o AWS. E se meu projeto estiver utilizando VMs em um ambiente VMWare? Ou se eu utilizar Google Cloud? Ou mesmo um ambiente mais heterogênio, com VMWare, Google Cloud e Amazon AWS? O CloudCloudFormation não seria a melhor opção, por ser restrito ao ambiente AWS.

Sempre fui a favor de utilizar as ferramentas corretas para cada tarefa em específico, portanto porque utilizar apenas uma se tenho outras disponíveis?

O Terraform, por outro lado, é específico para a criação da infraestrutura base, se saindo muito melhor do que Chef, Ansible ou Puppet nesta tarefa, mas nada impede (e eu encorajo e o faço) que você utilize Chef, Puppet ou Ansible, para gerenciar configurações, bootstraping ou deployments na infraestrutura criada pelo Terraform.

Além do mais, diferente do CloudFormation, o Terraform é uma solução agnóstica, permitindo-lhe criar infraestrutura em praticamente qualquer ambiente, seja ele em Cloud (Amazon AWS, Microsoft Azure, Google GCP, IBM Cloud, Digital Ocean, etc.), ambiente virtualizado, local ou em Data Centers (VMWare, Xen, Virtual Box, etc.), Docker, Kubernetes, além de recursos diversos de infraestrutura e softwares, tais como Redes (CloudFlare, DNS, DNSimple, F5 BIG-IP, Palo Alto Networks,etc.), Bancos de Dados (InfluxDB, MySQl ou PostgreSQL), dentre muitas outras coisas.

Terraform

O Terraform, da Hashicorp, lhe permite criar, alterar e melhorar sua infraestrutura de forma segura e previsível. É uma ferramenta Open Source que codifica APIs em arquivos de configuração declarativos que podem ser compartilhados entre membros de um time, tratados como código, editados, revisados e versionados.

A imagem acima descreve bem o fluxo básico da utilização de Terraform para codificar sua infraestrutura, na qual o fluxo mais simplista é:

  • Escrever o código de sua infraestrutura;
  • Planejar a execução do seu código, de forma que você receba informações antecipadamente de tudo o que acontecerá quando você aplicar o seu código;
  • Crie uma infraestrutura reproduzível ao aplicar seu código.

Apesar de este ser o fluxo mais simplista, com a utilização de infraestrutura como código você pode melhorar seu fluxo inserindo colaboração e compartilhamento, armazenando e gereciando seu código em um repositório git, por exemplo, além de ter assim um registro completo das mudanças e evoluções de sua infraestrutura, facilitando a automação em fluxos mais complexos, como por exemplo em pipelines de Integração Contínua.

O Terraform funciona basicamente através de recursos, ou resources, que definem o tipo de infraestrutura você estará criando bem como seus atributos. Além disso, conforme dito anteriormente, o Terraform também pode ser utilizado em paralelo com diversas outras ferramentas de automação em forma de provedores, ou providers, como Puppet, Chef, Ansible, etc.

Por estarmos tentando aplicar para a infraestrutura um conceito que seja mais próximo do que já era utilizado por desenvolvedores há muito tempo, o Terraform possui também uma abordagem que lhe permite reaproveitamento de código, através de módulos. Existem diversos módulos criados e disponibilizados gratuitamente, mas você pode também criar seus próprios módulos de forma a melhor organizar e reaproveitar seu próprio código em diversos projetos.

Arquivos de configuração descrevem ao Terraform os componentes necessários para rodar uma única aplicação que representa todo o seu datacenter. O Terraform gera um plano de execução descrevendo o que fará para alcançar o estado desejado, e em seguida, caso aprovado, o executará para criar a infraestrutura desejada. Conforme a configuração muda, o Terraform será capaz de determinar o que mudou e criará planos de execução incrementais que podem ser aplicados.

Vejamos o seguinte diagrama que descreve uma simples infraesturura. Imaginemos que esta é a infraestrutura que queremos rodar em nossa conta no Google Cloud para termos um site de e-commerce:

O diagrama acima possui diversos elementos:

  1. Uma organização no Google Cloud chamada Kalib Avante;
  2. Um diretório ou Folder (como chamado no Google Cloud) chamado Projetos;
  3. Um projeto chamado E-Commerce;
  4. Um projeto chamado Zebra Feliz;
  5. Dentro do projeto E-Commerce temos dois ambientes: Produção e Teste
  6. Repare que o projeto Zebra Feliz está incompleto e sem ambientes distintos, como Produção, teste, etc. Bom, trata-se de um projeto piloto ainda em desenvolvimento e planejamento, portanto os recursos não foram ainda criados por completo. Mas o Terraform nos permite incrementar recursos quando necessário, certo? Portanto, sem problemas com isto por enquanto.

Esta é a estrutura básica, já em termos de recursos temos Firewalls, clusters kubernetes com Nodes, buckets de storage para conservar o status ou state do Terraform e por consequência de sua infraestrutura, Discos persistentes, Container Registry (repositório de imagens Docker), VPCs, Load Balancer, VMs, etc.

Caso esteja se perguntando, sim o Terraform lhe permite criar esta infraestrutura inteira, bem como outras bem mais complexas, com mais projetos, mais ambientes, mais recursos, etc.

Desta forma podemos ter um código terraform dividido em alguns módulos, armazenado em um repositório Git, por exemplo, e criar toda essa infraestrutura, desde a Organização vazia, ao diretório de projetos, aos 2 projetos em si, buckets, clusters kubernetes, DNS, IAM, load balancer, VMs por trás do Load Balancer, etc. Tudo isto com um único comando:

1
terraform apply

Lembrando um pouco do que falamos lá em cima, sobre ser simples reproduzir, ou se recuperar de desastres… imagine que uma região inteira caiu no Google, onde temos nossa infraestrutura. Sim, eu sei que isso é extremamente raro, mas vamos imaginar os cenários mais absurdos e raros também. Imagine que perdi toda a minha infraestrutura. Imagine ter que recriar tudo isso (projetos, diretórios, storages, DNS, IAM, clusters, Load Balancer, VMs, etc, etc..) manualmente..? Demoraria bastante, certo?! Mas, como fomos espertos e criamos tudo via terraform, um simples terraform apply irá criar tudo novamente para nós, exatamente como era antes.

O mesmo se dá caso precisemos recriar toda essa mesma infraestrutura em uma nova região do Google, ou caso queiramos destruir nossa infra e recriá-la em outra zona, por algum motivo.

Aqui estamos lidando apenas com a Infraestrutura pois, conforme dito antes, o Terraform é excelente para criar a infraestrutura, mas o ideal ainda é utilizar outros softwares para provisionamento e deployment. Por exemplo, uma vez que temos nossa infraestrutura inteira criada, podemos começar a provisionar os sistemas e softwares através de outras ferrmentas, como Helm (para deployment dentro dos clusters Kubernetes), Chef, Puppet, Ansible, etc.

A ideia deste post é apenas dar uma introdução teórica, uma ideia de como infraestrutura como código funciona, e de como o Terraform é capaz de fazer tudo isso de forma segura e robusta. (Embora eu já veja uma enorme barra de rolagem aqui ao lado e sei que lhe fiz ler bastante, supondo que leu até aqui. :p)

Em meu próximo post pretendo fazer uma abordagem mais prática e com mão na massa, utilizando de fato o Terraform para criar uma simples infraestrutura via código.

Happy Hacking!

Criando Uma Imagem AWS EC2 Com Packer E Puppet

| Comments

Packer, Puppet, Bash e AWS

A plataforma AWS da Amazon é atualmente uma das maiores e mais populares quando o assunto é Cloud e automação em nuvem, permitindo o uso de soluções de infra estrutura completamente na nuvem, sem a necessidade de termos Hardware físico, otimizando custos e nos dando mais flexibilidade.

A própria plataforma nos disponibiliza diversos recursos para facilitar a implementação de nossas soluções e tornar nossas tarefas rotineiras mais simples. Por exemplo, para a criação de VMs, ou instâncias, no AWS de forma mais rápida, podemos utilizar uma imagem previamente criada, de forma que possamos evitar alguns passos e configurações repetitivas.

Uma vez que eu identifico uma necessidade para minha aplicação e sei que preciso de uma máquina virtual com configurações e aplicações específicas para poder rodar minha aplicação, eu posso criar uma imagem com todos estes pré-requisitos de forma que ao resolver criar uma nova VM, eu não precise realizar todos estes passos manualmente. Além de evitar trabalho repetitivo, nos garante uma maior flexibilidade ao ter nossa infraestrutura como código, de forma que podemos literalmente ter as instruções que compõem nossa infraestrutura em um repositório Git, por exemplo, além de nos permitir realizar alterações nesta imagem também de forma simples e rápida para a geração de novas imagens de instâncias com as nossas alterações em poucos segundos ou minutos, dependendo da quantidade de alterações envolvdidas.

Se você ainda não faz ideia de o que seja o Packer ou o que ele é capaz de fazer, sugiro que volte uma casa e leia meu post anterior, onde explico o que é o Packer e apresento um simples exemplo de seu uso para a criação de imagens para o Docker.

O intuito deste post é mostrar como podemos estruturar um simples código para que possamos criar uma imagem no AWS que poderá ser utilizada posteriormente para a criação de instâncias. Esta imagem será criada através do Packer e, para incrementar ainda mais nossa imagem, utilizaremos o recurso de provisioners (ou provisionadores/provedores) disponível no Packer. Utilizaremos dois provisioners como recursos externos para o provisionamento e configuração de nossa imagem, sendo eles bash script e Puppet.

AWS

Uma vez que estou assumindo que você já possui o Packer instalado, bem como que você já possui uma ideia de como ele funciona, vamos iniciar pelo AWS. (Ainda não possui o Packer e não sabe o que ele faz? Novamente, volte uma casa.)

O primeiro pré-requisito para este post/tutorial é uma conta no AWS. Caso você não possua uma e queira repetir os passos aqui descritos, siga e crie uma. Lembrando que o AWS lhe dá uma série de recursos que podem ser utilizados gratuitamente no que eles chamam de “Free Tier”. Uma vez que utilizaremos apenas recursos simples aqui, você não deverá ser cobrado por nada ao seguir os exemplos deste post. O ideal é que você exclua os recursos ou encerre sua conta após o término deste exercício para evitar ser cobrado por algo. Caso não o faça e resolva continuar testando algumas coisas no AWS, você pode ser cobrado em alguns centavos ou reais, dependendo de o que resolva testar e por quanto. (Sua responsabilidade, claro.)

A conta no AWS pode ser criada aqui: https://aws.amazon.com/free/

Uma vez que a conta no AWS esteja criada e pronta para uso, o primeiro passo será de fato conseguir uma chave para que possamos nos comunicar com o AWS via CLI através de uma API. Durante a criação de nosso código com o Packer precisaremos utilizar esta chave de acesso, portanto vá em frente e crie uma através deste link: https://console.aws.amazon.com/iam/home?#security_credential

Clique na opção Access Keys, ou Chaves de Acesso, e crie uma nova. É extremamente importante que você esteja atento neste momento, pois ele apenas lhe mostrará o ID e senha para a chave uma única vez, portanto esteja pronto para copiar e salvar ambos os valores. Será algo similar a isto:

É claro que eu já excluí essa chave… :p Não perca seu tempo… >]

Uma vez que você tenha salvo ambos os valores, vamos tratar da identificação/autenticação com o AWS.

O mecanismo padrão do Packer de autenticação neste caso seria através de duas variáveis em nosso arquivo json:

1
2
3
4
5
6
{
  "builders": [{
    "type": "amazon-ebs",
    "access_key": "SUA ACCESS KEY AQUI",
    "secret_key": "SUA SECRET ACCESS KEY AQUI",
... ... ...

Embora seja a forma mais simples, não a utilizaremos. Fica claro que não é uma forma muito segura, certo?! Quando se pensa em infraestrutura como código, um dos principais objetivos é podermos versionar e hospedar nosso código em um repositório Git, por exemplo. Ter nossa chave como parte do código não é nada seguro, especialmente se vamos compartilhar este código em um repositório Git.

A forma mais simples de lidarmos com isso é salvando nossa chave e senha como variáveis de ambiente e, em nosso arquivo json, importarmos estas variáveis de ambiente diretamente.

Em Linux ou OS X, digite o seguinte em um terminal ou console:

1
2
$ export AWS_ACCESS_KEY_ID=SUA ACCESS KEY AQUI
$ export AWS_SECRET_ACCESS_KEY=SUA SECRET ACCESS KEY AQUI

Certifique-se de que os valores foram definidos corretamente:

1
2
$ echo $AWS_ACCESS_KEY_ID
$ echo $AWS_SECRET_ACCESS_KEY

Por hora isso é tudo de que precisaremos para o AWS.

Packer

Hora de começarmos a escrever nosso código que será utilizado pelo Packer para a criação de nossa imagem.

Desta vez estaremos criando uma imagem EC2 para o AWS, portanto alguns provisioners e parâmetros serão diferentes dos utilizados no post anterior, onde criamos uma imagem para o Docker.

Comecemos criando um arquivo json vazio. Chamarei meu arquivo de ubuntuaws.json.

A primeira coisa que faremos é incluir as credenciais de nossa conta no AWS. Como criamos duas variáveis de ambiente em nosso host, chamadas AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY, invocaremos estas duas variáveis da seguinte forma no início de nosso arquivo: env `AWS_ACCESS_KEY_ID`, etc… Vamos ao código.

1
2
3
4
5
6
7
{
  "variables": {
    "aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",
    "aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",
    "region": "us-east-1"
  },
}

Normalmente uma varíavel poderia ser declarada apenas com “aws_access_key”: “sua_chave”, conforme fizemos com a variável region acima, no entanto, por questões de segurança, não queremos ter nossa chave exposta no código, certo?! Portanto, estamos trazendo os valores diretamente das variáveis de ambiente que criamos. A utilização do parâmetro env é o que indica ao Packer que ele deverá buscar estas variáveis em nosso env (environment).

É importante lembrar que o aws possui datacenters e recursos em diversas regiões do mundo. Você não precisa obrigatoriamente utilizar a região us-east-1. Optei por utilizar esta região em meu código pelo fato de eu morar em Toronto, o que faz desta região uma boa escolha para meus recursos de nuvem por conta da proximidade (menor delay).

Se você está no Brasil, provavelmente a melhor opção seja sa-east-1, a qual se encontra em São Paulo. De qualquer forma, você pode verificar a lista de regiões disponíveis no AWS através deste link.

Até então nosso código está simples e não faz basicamente nada além de definir as duas variáves para nossa autenticação, mas ainda assim é importante termos certeza de que não cometemos nenhum erro de sintaxe:

1
2
3
4
$ packer validate ubuntuaws.json
Error initializing core: 1 error(s) occurred:

* at least one builder must be defined

Por enquanto ignore este erro, nossa sintaxe esta correta. O Packer apenas está nos dizendo que não conseguiu iniciar o projeto pois ao menos um builder deve ser definido e, até então, nós não definimos nenhum. Este será o nosso próximo passo. Desta vez, ao invés de utilizarmos um builder do tipo Docker, utilizaremos um do tipo amazon-ebs. Começaremos inserindo uma vírgula ao fim do bloco de variáveis e nosso código agora ficará da seguinte forma:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  "variables": {
    "aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",
    "aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",
    "region": "us-east-1"
  },
"builders": [{
    "type": "amazon-ebs",
    "access_key": "{{user `aws_access_key`}}",
    "secret_key": "{{user `aws_secret_key`}}",
    "region": "{{user `region`}},
    "source_ami_filter": {
      "filters": {
      "virtualization-type": "hvm",
      "name": "ubuntu/images/*ubuntu-xenial-16.04-amd64-server-*",
      "root-device-type": "ebs"
      },
      "owners": ["099720109477"],
      "most_recent": true
    },
    "instance_type": "t2.micro",
    "ssh_username": "ubuntu",
    "ami_name": "packer-example {{timestamp}}"
  }]
}

O que temos agora:

builders: Inciamos nosso bloco de builders com as intruções ou parâmetros que definirão as especificações mais básicas para a criação de nossa imagem no AWS.

type: Aqui indicamos o tipo de builders que utilizaremos. No caso do EC2 do AWS, o tipo se chama amazon-ebs. Basicamente este builder irá utilizar uma imagem previamente existente, como as fornecidas por padrão pela Amazon, para criar uma nova imagem que poderá ser futuramente utilizada para provisionar suas instâncias EC2 com EBS (Elastic Block Storage).

access_key: e secret_key: Aqui apenas indicamos que queremos utilizar o valor das variáveis que criamos mais acima. É importante lembrar que quando as definimos, utilizamos env, para indicar que a origem delas estava em nossas variáveis de ambiente. Agora estamos utilizando user para indicar que são variáveis criadas em nosso código mesmo (usuário).

region: Novamente, assim como com as chaves, anteriormente nós declaramos esta variável e agora estamos inserindo-a em nosso código como uma variável de usuário user.

source_ami_filter: Neste bloco iremos passar as informações básicas sobre a imagem que será utilizada como origem ou source base para nossa imagem. Lembrando novamente de que utilizaremos uma AMI (Amazon Machine Image) já existente por padrão no AWS.

filters: Utilizaremos alguns filtros para definir a nossa imagem source.

virtualization_type: Nosso primeiro parâmetro de filtro será o tipo de virtualização que desejamos utilizar. Se você já utilizou AWS antes, provavelmente reparou que você possui algumas formas de virtualização disponíveis, como HVM e PV. Utilizaremos HVM em nosso código.

name: Aqui indicamos o nome da imagem source que queremos utilizar como base de nossa imagem. Como a Canonical vive atualizando suas imagens no marketplace do AWS, não utilizaremos um nome exato aqui, pois correríamos o risco de esta imagem ter sido descontinuada ou mesmo de estar desatualizada quando você estiver lendo e executando este tutorial, portanto utilizaremos um coringa (asterísco) e indicaremos um parâmetro extra para dizer que queremos utilizar a mais recente. Para nome, utilizaremos apenas: ubuntu/images/*ubuntu-xenial-16.04-amd64-server-* onde o asterísco do final indica que não nos importa o final do nome, e qualquer coisa será válida.

root-device-type: Indicamos ebs como tipo de storage para nossa imagem e tipo de instância.

owners: Indicamos o dono da imagem. Na página do AWS, cada imagem é vinculada a um dono, conforme imagem abaixo:

most_recent: Este é o parâmetro que, quando definido como true, fará com que seja utilizada a imagem mais recente que atenda aos demais filtros utilizados para a imagem.

instance_type: Indica o tipo de instância que será utilizada no AWS. O AWS possui dezenas de categorias de instâncias, onde cada categoria ou tipo possui uma quantidade diferente de memória, CPU, etc.

ami_name: Finalmente, aqui indicamos o nome que queremos atribuir à nossa imagem ao final da criação da mesma.

Agora que possuímos um código mais completo e que realmente conseguirá fazer algo, vamos validar o código e executá-lo em seguida para criarmos nossa primeira imagem no AWS.

1
2
$ packer validate ubuntuaws.json
Template validated successfully.

Código validado e sem erros.

É importante entender o que realmente acontece durante a criação de uma imagem. O Packer não tem como executar as instruções e rodar o que queremos para criar a imagem de forma estática e mágica, portanto o que vai acontecer na verdade será o seguinte:

  1. Primeiramente o Packer irá buscar a imagem que definimos que será utilizada como fonte;
  2. O packer irá criar literalmente uma instância no AWS utilizando as propriedades que definimos em nosso código para executar nossas instruções e garantir que tudo funcionará. Uma vez que todas as instruções sejam realizadas com sucesso, ele irá desligar e remover esta instância ou máquina virtual e irá salvar a imagem gerada;

Se durante a execução do Packer build você verificar o painel de instâncias EC2 no AWS, ficará claro que o Packer cria uma instância temporária durante a criação da imagem.

Hora do build:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ packer build ubuntuaws.json
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name: packer-example 1534022746
    amazon-ebs: Found Image ID: ami-5c150e23
==> amazon-ebs: Creating temporary keypair: packer_5b6f545a-8955-2f4f-b66a-8fd752d75bee
==> amazon-ebs: Creating temporary security group for this instance: packer_5b6f545c-0f04-553a-7944-a70d081be39d
==> amazon-ebs: Authorizing access to port 22 from 0.0.0.0/0 in the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
==> amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: "Name": "Packer Builder"
    amazon-ebs: Instance ID: i-0ef4a6036aebd0d56
==> amazon-ebs: Waiting for instance (i-0ef4a6036aebd0d56) to become ready...
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!
==> amazon-ebs: Stopping the source instance...
    amazon-ebs: Stopping instance, attempt 1
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating the AMI: packer-example 1534022746
    amazon-ebs: AMI: ami-0bbd7494d2e6cee71
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
==> amazon-ebs: Deleting temporary security group...
==> amazon-ebs: Deleting temporary keypair...
Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
us-east-1: ami-0bbd7494d2e6cee71

Verificando em minha interface de instâncias da região que escolhi (us-east-1) posso ver que existe uma instância que foi criada mas que já foi terminada ou deletada. Esta é a instância que o Packer criou automaticamente para dar início à criação de nossa imagem:

Da mesma forma, se formos na interface de imagens (AMI), veremos a nossa imagem recém criada:

De certa forma não fizemos nada aqui, visto que utilizamos uma imagem base do Ubuntu com o Packer e salvamos uma nova imagem sem mudar absolutamente nada neste Ubuntu, portanto basicamente criamos apenas uma cópia da imagem original. Nada empolgante…

Vamos incrementar um pouco nossa imagem realizando mudanças em nosso Ubuntu. De nada nos valeria criar uma imagem se ela não tiver nenhuma customização, certo?!

Começaremos criando um simples shell script chamado setup.sh com o seguinte conteúdo:

1
2
3
4
5
6
7
#!/bin/bash

sudo apt-get update

sudo apt-get upgrade -y

sudo apt-get install puppet -y

Trata-se de um simples script que basicamente irá atualizar o sistema operacional e em seguida instalar o Puppet no mesmo.

Voltando ao nosso arquivo ubuntuaws.json, vamos incluir um bloco de código provisioner ou provisionador. Existem diversos tipos de provisioners, mas para este momento utilizaremos apenas um, chamado shell pois desejamos executar um shell script. Nosso código agora estará assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
  "variables": {
    "aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",
    "aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",
    "region": "us-east-1"
  },
  "builders": [{
    "type": "amazon-ebs",
    "access_key": "{{user `aws_access_key`}}",
    "secret_key": "{{user `aws_secret_key`}}",
    "region": "{{user `region`}},
    "source_ami_filter": {
      "filters": {
      "virtualization-type": "hvm",
      "name": "ubuntu/images/*ubuntu-xenial-16.04-amd64-server-*",
      "root-device-type": "ebs"
      },
      "owners": ["099720109477"],
      "most_recent": true
    },
    "instance_type": "t2.micro",
    "ssh_username": "ubuntu",
    "ami_name": "packer-example {{timestamp}}"
  }],
  "provisioners": [
    {
      "type": "shell",
      "script": "./setup.sh"
    }
  ]
}

Antes de entrarmos em maiores detalhes e na utilização do Puppet em si para nossa imagem, vamos testar nosso código e aplicá-lo novamente:

Incluímos aqui:

provisioners: Para indicar que utilizaremos provisioners

type: Tipo de provisioner. Neste exemplo, será bash

script: Com o provisioner bash nós podemos declarar diretamente os comandos que queremos executar na imagem ou indicar um script com os comandos. Neste caso, optei por utilizar um script.

Validando e executando nosso código: (PS: Como a saída dos comandos apt-get update e apt-get upgrade são muito extensas, cortarei a maior parte aqui…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
$ packer validate ubuntuaws.json
Template validated successfully.

$ packer build ubuntuaws.json
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name: packer-example 1534024952
    amazon-ebs: Found Image ID: ami-5c150e23
==> amazon-ebs: Creating temporary keypair: packer_5b6f5cf9-dedc-aec2-ad77-acb29d37e8f9
==> amazon-ebs: Creating temporary security group for this instance: packer_5b6f5cfa-d35c-ff6c-aaad-cf02cedf8e74
==> amazon-ebs: Authorizing access to port 22 from 0.0.0.0/0 in the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
==> amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: "Name": "Packer Builder"
    amazon-ebs: Instance ID: i-0bbf230a251f76393
==> amazon-ebs: Waiting for instance (i-0bbf230a251f76393) to become ready...
==> amazon-ebs: Waiting for SSH to become available...
==> amazon-ebs: Connected to SSH!

######
--->>> REPARE A SEGUIR QUANDO O PACKER COMEÇA A EXECUTAR NOSSO SCRIPT SETUP.SH <<<---
######

==> amazon-ebs: Provisioning with shell script: ./setup.sh
    amazon-ebs: Hit:1 https://us-east-1.ec2.archive.ubuntu.com/ubuntu xenial InRelease
    amazon-ebs: Get:2 https://us-east-1.ec2.archive.ubuntu.com/ubuntu xenial-updates InRelease [109 kB]

######
--->>> NESTE MOMENTO O COMANDO "SUDO APT-GET UPDATE" ESTÁ SENDO EXECUTADO <<<---
######
...
...

######
--->>> A PARTIR DAQUI O COMANDO "SUDO APT-GET UPGRADE -Y" ESTÁ SENDO EXECUTADO <<<---
######

amazon-ebs: Calculating upgrade...
amazon-ebs: The following packages will be upgraded:
amazon-ebs:   cloud-init gnupg gpgv grub-legacy-ec2
amazon-ebs: 4 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
amazon-ebs: Need to get 1,188 kB of archives.
amazon-ebs: After this operation, 76.8 kB of additional disk space will be used.
amazon-ebs: Get:1 https://us-east-1.ec2.archive.ubuntu.com/ubuntu xenial-updates/main amd64 gpgv amd64 1.4.20-1ubuntu3.3 [165 kB]
amazon-ebs: Get:2 https://us-east-1.ec2.archive.ubuntu.com/ubuntu xenial-updates/main amd64 gnupg amd64 1.4.20-1ubuntu3.3 [626 kB]

...
...

######
--->>> A PARTIR DAQUI O COMANDO "SUDO APT-GET INSTALL PUPPET -Y" ESTÁ SENDO EXECUTADO <<<---
######

amazon-ebs: The following additional packages will be installed:
 amazon-ebs:   augeas-lenses debconf-utils facter fonts-lato hiera javascript-common
 amazon-ebs:   libaugeas0 libjs-jquery libruby2.3 puppet-common rake ruby ruby-augeas
 amazon-ebs:   ruby-deep-merge ruby-did-you-mean ruby-json ruby-minitest ruby-net-telnet
 amazon-ebs:   ruby-nokogiri ruby-power-assert ruby-rgen ruby-safe-yaml ruby-selinux
 amazon-ebs:   ruby-shadow ruby-test-unit ruby2.3 rubygems-integration unzip virt-what zip
 amazon-ebs: Suggested packages:
 amazon-ebs:   augeas-doc mcollective-common apache2 | lighttpd | httpd augeas-tools
 amazon-ebs:   puppet-el vim-puppet etckeeper ruby-rrd ri ruby-dev bundler
 amazon-ebs: The following NEW packages will be installed:
 amazon-ebs:   augeas-lenses debconf-utils facter fonts-lato hiera javascript-common
 amazon-ebs:   libaugeas0 libjs-jquery libruby2.3 puppet puppet-common rake ruby
 amazon-ebs:   ruby-augeas ruby-deep-merge ruby-did-you-mean ruby-json ruby-minitest
 amazon-ebs:   ruby-net-telnet ruby-nokogiri ruby-power-assert ruby-rgen ruby-safe-yaml
 amazon-ebs:   ruby-selinux ruby-shadow ruby-test-unit ruby2.3 rubygems-integration unzip
 amazon-ebs:   virt-what zip
 amazon-ebs: 0 upgraded, 31 newly installed, 0 to remove and 0 not upgraded.
 amazon-ebs: Need to get 8,267 kB of archives.
 amazon-ebs: After this operation, 38.2 MB of additional disk space will be used.
 amazon-ebs: Get:1 https://us-east-1.ec2.archive.ubuntu.com/ubuntu xenial/main amd64 fonts-lato all 2.0-1 [2,693 kB]
...
...

######
--->>>  E O PROCESSO SE ENCERRA <<<---
######

==> amazon-ebs: Stopping the source instance...
    amazon-ebs: Stopping instance, attempt 1
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating the AMI: packer-example 1534024952
    amazon-ebs: AMI: ami-06fe27bd22afcaa71
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
==> amazon-ebs: Deleting temporary security group...
==> amazon-ebs: Deleting temporary keypair...
Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
us-east-1: ami-06fe27bd22afcaa71

A partir deste momento já temos duas imagens criadas. Uma vez que a primeira não tinha nada de diferente do Ubuntu convencional e padrão do AWS, poderíamos muito bem deletá-la. Já a segunda imagem, é um pouco diferente da imagem padrão, visto que ela já conta com um sistema mais atualizado (apt-get upgrade), bem como possui o puppet já instalado nela.

Desta mesma maneira seria possível fazer um deployment bem mais complexo de acordo com suas necessidades e, sempre que lhe fosse necessário atualizar ou modificar algo, você poderia gerar uma nova imagem e aplicá-la onde desejasse.

Mas vamos incrementar um pouco mais nossa imagem, desta vez com o puppet como segundo provisioner.

Puppet

Uma vez que o objetivo deste post não é apresentar o Puppet em si, por hora ficaremos apenas com a informação de que o Puppet é uma ferramenta open source para gerenciamento de configurações (CM Tool – Configuration Management Tool) muito robusta e flexível.

Em futuros posts pretendo apresentar mais detalhes e explicações sobre o Puppet em si, mas para este post o objetivo é apenas demonstrar a flexibilidade do Packer para a criação de imagens, mesmo quando integramos diversos elementos a ele, como bash script e Puppet.

Crie um novo arquivo chamado deployment.pp. O Puppet chama seus arquivos de configuração ou instruções de manifests (manifestos) e estes sempre possuem a extensão .pp.

Insira o seguinte conteúdo em seu arquivo deployment.pp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
exec { 'apt-update':
  command => '/usr/bin/apt-get update'
}

package { 'apache2':
  ensure => installed,
  before => Service['apache2'],
}

service { 'apache2':
  ensure => running,
  before => Package['mysql-server'],
}

package { 'mysql-server':
  ensure => installed,
  before => Service['mysql'],
}

service { 'mysql':
  ensure => running,
  before => Package['php'],
}

package { 'php':
  ensure => installed,
  before => File['/var/www/html/info.php'],
}

file { '/var/www/html/info.php':
  ensure => file,
  content => '<?php  phpinfo(); ?>',
  require => Package['apache2'],
}

Este manifesto Puppet tem a tarefa de instalar um servidor web LAMP em nossa imagem Ubuntu, com Apache, Mysql e PHP, bem como expor uma página de informações do php default (php.info).

Como puppet não é o foco para este post, não entrarei em detalhes sobre as linhas contidas neste manifesto deployment.pp.

PS: Também estou assumindo que você já possui o puppet instalado em sua máquina. ;] (sudo apt-get install puppet -y)

Vamos primeiramente validar nosso código puppet para garantirmos que está tudo em ordem:

1
$ puppet parser validate deployment.pp

Se nada for apresentado na tela, significa que está tudo certo.

Agora vamos voltar ao nosso código packer e editar o arquivo ubuntuaws.json para inserir nosso segundo provisioner (Puppet):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
{
  "variables": {
    "aws_access_key": "{{env `AWS_ACCESS_KEY_ID`}}",
    "aws_secret_key": "{{env `AWS_SECRET_ACCESS_KEY`}}",
    "region": "us-east-1"
  },
  "builders": [{
    "type": "amazon-ebs",
    "access_key": "{{user `aws_access_key`}}",
    "secret_key": "{{user `aws_secret_key`}}",
    "region": "{{user `region`}},
    "source_ami_filter": {
      "filters": {
      "virtualization-type": "hvm",
      "name": "ubuntu/images/*ubuntu-xenial-16.04-amd64-server-*",
      "root-device-type": "ebs"
      },
      "owners": ["099720109477"],
      "most_recent": true
    },
    "instance_type": "t2.micro",
    "ssh_username": "ubuntu",
    "ami_name": "packer-example {{timestamp}}"
  }],
  "provisioners": [
    {
      "type": "shell",
      "script": "./setup.sh"
    },
    {
      "type": "puppet-masterless",
      "manifest_file": "deployment.pp"
    }
  ]
}

O que incluímos? Apenas mais um item dentro do bloco provisioners, porém desta vez com o tipo puppet-masterless:

type: Indicamos que o tipo deste provisioner é puppet-masterless. O Puppet pode funcionar de forma cliente/servidor, ou de forma autônoma, sem um master. Aqui queremos que ele funcione de forma autônoma e independente, portanto utilizaremos puppet-masterless (puppet sem master).

manifest_file: Indicamos que arquivo(s) de manifesto(s) do puppet devem ser executados. Neste caso, iremos apenas apontar para nosso deployment.pp.

Validando nosso código:

1
2
$ packer validate ubuntuaws.json
Template validated successfully.

Tudo certo com nosso código, portanto vamos executar novamente nosso build. Desta vez o Packer irá criar nossa imagem Ubuntu executando ambos, o shell script e o manifesto puppet, portanto nossa saída será bastante extensa. Como já vimos os detalhes nas execuções anteriores, irei cortar a saída aqui para focar na execução do manifesto puppet em si:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$ packer build ubuntuaws.json
amazon-ebs output will be in this color.

==> amazon-ebs: Prevalidating AMI Name: packer-example 1534028156
    amazon-ebs: Found Image ID: ami-5c150e23
==> amazon-ebs: Creating temporary keypair: packer_5b6f697c-9aa2-4a5b-0554-255f223460ce
==> amazon-ebs: Creating temporary security group for this instance: packer_5b6f697e-92da-f29d-8688-92b3d787afff
==> amazon-ebs: Authorizing access to port 22 from 0.0.0.0/0 in the temporary security group...
==> amazon-ebs: Launching a source AWS instance...
==> amazon-ebs: Adding tags to source instance
    amazon-ebs: Adding tag: "Name": "Packer Builder"
    amazon-ebs: Instance ID: i-01b0ccb4a7d4462ae
...
...
...

#####
--->>> A PARTIR DAQUI O PACKER INICIA A EXECUÇÃO DO MANIFESTO COM O PUPPET <<<---
#####

==> amazon-ebs: Provisioning with Puppet...
    amazon-ebs: Creating Puppet staging directory...
    amazon-ebs: Creating directory: /tmp/packer-puppet-masterless
    amazon-ebs: Uploading manifests...
    amazon-ebs: Creating directory: /tmp/packer-puppet-masterless/manifests
    amazon-ebs: Uploading manifest file from: deployment.pp
    amazon-ebs: Running Puppet: cd /tmp/packer-puppet-masterless && FACTER_packer_build_name='amazon-ebs' FACTER_packer_builder_type='amazon-ebs' sudo -E puppet apply --detailed-exitcodes /tmp/packer-puppet-masterless/manifests/deployment.pp
    amazon-ebs: Notice: Compiled catalog for ip-172-31-91-36.ec2.internal in environment production in 0.43 seconds
    amazon-ebs: Notice: /Stage[main]/Main/Exec[apt-update]/returns: executed successfully
    amazon-ebs: Notice: /Stage[main]/Main/Package[apache2]/ensure: ensure changed 'purged' to 'present'
    amazon-ebs: Notice: /Stage[main]/Main/Package[mysql-server]/ensure: ensure changed 'purged' to 'present'
    amazon-ebs: Notice: /Stage[main]/Main/Package[php]/ensure: ensure changed 'purged' to 'present'
    amazon-ebs: Notice: /Stage[main]/Main/File[/var/www/html/info.php]/ensure: defined content as '{md5}d9c0c977ee96604e48b81d795236619a'
    amazon-ebs: Notice: Finished catalog run in 35.64 seconds

#####
--->>> FINALIZOU A EXECUÇÃO DO MANIFESTO PUPPET <<<---
#####

==> amazon-ebs: Stopping the source instance...
    amazon-ebs: Stopping instance, attempt 1
==> amazon-ebs: Waiting for the instance to stop...
==> amazon-ebs: Creating the AMI: packer-example 1534031735
    amazon-ebs: AMI: ami-04b480ee18474759c
==> amazon-ebs: Waiting for AMI to become ready...
==> amazon-ebs: Terminating the source AWS instance...
==> amazon-ebs: Cleaning up any extra volumes...
==> amazon-ebs: No volumes to clean up, skipping
==> amazon-ebs: Deleting temporary security group...
==> amazon-ebs: Deleting temporary keypair...
Build 'amazon-ebs' finished.

==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
us-east-1: ami-04b480ee18474759c

E nossa imagem foi criada com sucesso.

A partir deste momento você decide o que fazer. As possibilidades são infinitas, dependendo do que se deseja conseguir ou arquitetar e como deseja que sua pipeline funcione.

Por exemplo, você poderia ter uma pipeline no Jenkins que iniciasse o build de uma nova imagem atualizada, que por sua vez chamasse shell script e puppet para provisionamento e configuração desta imagem, em seguida o Jenkins poderia chamar o terraform para criar uma instância ou múltiplas instâncias em um cluster por trás de um load balancer utilizando a imagem que foi criada pelo Packer, etc..etc..etc.. Sua criatividade será o seu limite.

Para confirmarmos que nosso código completo funcionou e que nossa imagem foi gerada com sucesso, você pode criar manualmente uma instância a partir do AWS com esta sua nova imagem.

Testando sua imagem

1- Efetue login em sua conta do AWS;

2- Vá ao menu de Serviços/Services –> Imagens/Images –> AMIs;

3- Na lista de imagens disponíveis, selecione a sua mais recente, para ter certeza de que está escolhendo a que foi criada por último, pois ela será a imagem que contém o deployment via puppet por completo (Repare a data de criação para ter certeza);

4- Ao selecionar a imagem, clique em Lançar/Launch, conforme imagem abaixo:

5- Selecione o tipo de instância padrão que pode ser utilizada gratuitamente para este exemplo, t2.micro, em seguida clique em Próximo/Next, conforme imagem abaixo:

6- Na tela seguinte, deixe tudo como está exceto a opção de Atribuir Ip Público/Auto-Assign Public IP. Ative esta opção, em seguida clique em Próximo, conforme imagem abaixo:

7- Na tela seguinte, pode manter o padrão de 8GB de disco/storage e clicar em Próximo;

8- Na parte de Tags, pode novamente manter tudo vazio para este exemplo e clicar em Próximo;

9- Nas configurações de Grupo de Segurança/Security Group, pode manter o padrão, mas certifique-se de inserir mais uma regra de firewall para que possamos testar o servidor web. Por padrão o AWS já apresentará a porta 22 (SSH) aberta, portanto vamos abrir também a porta 80, conforme imagem abaixo. Em seguida, clique em Revisar e Lançar/Review and Launch;

10- Em seguida, confirme novamente e clique em Lançar/Launch;

11- Uma janela popup será apresentada lhe perguntando se você deseja criar um par de chaves. Fica a seu critério. Caso deseje se conectar a esta instância via ssh, crie uma chave, do contrário, pode prosseguir sem criar uma chave. Como eu apenas quero testar se o servidor web estará rodando, e já abrimos a porta 80 no firewall, poderemos confirmar isto através de nosso navegador, portanto ignorarei a chave e clicarei em Lançar Instância/Launch Instance;

12- O AWS lhe informará que sua instância está sendo criada. Como se trata de uma instância Linux, costuma ser um processo rápido, geralmente leva algo entre 1 e 2 minutos. Você pode ir para a página principal de instâncias e aguardar sua instância estar com status running;

13- Copie o IP público ou externo que o AWS atribuiu à sua instância conforme imagem abaixo:

14- Agora cole o ip em qualquer browser e você deverá ver uma página default do apache que está rodando em seu novo servidor web:

Finalizado.

Lembre-se de excluir os recursos no AWS para evitar ser cobrado:

  • Instâncias
  • Volumes
  • Imagens
  • Security Groups

… ou o que mais você tiver criado em seus testes caso não os vá mais utilizar.

Automatizando a Criação De Imagens Com Packer

| Comments

O que é Packer e qual sua importância

Packer é uma ferramenta Open Source, criada e mantida pela HashiCorp, para a criação de imagens de máquinas idênticas para múltiplas plataformas a partir de uma única fonte ou código de configuração. O Packer é leve, roda em praticamente todos os principais Sistemas Operacionais e possui alta performance, permitindo a criação de imagens para múltiplas plataformas em paralelo.

Uma imagem de máquina, nada mais é que um arquivo estático que contém um sistema operacional pré-configurado, com determinados softwares instalados e que pode ser utilizada para criar novas máquinas ou servidores de forma mais rápida, evitando o trabalho repetitivo de instalar o sistema operacional e configurar aplicações padrões.

Existem diversos formatos diferentes de imagens que podem ser criadas através do Packer, como por exemplo Amazon EC2, VirtualBox, VMware, Google Cloud Platform, Microsoft Azure, Docker, QEMU, CloudStack, DigitalOcean, etc.

O Packer suporta e é compatível com diversas ferramentas de provisionamento e automação, como shell script, Ansible, Puppet, Chef, etc., fazendo com que a criação de imagens seja ainda mais simples, dinâmica e robusta.

A ideia de criar imagens não é nova, visto que Sysadmins já o fazem há muitos anos, porém esta sempre foi uma tarefa tediosa, demorada e muito pouco produtiva. Basicamente a ideia de se construir uma imagem partia de, antes de mais nada, realizar de fato uma instalação completa de um Sistema Operacional e em seguida utilizar algum aplicativo para “salvar” aquele estado em uma imagem, que poderia ser posteriormente aplicada em outras máquinas. Isto por si só já facilitava muito a vida de Sysadmins em geral, visto que eles apenas realizavam a instalação completa do SO uma vez. Caso, além do SO, fossem necessárias outras aplicações, o processo seria basicamente o mesmo, instalando-se uma vez o SO completo e em seguida instalando todas as aplicações desejadas para a imagem.

Até então a criação da imagem parecia ser um sucesso, no entanto isto era algo improdutivo por ser absolutamente estático e imutável. Sempre que fosse necessário fazer uma mudança na imagem, atualizar versão de sistema operacional, aplicar patches, atualizar demais aplicações ou mudar configurações, novamente o processo deveria se repetir do início. Não é necessário sequer mencionar que não era nada simples gerenciar e versionar isto.

Eis que surge o Packer para deixar a criação de imagens menos entediante, flexível e mais gerenciável.

Instalação

A instalação não é complexa e pode ser feita através do binário disponível na página de downloads do Packer: https://www.packer.io/downloads.html

Ubuntu – Caso não queira instalar através do binário fornecido no link acima, pode instalar via:

1
$ sudo apt-get install packer -y

Arch linux – Caso não queira instalar através do binário fornecido no link acima, pode instalar através do pacote disponível no AUR.

OS X – Caso não queira instalar através do binário fornecido no link acima, pode instalar através do homebrew:

1
$ brew install packer

Independente de sua forma de instalação, confirme que a instalação foi concluída com sucesso:

1
2
3
4
5
6
7
8
9
10
$ packer
Usage: packer [--version] [--help] <command> [<args>]

Available commands are:
    build       build image(s) from template
    fix         fixes templates from old versions of packer
    inspect     see components of a template
    push        push a template and supporting files to a Packer build service
    validate    check that a template is valid
    version     Prints the Packer version

Se você recebeu algo similar, significa que você já pode começar a criar suas imagens. Caso tenha recebido um erro informando que o Packer não foi encontrado, significa que o mesmo não foi inserido corretamente na variável de ambiente de seu PATH. Certifique-se de inserir o diretório no qual o Packer foi instalado em seu PATH.

Criando Imagens com o Packer

Conforme dito anteriormente, o Packer pode criar imagens para diversas extensões e plataformas, portanto o primeiro passo é saber para qual plataforma você deseja criar sua imagem.

Para este tutorial introdutório, utilizaremos o Docker como destino para nossa imagem, ou seja, criaremos uma imagem de container que poderá rodar com o Docker.

OBS: A partir deste ponto estou assumindo que você já possui o Docker instalado em seu sistema. Para confirmar, digite:

1
2
$ docker --version
Docker version 18.06.0-ce, build 0ffa825

Vamos ao Packer.

Primeiramente mostrarei como não tenho nenhuma imagem Docker neste momento em meu sistema:

1
2
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

1- Escolha um editor de textos de sua preferência e inicie um arquivo chamado template.json;

2- Digite o seguinte em seu arquivo template.json.

1
2
3
4
5
6
7
{
  "builders": [{
    "type": "docker",
    "image": "ubuntu",
    "commit": true
  }]
}

O que temos…

builders: Abre o bloco builders (construtores), onde iniciamos as instruções que definirão como a nossa imagem será criada;

type: Especifica o tipo de construtor que será utilizado. Neste exemplo escolhemos Docker. Aqui poderíamos utilizar AWS EC2, Google Cloud Instance, etc.

image: Parâmetro no qual indicaremos qual imagem será utilizada como origem para a nossa imagem final.

commit: Indica que queremos realizar o commit da imagem gerada no final.

*OBS: É importante lembrar que o Packer possui uma infinidade de parâmetros ou propriedades diferentes que podem ser específicos para cada tipo de builder. Lista de parâmetros e opções para o builder Docker.

Primeiramente, o indicado é validarmos a sintaxe de nosso código json: ($ packer validate template.json)

1
2
$ packer validate template.json
Template validated successfully.

O código parece estar correto do ponto de vista do Packer. Caso houvesse algo errado com o código, a mensagem daria alguma pista de onde está o erro. Removerei o último } do arquivo template.json para forçar um erro: ($ packer validate template.json)

1
2
3
4
5
$ packer validate template.json
Failed to parse template: Error parsing JSON: unexpected end of JSON input
At line 7, column 1 (offset 88):
    6:   }]
    7:

Com o arquivo de volta à sua versão correta, o próximo passo seria de fato o build, onde criaremos a imgem desejado de acordo com as intruções que demos: ($ packer build template.json)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$ packer build template.json
docker output will be in this color.

==> docker: Creating a temporary directory for sharing data...
==> docker: Pulling Docker image: ubuntu
    docker: Using default tag: latest
    docker: latest: Pulling from library/ubuntu
    docker: c64513b74145: Pulling fs layer
    docker: 01b8b12bad90: Pulling fs layer
    docker: c5d85cf7a05f: Pulling fs layer
    docker: b6b268720157: Pulling fs layer
    docker: e12192999ff1: Pulling fs layer
    docker: b6b268720157: Waiting
    docker: e12192999ff1: Waiting
    docker: c5d85cf7a05f: Verifying Checksum
    docker: c5d85cf7a05f: Download complete
    docker: 01b8b12bad90: Verifying Checksum
    docker: 01b8b12bad90: Download complete
    docker: e12192999ff1: Verifying Checksum
    docker: e12192999ff1: Download complete
    docker: b6b268720157: Verifying Checksum
    docker: b6b268720157: Download complete
    docker: c64513b74145: Verifying Checksum
    docker: c64513b74145: Download complete
    docker: c64513b74145: Pull complete
    docker: 01b8b12bad90: Pull complete
    docker: c5d85cf7a05f: Pull complete
    docker: b6b268720157: Pull complete
    docker: e12192999ff1: Pull complete
    docker: Digest: sha256:3f119dc0737f57f704ebecac8a6d8477b0f6ca1ca0332c7ee1395ed2c6a82be7
    docker: Status: Downloaded newer image for ubuntu:latest
==> docker: Starting docker container...
    docker: Run command: docker run -v /Users/kalib/.packer.d/tmp/packer-docker835877235:/packer-files -d -i -t ubuntu /bin/bash
    docker: Container ID: 50211726119aa045b0bc5eb9da8c9af243bc179fef0aad3cd94156d0f2a7a45a
==> docker: Committing the container
    docker: Image ID: sha256:3a6a21aab4706e2b512f0c1fcbe8265e2527d4794f7c6fbdc5e4faf907d10622
==> docker: Killing the container: 50211726119aa045b0bc5eb9da8c9af243bc179fef0aad3cd94156d0f2a7a45a
Build 'docker' finished.

==> Builds finished. The artifacts of successful builds are:
--> docker: Imported Docker image: sha256:3a6a21aab4706e2b512f0c1fcbe8265e2527d4794f7c6fbdc5e4faf907d10622

Na saída do comando, ou output, podemos ver todos os passos e instruções que foram realizadas na criação de nossa imagem. Neste exemplo, o Docker baixou a última imagem ubuntu dos repositórios do Docker e em seguida gerou nossa imagem.

Para confirmar, podemos utilizar novamente o docker images:

1
2
3
4
docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
<none>              <none>              3a6a21aab470        5 minutes ago       83.5MB
ubuntu              latest              735f80812f90        6 days ago          83.5MB

Como esperado, agora temos duas imagens: a do ubuntu padrão, que foi baixada para servir de base para a nossa, bem como a nossa, até então sem nome.

De certa forma nada foi feito até então. Nós apenas geramos uma imagem idêntica à que já existia do ubuntu, sem qualquer mudança. Vamos então dar algum sentido à nossa imagem em seguida. Além disso, podemos perceber que nossa imagem não tinha sequer nome, ficando listada como . Corrigiremos isso também.

O Packer considera o Docker apenas como uma plataforma para gerar/rodar containers e, como tal, para gerar uma imagem o Packer descarta a necessidade da utilização de um Dockerfile, como de costume para quem já criou containers com Docker. Através de blocos de código changes e provisioners, o Packer nos permite passar praticamente todas as informações de metadata que fariam parte de um Dockerfile.

Para demonstrar, criaremos uma imagem de um ubuntu rodando nginx, porém utilizaremos a imagem padrão do ubuntu que já baixamos anteriormente.

O código do template.json agora deverá ser o seguinte:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "builders": [{
    "type": "docker",
    "image": "ubuntu",
    "commit": true,
    "changes": [
      "CMD [\"nginx\", \"-g\", \"daemon off;\"]",
      "EXPOSE 80"
   ]
  }],
  "provisioners": [
    {
      "type" : "shell",
      "inline" : ["apt-get update && apt-get install -y nginx"]
    }
  ],
  "post-processors": [
      {
        "type": "docker-tag",
        "repository": "kalib/ubuntunginx",
        "tag": "0.1"
      }
  ]
}

Vejamos o que foi incluído:

changes: Parâmetro que nos permite alterar a meta-data de nossa imagem docker.

CMD [\“nginx\”, \“-g\”, \“daemon off;\”]: Comando para iniciar o nginx no container, assim como faríamos utilizando Dockerfile.

EXPOSE 80: Da mesma forma, apenas informando que a porta 80 no container deverá estar disponível, assim como faríamos em um Dockerfile.

provisioners: É onde indicamos que ferramentas utilizaremos para provisionar as alterações/configurações em nossa imagem. Podemos ter um ou mais provisioners ao mesmo tempo. Neste exemplo estaremos utilizando um simples comando bash, portanto nosso provisioner será bash, mas poderia ser chef, ansible, puppet, ou mesmo um script bash mais complexo, mas como o objetivo para este post é uma abordagem simples e introdutória, manteremos o provisioner mais simples possível.

type: especificamos que o tipo de provisioner será bash.

inline: Inserimos o comando bash que desejamos que seja executado para personalizar nossa imagem. Neste caso, apt-get update && apt-get install -y nginx irá atualizar a base de dados dos repositórios e em seguida instalar o nginx.

post-processors: Como o nome já diz, são instruções ou processors que acontecem ao final do build.

type: Indica o tipo de post-processor que utilizaremos. No exemplo, utilizaremos docker-tag, para que possamos dar uma tag/nome para nossa imagem. (Lembrando que como uma boa prática, ao criar uma imagem Docker, sempre inserimos o padrão repositório/imagem)

repository: É onde indicamos o repositório e o nome da imagem que criaremos.

tag: Onde podemos definir a tag que ajudar a manter o controle de versão de nossa imagem.

Novamente, vamos validar nosso código:

1
2
$ packer validate template.json
Template validated successfully.

Desta vez o output será muito extenso, portanto colarei apenas alguns trechos aqui para demonstrar o resultado:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
$ packer build template.json
docker output will be in this color.

==> docker: Creating a temporary directory for sharing data...
==> docker: Pulling Docker image: ubuntu
    docker: Using default tag: latest
    docker: latest: Pulling from library/ubuntu
    docker: Digest: sha256:3f119dc0737f57f704ebecac8a6d8477b0f6ca1ca0332c7ee1395ed2c6a82be7
    docker: Status: Image is up to date for ubuntu:latest
==> docker: Starting docker container...
    docker: Run command: docker run -v /Users/kalib/.packer.d/tmp/packer-docker737353515:/packer-files -d -i -t ubuntu /bin/bash
    docker: Container ID: 097a85a796e0e928fbc82cd6618bd60e286b46dfadad20e823027fc2f93d25db
==> docker: Provisioning with shell script: /var/folders/nn/njz4sd054fv_tvq30vh0zygh0000gp/T/packer-shell161880041
    docker: Get:1 https://archive.ubuntu.com/ubuntu bionic InRelease [242 kB]
    docker: Get:2 https://security.ubuntu.com/ubuntu bionic-security InRelease [83.2 kB]
    ...
    ...
    docker: Reading state information...
docker: The following additional packages will be installed:
docker:   fontconfig-config fonts-dejavu-core geoip-database libbsd0 libexpat1
docker:   libfontconfig1 libfreetype6 libgd3 libgeoip1 libicu60 libjbig0
docker:   libjpeg-turbo8 libjpeg8 libnginx-mod-http-geoip
docker:   libnginx-mod-http-image-filter libnginx-mod-http-xslt-filter
docker:   libnginx-mod-mail libnginx-mod-stream libpng16-16 libssl1.1 libtiff5
docker:   libwebp6 libx11-6 libx11-data libxau6 libxcb1 libxdmcp6 libxml2 libxpm4
docker:   libxslt1.1 multiarch-support nginx-common nginx-core ucf
docker: Suggested packages:
docker:   libgd-tools geoip-bin fcgiwrap nginx-doc ssl-cert
docker: The following NEW packages will be installed:
docker:   fontconfig-config fonts-dejavu-core geoip-database libbsd0 libexpat1
docker:   libfontconfig1 libfreetype6 libgd3 libgeoip1 libicu60 libjbig0
docker:   libjpeg-turbo8 libjpeg8 libnginx-mod-http-geoip
docker:   libnginx-mod-http-image-filter libnginx-mod-http-xslt-filter
docker:   libnginx-mod-mail libnginx-mod-stream libpng16-16 libssl1.1 libtiff5
docker:   libwebp6 libx11-6 libx11-data libxau6 libxcb1 libxdmcp6 libxml2 libxpm4
docker:   libxslt1.1 multiarch-support nginx nginx-common nginx-core ucf
docker: 0 upgraded, 35 newly installed, 0 to remove and 0 not upgraded.
docker: Need to get 16.1 MB of archives.
docker: After this operation, 58.8 MB of additional disk space will be used.
docker: Get:1 https://archive.ubuntu.com/ubuntu bionic/main amd64 multiarch-support amd64 2.27-3ubuntu1 [6916 B]
docker: Get:2 https://archive.ubuntu.com/ubuntu bionic/main amd64 libxau6 amd64 1:1.0.8-1 [8376 B]
docker: Get:3 https://archive.ubuntu.com/ubuntu bionic-updates/main amd64 libjpeg-turbo8 amd64 1.5.2
...
...
docker: Setting up nginx (1.14.0-0ubuntu1) ...
    docker: Processing triggers for libc-bin (2.27-3ubuntu1) ...
==> docker: Committing the container
    docker: Image ID: sha256:0cbafebeaf17d8b8e8eccad87e706845993b78265a99e3e0f1ef8bd0bd724aa7
==> docker: Killing the container: 097a85a796e0e928fbc82cd6618bd60e286b46dfadad20e823027fc2f93d25db
==> docker: Running post-processor: docker-tag
    docker (docker-tag): Tagging image: sha256:0cbafebeaf17d8b8e8eccad87e706845993b78265a99e3e0f1ef8bd0bd724aa7
    docker (docker-tag): Repository: kalib/ubuntunginx:0.1
Build 'docker' finished.

==> Builds finished. The artifacts of successful builds are:
--> docker: Imported Docker image: sha256:0cbafebeaf17d8b8e8eccad87e706845993b78265a99e3e0f1ef8bd0bd724aa7
--> docker: Imported Docker image: kalib/ubuntunginx:0.1

Como podemos ver, todos os passos foram executados, incluindo a atualização da lista de pacotes dos repositórios, a instalação do nginx e a criação da imagem com o repositório, nome e tag de versão que indicamos.

Vejamos nossa imagem na lista de imagens disponíveis no Docker:

1
2
3
4
5
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
kalib/ubuntunginx   0.1                 0cbafebeaf17        5 minutes ago       182MB
<none>              <none>              4d86b1edf015        43 minutes ago      83.5MB
ubuntu              latest              735f80812f90        6 days ago          83.5MB

Sucesso, aparentemente nossa imagem foi criada conforme o esperado. Hora de testar e ver se tudo realmente funcionou.

OBS: Como dito no início do post, o objetivo deste post não é ensinar Docker, portanto estou assumindo que você já possui alguma familiaridade com esta ferramenta.

Vamos iniciar um container a partir desta nossa imagem em background, mapeando a porta 80 do container na porta 80 de nosso host, para facilitar o teste:

1
2
$ docker run -d -p 80:80 kalib/ubuntunginx:0.1
53de48d3e7708a7ac16dbb5ba6ed71b8672201729264e51945ee3412a02e0deb

Nenhuma mensagem de erro, portanto ao que tudo indica nosso container está rodando corretamente. Vamos confirmar isto com docker ps:

1
2
3
$ docker ps
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                NAMES
53de48d3e770        kalib/ubuntunginx:0.1   "nginx -g 'daemon of…"   5 seconds ago       Up 4 seconds        0.0.0.0:80->80/tcp   clever_keldysh

Tudo certo aqui também. Podemos ver nosso container rodando, o processo do nginx aparentemente rodando, bem como a porta 80 mapeada como esperado. Vamos abrir o browser em nosso host e acessar localhost:80

Pronto, validamos a nossa imagem criando um container e confirmando que o nginx está de fato rodando como deveria.

Em futuros posts, pretendo dar exemplos mais aprofundados, utilizando combinações com outros provisioners, como Puppet, Ansible e Shell Scripts, bem como outros builders, como por exemplo o AWS.