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.