Chef: Uso De Condicionais Not_if E Only_if

| Comments

Pare e volte uma casa

Se você nunca utilizou ou sabe pouco sobre o Chef é importante que você pare aqui mesmo e volte uma casa. Sugiro que leia o post Chef: Automação e Gerenciamento de Configuração antes de seguir este, uma vez que através dele você entenderá o que é, e como funciona o Chef, bem como sua instalação básica.

Resources

Conforme dito no post anterior, resource é uma descrição de estado que:

  • Descreve o estado desejado para um item de configuração
  • Declara os passos necessários para levar o item especificado ao estado desejado
  • Especifica o tipo de resource – por exemplo, package, template ou service
  • Lita detalhes adicionais (também conhecidos como propriedades de resources), conforme necessário
  • São agrupados em recipes, que descrevem configurações em geral

Utilizando (Guardas) not_if e only_if

Todos os resources (incluindo os personalizados) no Chef compartilham um conjunto de opções comuns: ações, propriedades, condicionais, notificações e paths relativos.

Guards

Propriedades de guarda, ou guards, como são chamadas no Chef, podem ser utilizadas para avaliar o estado de um node durante a fase de execução do chef-client. Esta avaliação funciona como uma condicional e, baseando-se nos resultados da mesma, a propriedade guard é então utilizada para indicar ao chef-client se ele deve ou não continuar a execução de um resource. Uma propriedade guard aceita tanto um valor string quanto um bloco de código Ruby.

  • A string é executada como um comando shell. Caso o comando retorne 0 (true), a propriedade guard é aplicada. Caso o comando retorne qualquer outro valor a propriedade guard não será aplicada.
  • Um bloco é executado como um código Ruby que deve retornar true ou false. Da mesma forma, caso o comando retorne true, a propriedade guard é aplicada. Caso o comando retorne false a propriedade guard não será aplicada.

Uma guard é importante para garantir que um resource seja idempotente ao permitir um teste no próprio resource certificando-se de que o mesmo se encontra no estado desejado de forma que o chef-client não faça nada.

Atributos

Os seguintes atributos podem ser utilizados para definir uma guard que é avaliada durante a fase de execução do chef-client:

not_ifImpede a execução de um resource quando a condição retornar true.

only_if – Permite a execução de um resource apenas quando a condição retornar true.

Mãos à obra

Para simplificar seguirei utilizando a recipe utilizada no post anterior. Não sabe o que é uma recipe? -> Novamente, caso não entenda o que estou dizendo, volte uma casa e leia o post anterior, no qual explico o que é uma recipe, bem como cada elemento da mesma, visto que a utilizaremos aqui.

Nossa recipe era a seguinte:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package 'apache' do
        package_name 'httpd'
        action :install
end

service 'httpd' do
        action [:enable, :start]
end

file '/var/www/html/index.html' do
        content 'Hello World!'
        mode '0755'
        owner 'root'
        group 'apache'
end

Como primeira alteração vamos editar o arquivo /etc/motd. Este arquivo nada mais é do que a definição de um baner que será apresentado sempre que alguém se logar em seu servidor.

Por padrão, este arquivo costuma vir vazio. Este é o caso da máquina utilizada para este exemplo:

1
2
[root@kalib6 ~]# cat /etc/motd
[root@kalib6 ~]#

Começaremos definindo o conteúdo que deverá existir em nosso arquivo /etc/motd incluindo um resource do tipo file no final de nossa recipe exemplo.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package 'apache' do
        package_name 'httpd'
        action :install
end

service 'httpd' do
        action [:enable, :start]
end

file '/var/www/html/index.html' do
        content 'Hello World!'
        mode '0755'
        owner 'root'
        group 'apache'
end

file '/etc/motd' do
        content 'Bem Vindo!'
end

Até aqui nenhuma novidade (caso você tenha lido de fato o post anterior ou já tenha utilizado Chef anteriormente), apenas incluímos mais um resource em nossa recipe exemplo.rb informando que o arquivo /etc/motd deve existir e que seu conteúdo deverá ser Bem Vindo!.

Validando e executando localmente nossa recipe conforme feito anteriormente veremos que todos os demais passos ou resources serão ignorados por já estarem estarem no estado desejado (idempotência), restando apenas a inclusão do conteúdo no /etc/motd:

Validando

1
2
3
4
5
6
7
[root@kalib6 ~]# ruby -c exemplo.rb && foodcritic exemplo.rb
Syntax OK
Checking 1 files
x
FC011: Missing README in markdown format: ../README.md:1
FC031: Cookbook without metadata.rb file: ../metadata.rb:1
FC071: Missing LICENSE file: ../LICENSE:1

Executando

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
[root@kalib6 ~]# chef-client --local-mode exemplo.rb
[2018-04-15T22:03:26+00:00] WARN: No config file found or specified on command line, using command line options.
[2018-04-15T22:03:26+00:00] WARN: No cookbooks directory found at or above current directory.  Assuming /root.
Starting Chef Client, version 13.8.5
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Installing Cookbook Gems:
Compiling Cookbooks...
[2018-04-15T22:03:29+00:00] WARN: Node kalib6.test.com has an empty run list.
Converging 4 resources
Recipe: @recipe_files::/root/exemplo.rb
  * yum_package[apache] action install (up to date)
  * service[httpd] action enable (up to date)
  * service[httpd] action start (up to date)
  * file[/var/www/html/index.html] action create (up to date)
  * file[/etc/motd] action create
    - update content in file /etc/motd from e3b0c4 to f13843
    --- /etc/motd       2018-04-15 20:51:00.411479476 +0000
    +++ /etc/.chef-motd20180415-1681-1p1fm7m    2018-04-15 22:03:42.142091791 +0000
    @@ -1 +1,2 @@
    +Bem Vindo!
    - restore selinux security context

Running handlers:
Running handlers complete
Chef Client finished, 1/5 resources updated in 15 seconds

Para termos certeza de que o nosso arquivo foi corretamente alterado…

1
2
[root@kalib6 ~]# cat /etc/motd
Bem Vindo!

Novamente, nenhuma novidade até aqui.

Vamos agora utilizar o tipo de resource execute, o qual nos permite executar um comando a cada execução do chef-client. Vale lembrar que uma das características do Chef é a idempotência, portanto o execute pode ser considerado uma exceção, já que o comando será executado sempre, mesmo que já tenha sido executado anteriormente. E é neste tipo de situação que os guards se mostram importantes.

Comecemos inserindo um resource do tipo execute em nossa recipe:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package 'apache' do
        package_name 'httpd'
        action :install
end
https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet
service 'httpd' do
        action [:enable, :start]
end

file '/var/www/html/index.html' do
        content 'Hello World!'
        mode '0755'
        owner 'root'
        group 'apache'
end

file '/etc/motd' do
        content 'Bem Vindo!'
end

execute 'meu-comando' do
        command 'echo " Obrigado!" >> /etc/motd'
        only_if 'test -r /etc/motd'
end

O que adicionamos aqui?

  • Resource Type: execute (Para que possamos executar um comando)
  • Resource Name: meu-comando (Poderíamos ter utilizado qualquer nome)
  • Command: ‘echo “ Obrigado!” >> /etc/motd’ (O comando que desejamos executar. Estou utilizando echo para inserir * Obrigado!* ao meu arquivo /etc/motd)
  • Guard: only_if ‘test -r /etc/motd’ (only_if implica que meu resource meu-comando será executado apenas caso o resultado de test -r /etc/motd seja positivo. test -r irá verificar se o arquivo /etc/motd existe no sistema. Caso sim, meu comando echo " Obrigado!" >> /etc/motd será executado conforme planejado, do contrário será ignorado)

Nós já sabemos que o arquivo existe, portanto esperamos que * Obrigado!* seja adicionada ao mesmo após execução do chef-client.

Verificando nosso código

1
2
3
4
5
6
7
[root@kalib6 ~]# ruby -c exemplo.rb && foodcritic exemplo.rb
Syntax OK
Checking 1 files
x
FC011: Missing README in markdown format: ../README.md:1
FC031: Cookbook without metadata.rb file: ../metadata.rb:1
FC071: Missing LICENSE file: ../LICENSE:1

Tudo ok. Executando…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@kalib6 ~]# chef-client --local-mode exemplo.rb
[2018-04-15T23:08:54+00:00] WARN: No config file found or specified on command line, using command line options.
[2018-04-15T23:08:54+00:00] WARN: No cookbooks directory found at or above current directory.  Assuming /root.
Starting Chef Client, version 13.8.5
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Installing Cookbook Gems:
Compiling Cookbooks...
[2018-04-15T23:09:03+00:00] WARN: Node kalib6.test.com has an empty run list.
Converging 5 resources
Recipe: @recipe_files::/root/exemplo.rb
  * yum_package[apache] action install (up to date)
  * service[httpd] action enable (up to date)
  * service[httpd] action start (up to date)
  * file[/var/www/html/index.html] action create (up to date)
  * file[/etc/motd] action create (up to date)
  * execute[meu-comando] action run
    - execute echo " Obrigado!" >> /etc/motd

Running handlers:
Running handlers complete
Chef Client finished, 1/6 resources updated in 31 seconds

Repare que o Chef executou o comando para inserir Obrigado!.

1
2
[root@kalib6 ~]# cat /etc/motd
Bem Vindo! Obrigado!

Conforme dito anteriormente, o resource execute irá executar o meu comando SEMPRE que o chef-client rodar. Vejamos o que acontece ao executar novamente o chef-client sem alterar a recipe.

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
[root@kalib6 ~]# chef-client --local-mode exemplo.rb
[2018-04-15T23:18:10+00:00] WARN: No config file found or specified on command line, using command line options.
[2018-04-15T23:18:10+00:00] WARN: No cookbooks directory found at or above current directory.  Assuming /root.
Starting Chef Client, version 13.8.5
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Installing Cookbook Gems:
Compiling Cookbooks...
[2018-04-15T23:18:20+00:00] WARN: Node kalib6.test.com has an empty run list.
Converging 5 resources
Recipe: @recipe_files::/root/exemplo.rb
  * yum_package[apache] action install (up to date)
  * service[httpd] action enable (up to date)
  * service[httpd] action start (up to date)
  * file[/var/www/html/index.html] action create (up to date)
  * file[/etc/motd] action create
    - update content in file /etc/motd from 201ddf to f13843
    --- /etc/motd       2018-04-15 23:16:13.705005377 +0000
    +++ /etc/.chef-motd20180415-2432-zhtjnq     2018-04-15 23:18:42.138379629 +0000
    @@ -1,2 +1,2 @@
    -Bem Vindo! Obrigado!
    +Bem Vindo!
    - restore selinux security context
  * execute[meu-comando] action run
    - execute echo " Obrigado!" >> /etc/motd

Running handlers:
Running handlers complete
Chef Client finished, 2/6 resources updated in 31 seconds

Como em nossa recipe temos o resource file que determina o conteúdo do arquivo /etc/motd como sendo Bem Vindo!, o chef percebeu que o arquivo se encontrava diferente, pois acrescentamos o Obrigado! no passo anterior. O arquivo foi corrigido, voltando a ter seu conteúdo original, em seguida o Chef inseriu novamente Obrigado!, pois assim está determinado em nossa recipe.

É fácil perceber isto nas linhas a seguir, retiradas do resultado de nossa última execução do chef-client:

1
2
3
4
5
6
7
8
9
10
* file[/etc/motd] action create
  - update content in file /etc/motd from 201ddf to f13843
  --- /etc/motd       2018-04-15 23:16:13.705005377 +0000
  +++ /etc/.chef-motd20180415-2432-zhtjnq     2018-04-15 23:18:42.138379629 +0000
  @@ -1,2 +1,2 @@
  -Bem Vindo! Obrigado!
  +Bem Vindo!
  - restore selinux security context
* execute[meu-comando] action run
  - execute echo " Obrigado!" >> /etc/motd

Se verificarmos nosso arquivo, veremos que ele está da mesma forma:

1
2
[root@kalib6 ~]# cat /etc/motd
Bem Vindo! Obrigado!

Para percebermos a diferença, vamos comentar as linhas do resource file /etc/motd:

1
2
3
4
5
...
#file '/etc/motd' do
#       content 'Bem Vindo!'
#end
...

Ao comentar estas linhas, nossa recipe não mais indicará que o conteúdo do arquivo /etc/motd deve ser Bem Vindo!, portanto vejamos o que acontece quando executamos novamente o chef-client.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@kalib6 ~]# chef-client --local-mode exemplo.rb
[2018-04-15T23:28:32+00:00] WARN: No config file found or specified on command line, using command line options.
[2018-04-15T23:28:33+00:00] WARN: No cookbooks directory found at or above current directory.  Assuming /root.
Starting Chef Client, version 13.8.5
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Installing Cookbook Gems:
Compiling Cookbooks...
[2018-04-15T23:28:42+00:00] WARN: Node kalib6.test.com has an empty run list.
Converging 4 resources
Recipe: @recipe_files::/root/exemplo.rb
  * yum_package[apache] action install (up to date)
  * service[httpd] action enable (up to date)
  * service[httpd] action start (up to date)
  * file[/var/www/html/index.html] action create (up to date)
  * execute[meu-comando] action run
    - execute echo " Obrigado!" >> /etc/motd

Running handlers:
Running handlers complete
Chef Client finished, 1/5 resources updated in 32 seconds

Uma vez que nosso arquivo /etc/motd já possuía o conteúdo Bem Vindo! Obrigado! e desta vez não tentou garantir que o conteúdo do mesmo fosse apenas Bem Vindo!, vejamos como ele se encontra:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@kalib6 ~]# cat /etc/motd
Bem Vindo! Obrigado!
 Obrigado!
 ```

 **C**omo esperado, inserimos mais um ` Obrigado!` ao arquivo. Se executarmos novamente o chef-client, estaremos acrescentando novamente um ` Obrigado!` ao arquivo, pois o mesmo existe e não estamos mais tentando validar seu conteúdo.

 **E** se removermos manualmente o arquivo `/etc/motd`?

 ```
[root@kalib6 ~]# rm /etc/motd
rm: remove regular file ‘/etc/motd’? y
[root@kalib6 ~]# cat /etc/motd
cat: /etc/motd: No such file or directory

Vejamos o que o chef-client fará:

1
2
3
... (Ignorando linhas desnecessárias)
  * execute[meu-comando] action run (skipped due to only_if)
...

Como já tínhamos comentado as linhas que garantem que o /etc/motd existe e possui o conteúdo Bem Vindo!, o resource que incluiría Obrigado! foi ignorado, pois em nossa guard temos a condição only_if, que indica que o comando só será executado SE o arquivo /etc/motd existir. Para ter certeza disto, vamos verificar:

1
2
[root@kalib6 ~]# cat /etc/motd
cat: /etc/motd: No such file or directory

Aproveitando a situação, vamos alterar nossa recipe e utilizar not_if ao invés de only_if:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package 'apache' do
        package_name 'httpd'
        action :install
end

service 'httpd' do
        action [:enable, :start]
end

file '/var/www/html/index.html' do
        content 'Hello World!'
        mode '0755'
        owner 'root'
        group 'apache'
end

#file '/etc/motd' do
#       content 'Bem Vindo!'
#end

execute 'meu-comando' do
        command 'echo " Obrigado!" >> /etc/motd'
        not_if 'test -r /etc/motd'
end

Já sabemos que o arquivo não existe, portanto a nossa regra agora será satisfeita, visto que queremos adicionar o conteúdo Obrigado! APENAS caso o arquivo NÃO exista.

Vale lembrar que, por padrão, o comando echo ' Obrigado!' >> /etc/motd irá adicionar o conteúdo ao arquivo e, caso o mesmo não exista, ele será criado com o determinado conteúdo. Isto não é um recurso do Chef, mas sim do próprio linux/echo. Executando nosso chef-client:

1
2
3
4
5
6
... (Ignorando linhas desnecessárias)
* execute[meu-comando] action run
  - execute echo " Obrigado!" >> /etc/motd

Running handlers:
Running handlers complete

Repare que o arquivo foi criado apenas com o conteúdo Obrigado!, conforme descrito em nossa recipe.

1
2
[root@kalib6 ~]# cat /etc/motd
 Obrigado!

Caso o chef-client seja executado novamente, nada acontecerá, uma vez que o resource atual indica que o comando APENAS deverá ser executado caso o arquivo NÃO exista.

1
2
3
... (Ignorando linhas desnecessárias)
  * execute[meu-comando] action run (skipped due to not_if)
...

Simples, certo?! É importante lembrar que o Chef executará os resources na devida ordem em que forem listados na recipe, portanto é importante alinhar todas as instruções e resources de acordo com o resultado desejado.

Happy hacking!

Chef: Automação E Gerenciamento De Configuração

| Comments

Uma coisa de cada vez

Chef é uma popular ferramenta de Gerenciamento de Configurações criado pela empresa de mesmo nome, Chef. O Chef é desenvolvido em Ruby e Erlang, e utiliza uma linguagem DSL (domain-specific language) em Ruby puro para escrever arquivos de configuração de sistemas chamados “recipes” (receitas).

Antes de falarmos sobre o Chef é importante entender primeiramente o conceito e a utilidade de ferramentas de Gerenciamento de Configuração.

Gerenciamento de Configuração

Gerenciamento de Configuração, ou CM (Configuration Management), é um processo de engenharia de sistemas que visa garantir a consistência entre ativos físicos e lógicos em um ambiente operacional. O processo de gerenciamento de configuração busca identificar e rastrear itens individuais de configuração, documentando capacidades funcionais e interdependências. Administradores, técnicos e desenvolvedores de sistemas podem utilizar ferramentas de Gerenciamento de Configuração para verificar o efeito que uma mudança em um item de configuração terá em outros sistemas.

Em palavras simples, o objetivo de uma ferramenta de gerenciamento de configuração é simplificar a vida de quem administra serviços e sistemas garantindo uma uniformidade no quesito configuração.

Como exemplo prático e simplista, imagine um servidor web que será responsável por hospedar um pequeno site em php. Este servidor possuirá alguns atributos/aplicativos/configurações, tais como:

  • Apache instalado;
  • Alterações específicas nos arquivos de configuração do Apache;
  • Serviço do Apache ativo e iniciado;
  • Arquivos referentes ao site em si em um diretório específico;
  • Permissões específicas atribuídas ao diretório específico do site;
  • etc…

Configurar tudo isso manualmente em um único servidor é simples. Você poderia conectar-se via SSH no servidor, instalar o apache com o gerenciador de pacotes da distribuição utilizada, configurar o que for necessário no apache, iniciar o serviço, etc, etc, etc. Mas o que fazer quando sua infraestrutura cresce? Quando se quer maior disponibilidade do site, quando agora você roda este site em um cluster com 5 servidores?

Basicamente a mesma coisa, certo? Você pode se conectar em cada um dos servidores e repetir os mesmos passos. O problema se dá justamente nessa repetição de passos, onde você pode cometer erros, perder tempo, etc. Além disso, o que acontece se um colega seu modificar algo em um dos servidores e não lhe avisar? E se ele esquecer de replicar esta mudança nos demais?

É fácil achar diversas razões pelas quais torna-se difícil administrar e gerenciar configurações em ambientes mais complexos. A ideia por trás de uma ferramenta de Gerenciamento de Configuração é justamente reduzir esta complexidade, eliminando a necessidade de conectar-se manualmente à diversos servidores para aplicar as mesmas rotinas, passos e configurações.

Através de arquivos de texto podemos literalmente descrever o estado e as ações desejadas para nossos serviços e sistemas. Por exemplo: Posso dizer que possuo um grupo de servidores chamado WebServer, o qual contém 10 servidores com o Sistema Operacional CentOS 7. Posso incluir a informação de que preciso que todos eles estejam com a versão X do Apache instalada e que o serviço esteja ativo e rodando. Além disso, posso dizer que desejo que exista no diretório /var/www/meusite/ todo o conteúdo que está em um mapeamento de rede específico, ou mesmo em um repositório que possuo no github.

Ao invés de me conectar em cada um dos 10 servidores para fazer tudo isso, um simples comando será o suficiente. O comando em específico dependerá da solução adotada, visto que existem diversas ferramentas de CM (Gerenciamento de Configuração). Mas, basicamente, ele irá ler o(s) arquivo(s) de “instruções” que nós definimos e saberá em quais servidores ele deverá instalar o Apache e configurar de acordo com o especificado, nos dando apenas o resultado final em forma de relatório simples.

E se alguém da equipe alterar um arquivo de configuração diretamente em um dos servidores? A ferramenta, em sua próxima execucação, irá identificar que o estado desejado para aquele meu grupo de servidores está diferente em um dos servidores. Ela então modificará aquele arquivo específico naquele servidor para que ele volte ao seu estado desejado.

Além desta segurança, nós agora passamos a ter um ponto único de modificação. Ao desejarmos mudar algo, o faremos apenas no nosso “código”, ao invés de o fazer nos 10 servidores manualmente.

O mesmo benefício se dá em caso de erros e falhas: um ponto único de correção.

Com este mecanismo de descrever o estado de nossa infraestrutura em arquivos de texto/código, entramos em um novo conceito: Infrastructure as Code, ou Infraestrutura como Código. Uma vez que temos nossa infraestrutura em formato de código, podemos literalmente versionar e gerenciar nossa infraestrutura com repositórios Git, por exemplo.

O que é Chef?

Conforme dito mais acima, Chef é uma das mais populares ferramentas de Gerenciamento de Configuração disponíveis atualmente. É compatível e facilmente integrado à plataformas de computação em nuvem, tais como Internap, Amazon EC2, Google Cloud Platform, OpenStack, SoftLayer, Microsoft Azure e Rackspace, provendo e configurando servidores automaticamente.

O usuário escreve “recipes” (receitas) que descrevem como o Chef deve gerenciar aplicações, servidores e utilitários. Estas “recipes”, as quais podem ser agrupadas em “cookbooks” (livros de receitas) descrevem uma série de recursos que devem estar em um determinado estado. Este recursos podem ser pacotes, serviços ou mesmo arquivos.

Chef pode rodar em um modo cliente/servidor ou standalone, com o chamado “chef-solo”. No modo cliente/servidor, o cliente Chef envia uma série de atributos sobre o node ou cliente/host para o Chef server. O Chef server utiliza-se da ferramenta Solr para indexar estes atributos e provê uma API na qual os clientes podem fazer consultas. As recipes podem fazer requisições à esta base de atributos e utilizar os dados resultantes para configurar o cliente ou node.

Embora inicialmente o Chef fosse utilizado para gerenciar exclusivamente máquinas Linux, as versões mais atuais também suportam máquinas Windows.

A ideia para este post é dar uma breve introdução ao Chef, portanto não vou entrar em maiores detalhes do funcionamento por hora.

Resources

Como dito antes, um Resource é uma descrição de estado desejado para um determinado item. Estes resources são gerenciados através de recipes.

Um resource possui basicamente 4 componentes fundamentais que são definidos em um bloco de código Ruby:

  • Resource Type – Tipo de resource (Pode ser um pacote, serviço, arquivo…)
  • Resource Name – Nome do resource
  • Resource Properties – Propriedades do resource
  • Actions – Ações a serem aplicadas ao resource

Um exemplo de recipe para instalar o Apache em um servidor Ubuntu, por exemplo, seria o seguinte:

1
2
3
package 'httpd' do
    action :install
end

No exemplo acima temos o tipo de resource como sendo “package”, o nome do resource como sendo “httpd” e a ação “install”.

O que vai acontecer aqui? Simples de entender, certo? O pacote httpd (Apache) será instalado. Mas o que acontece caso o pacote httpd já esteja instalado?

Nada. Uma das características do Chef é a idempotência. Na matemática e ciência da computação, a idempotência é a propriedade que algumas operações têm de poderem ser aplicadas várias vezes sem que o valor do resultado se altere após a aplicação inicial. Ou seja, O Chef primeiramente confere se o estado desejado já está aplicado e, caso sim, ignora aquela instrução.

Novamente… A ideia para este post é dar uma breve introdução ao Chef, portanto não vou entrar em maiores detalhes sobre os tipos de resources e suas possíveis ações. Vamos ao que interessa…

Instalando o Chef

Conforme explicado acima, o Chef pode ser utilizado em modo cliente/servidor ou standalone. Para esta introdução utilizaremos o modo standalone ou local para simplificar as coisas.

Para instalar podemos utilizar o gerenciador de pacotes da distribuição Linux que utilizamos ou baixando o chefdk (Development Kit) através da página de downloads do chef.

Arch Linux

No meu caso, utilizarei o pacote chef-dk existente para o Arch Linux, mas sinta-se livre para baixar diretamente no site e executar o pacote de acordo com sua distribuição.

1- Baixar o pacote do AUR:

1
$ wget https://aur.archlinux.org/cgit/aur.git/snapshot/chef-dk.tar.gz

2- Descompactar e Compilar:

1
2
3
4
5
$ tar -xvzf chef-dk.tar.gz

$ cd chef-dk

$ makepkg

3- Instalar o pacote:

1
$ sudo pacman -U chef-dk-2.5.3-1-x86_64.pkg.tar.xz

4- Confirmar que deu tudo certo:*

1
2
3
4
5
6
7
8
$ chef --version

Chef Development Kit Version: 2.5.3
chef-client version: 13.8.5
delivery version: master (73ebb72a6c42b3d2ff5370c476be800fee7e5427)
berks version: 6.3.1
kitchen version: 1.20.0
inspec version: 1.51.21

Centos, Debian, Ubuntu…

No CentOS, Ubuntu, Debian ou outras distribuições, o procedimento será relativamente parecido, portanto vejamos como seria no caso do CentOS baixando o arquivo diretamente do site de downloads:

1- Baixar o arquivo .rpm para Red Hat: Aqui

1
$ wget https://packages.chef.io/files/stable/chefdk/2.5.3/el/7/chefdk-2.5.3-1.el7.x86_64.rpm

2- Instalar via RPM:

1
$ sudo rpm -ivh chefdk-2.5.3-1.el7.x86_64.rpm

3- Confirmar que deu tudo certo:

1
2
3
4
5
6
7
8
$ chef --version

Chef Development Kit Version: 2.5.3
chef-client version: 13.8.5
delivery version: master (73ebb72a6c42b3d2ff5370c476be800fee7e5427)
berks version: 6.3.1
kitchen version: 1.20.0
inspec version: 1.51.21

Testando o Chef Localmente

Para facilitar o entendimento, vamos criar uma recipe simples para aplicarmos localmente.

Vamos começar criando um arquivo chamado exemplo.rb com o seguinte conteúdo:

1
2
3
4
package 'apache' do
        package_name 'httpd'
        action :install
end

O que temos aqui?

  • Resource Type: package (Pois queremos instalar o pacote httpd)
  • Resource Name: apache (Embora o nome do pacote no CentOS seja httpd, o nome do nosso resource aqui é apache, mas poderia ser qualquer coisa que desejarmos)
  • Resource Properties: Aqui temos apenas package_name como propriedade, no qual damos o nome do pacote desejado. OBS: Caso não utilizemos a propriedade package_name, ele buscará por um pacote com mesmo nome do resource. No nosso caso, demos o nome apache para nosso resource, portanto ele buscaria por um pacote chamado apache e falharia, pois no CentOS este pacote não existe.
  • Actions: install (Temos apenas uma ação para esta recipe, que é justamente a de instalar o pacote, caso já não esteja instalado (idempotência))

Salve o arquivo e verifique o mesmo com os dois passos a seguir:

1- Verifique se a sintaxe ruby está correta:

1
2
3
# ruby -c exemplo.rb

Syntax OK

2- Utilize uma ferramenta do chef para verificar se a recipe está de acordo com o esperado pelo Chef:

1
2
3
4
5
6
# foodcritic exemplo.rb
Checking 1 files
x
FC011: Missing README in markdown format: ../README.md:1
FC031: Cookbook without metadata.rb file: ../metadata.rb:1
FC071: Missing LICENSE file: ../LICENSE:1

PS: Não se espante por enquanto com estes Warnings. Ele apenas está indicando que não possuímos um metadata, um readme e uma licença, pois não os criamos para este exemplo.

Verificado o código e aprovado, vamos executar esta recipe localmente. (Repare no retorno que será apresentado, onde ele verifica o tipo de resource e identifica que estamos rodando em uma máquina CentoOS, portanto utiliza por padrão o yum para instalar o pacote desejado.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# chef-client --local-mode exemplo.rb

[2018-04-01T19:18:00+00:00] WARN: No config file found or specified on command line, using command line options.
[2018-04-01T19:18:00+00:00] WARN: No cookbooks directory found at or above current directory.  Assuming /root.
Starting Chef Client, version 13.8.5
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Installing Cookbook Gems:
Compiling Cookbooks...
[2018-04-01T19:18:03+00:00] WARN: Node kalib6.test.com has an empty run list.
Converging 1 resources
Recipe: @recipe_files::/root/exemplo.rb
  * yum_package[apache] action install
    - install version 2.4.6-67.el7.centos.6 of package httpd

Running handlers:
Running handlers complete
Chef Client finished, 1/1 resources updated in 16 seconds
[2018-04-01T19:18:17+00:00] WARN: No config file found or specified on command line, using command line options.

Simples, certo? O pacote httpd (Apache) foi instalado em nosso CentOS. Verificando o status do serviço httpd, veremos que o serviço não está rodando e também não está ativo.

1
2
3
4
5
6
# systemctl status httpd
● httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; disabled; vendor preset: disabled)
   Active: inactive (dead)
     Docs: man:httpd(8)
           man:apachectl(8)

Isto está correto, afinal o Chef vai deixar a máquina no estado que determinamos. O determinado foi apenas instalar o pacote httpd. Mas de nada ele serve sem estar rodando como serviço, portanto vamos editar nossa recipe exemplo.rb e incluir nela um novo resource, desta vez um resource do tipo service, ou serviço. Sim, podemos ter diversos resources em uma mesma recipe. ;]

Edite sua recipe para que ela possua o seguinte conteúdo:

1
2
3
4
5
6
7
8
package 'apache' do
        package_name 'httpd'
        action :installchef_httpd_default.png
end

service 'httpd' do
        action [:enable, :start]
end

O que adicionamos aqui?

  • Resource Type: service (Pois queremos gerenciar o serviço httpd)
  • Resource Name: httpd (Poderíamos ter utilizado qualquer nome, mas para simplificar e não precisarmos utilizar uma propriedade de nome, deixaremos o resource com o nome do serviço, httpd)
  • Actions: enable e start (Como nosso objetivo é não apenas iniciar o serviço, mas também deixá-lo habilitado para ser iniciado automaticamente após reinicialização, utilizaremos o enable e o start) –> equivalente aos comandos systemctl enable httpd e systemctl start httpd

Novamente vamos verificar nosso código via ruby e foodcritic:

1
# ruby -c exemplo.rb && foodcritic exemplo.rb

E executando nossa recipe. (Novamente, o pacote httpd já está instalado, esta parte da recipe será ignorada automaticamente pelo Chef.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# chef-client --local-mode exemplo.rb
[2018-04-01T19:32:26+00:00] WARN: No config file found or specified on command line, using command line options.
[2018-04-01T19:32:26+00:00] WARN: No cookbooks directory found at or above current directory.  Assuming /root.
Starting Chef Client, version 13.8.5
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Installing Cookbook Gems:
Compiling Cookbooks...
[2018-04-01T19:32:28+00:00] WARN: Node kalib6.test.com has an empty run list.
Converging 2 resources
Recipe: @recipe_files::/root/exemplo.rb
  * yum_package[apache] action install (up to date)
  * service[httpd] action enable
    - enable service service[httpd]
  * service[httpd] action start
    - start service service[httpd]

Running handlers:
Running handlers complete
Chef Client finished, 2/3 resources updated in 06 seconds
[2018-04-01T19:32:33+00:00] WARN: No config file found or specified on command line, using command line options.

Agora podemos verificar que nosso serviço httpd está de fato rodando.

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
# systemctl status httpd && ps aux | grep httpd
● httpd.service - The Apache HTTP Server
   Loaded: loaded (/usr/lib/systemd/system/httpd.service; enabled; vendor preset: disabled)
   Active: active (running) since Sun 2018-04-01 19:32:33 UTC; 1min 17s ago
     Docs: man:httpd(8)
           man:apachectl(8)
 Main PID: 2409 (httpd)
   Status: "Total requests: 0; Current requests/sec: 0; Current traffic:   0 B/sec"
   CGroup: /system.slice/httpd.service
           ├─2409 /usr/sbin/httpd -DFOREGROUND
           ├─2410 /usr/sbin/httpd -DFOREGROUND
           ├─2411 /usr/sbin/httpd -DFOREGROUND
           ├─2412 /usr/sbin/httpd -DFOREGROUND
           ├─2413 /usr/sbin/httpd -DFOREGROUND
           └─2414 /usr/sbin/httpd -DFOREGROUND

Apr 01 19:32:33 kalib6.test.com systemd[1]: Starting The Apache HTTP Server...
Apr 01 19:32:33 kalib6.test.com systemd[1]: Started The Apache HTTP Server.
root      2409  0.0  0.2 226040  4948 ?        Ss   19:32   0:00 /usr/sbin/httpd -DFOREGROUND
apache    2410  0.0  0.1 226040  2872 ?        S    19:32   0:00 /usr/sbin/httpd -DFOREGROUND
apache    2411  0.0  0.1 226040  2872 ?        S    19:32   0:00 /usr/sbin/httpd -DFOREGROUND
apache    2412  0.0  0.1 226040  2872 ?        S    19:32   0:00 /usr/sbin/httpd -DFOREGROUND
apache    2413  0.0  0.1 226040  2872 ?        S    19:32   0:00 /usr/sbin/httpd -DFOREGROUND
apache    2414  0.0  0.1 226040  2872 ?        S    19:32   0:00 /usr/sbin/httpd -DFOREGROUND
root      2425  0.0  0.0 112660   976 pts/0    R+   19:33   0:00 grep --color=auto httpd

Além disso, você pode testar seu novo servidor web diretamente em seu navegador. Caso esteja executando tudo em localhost, pode utilizar localhost como endereço. Caso contrário, pode utilizar o endereço ip da máquina em questão.

A página padrão do Apache não é algo que queremos, portanto vamos criar nosso próprio site (Hello World) para exemplificar melhor. Para isto editaremos novamente nossa recipe e incluiremos mais um resource, do tipo file, ou arquivo. O conteúdo de sua recipe deverá ficar assim:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package 'apache' do
        package_name 'httpd'
        action :install
end

service 'httpd' do
        action [:enable, :start]
end

file '/var/www/html/index.html' do
        content 'Hello World!'
        mode '0755'
        owner 'root'
        group 'apache'
end

O que adicionamos aqui?

  • Resource Type: file (Pois queremos gerenciar o nosso arquivo index.html, o qual, caso não exista, será criado)
  • Resource Name: /var/www/html/index.html (Poderíamos ter utilizado qualquer nome, mas para simplificar e não precisarmos utilizar uma propriedade de nome, deixaremos o resource com o nome do arquivo que vamos utilizar, /var/www/html/index.html)
  • Content: Hello World! (Para simplificar teremos uma simples string Hello World! como conteúdo de nosso site)
  • Mode: Permissão que desejamos atribuir ao arquivo index.html
  • Owner: Dono que deve ser atribuído ao arquivo index.html
  • Group: Grupo que deve ser atribuído ao arquivo index.html

Novamente vamos verificar nosso código via ruby e foodcritic:

1
# ruby -c exemplo.rb && foodcritic exemplo.rb

E executaremos novamente nossa recipe. (Assim como anteriormente, o Chef ignorará as instruções referentes aos resources que já se encontram no estado desejado)

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
# chef-client --local-mode exemplo.rb          
[2018-04-01T19:48:00+00:00] WARN: No config file found or specified on command line, using command line options.
[2018-04-01T19:48:00+00:00] WARN: No cookbooks directory found at or above current directory.  Assuming /root.
Starting Chef Client, version 13.8.5
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Installing Cookbook Gems:
Compiling Cookbooks...
[2018-04-01T19:48:02+00:00] WARN: Node kalib6.test.com has an empty run list.
Converging 3 resources
Recipe: @recipe_files::/root/exemplo.rb
  * yum_package[apache] action install (up to date)
  * service[httpd] action enable (up to date)
  * service[httpd] action start (up to date)
  * file[/var/www/html/index.html] action create
    - create new file /var/www/html/index.html
    - update content in file /var/www/html/index.html from none to 7f83b1
    --- /var/www/html/index.html        2018-04-01 19:48:06.829566745 +0000
    +++ /var/www/html/.chef-index20180401-2631-164958p.html     2018-04-01 19:48:06.829566745 +0000
    @@ -1 +1,2 @@
    +Hello World!
    - change mode from '' to '0755'
    - change owner from '' to 'root'
    - change group from '' to 'apache'
    - restore selinux security context

Running handlers:
Running handlers complete
Chef Client finished, 1/4 resources updated in 06 seconds
[2018-04-01T19:48:07+00:00] WARN: No config file found or specified on command line, using command line options.

Podemos verificar que o Chef criou o nosso arquivo index.html, atribuindo o dono, grupo e permissão que indicamos:

1
2
3
# ls -lh /var/www/html/
total 4.0K
-rwxr-xr-x. 1 root apache 12 Apr  1 19:48 index.html

Também podemos voltar em nosso navegador e atualizar a página para que vejamos o nosso Hello World ao invés da página padrão do Apache.

Vale a pena?

Realizar este processo manualmente na mesma máquina CentoOS seria mais rápido do que utilizando o Chef. Vamos rever:

O que precisamos?

  • Instalar o pacote httpd
  • Habilitar e Iniciar o serviço httpd
  • Criar o arquivo index.html com o conteúdo “Hello World!”

Fazendo manualmente seria apenas uma questão de executarmos 4 comandos:

1
2
3
4
5
6
7
# yum install httpd

# systemctl enable httpd && systemctl start httpd

# echo "Hello World!" > /var/www/html/index.html

# chmod 0755 /var/www/html/index.html && chown root:apache /var/www/html/index.html

Sim, é verdade que fazendo manualmente neste caso seria MUITO mais rápido. O importante é lembrar do que falamos anteriormente: E se não for apenas 1 servidor? E se for um grupo? E se ao invés de apenas 3 resources, tiver 15? ou 40? E se alguém modificar algo em algum dos resources? Como você saberá qual foi? Vai verificar todos um a um para identificar o que precisa ser corrigido?

Vantagens:

Imagine que algum membro de sua equipe alterou a permissão do arquivo index.html sem lhe avisar, por exemplo ele foi lá e…

1
# chmod 0666 /var/www/html/index.html

Lembrando que em nossa recipe exemplo.rb, definimos a permissão 0755. Neste caso, sempre que executarmos a recipe, o Chef irá verificar todos os resources e corrigir o que quer que tenha sido alterado.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# chef-client --local-mode exemplo.rb
[2018-04-01T20:04:08+00:00] WARN: No config file found or specified on command line, using command line options.
[2018-04-01T20:04:08+00:00] WARN: No cookbooks directory found at or above current directory.  Assuming /root.
Starting Chef Client, version 13.8.5
resolving cookbooks for run list: []
Synchronizing Cookbooks:
Installing Cookbook Gems:
Compiling Cookbooks...
[2018-04-01T20:04:10+00:00] WARN: Node kalib6.test.com has an empty run list.
Converging 3 resources
Recipe: @recipe_files::/root/exemplo.rb
  * yum_package[apache] action install (up to date)
  * service[httpd] action enable (up to date)
  * service[httpd] action start (up to date)
  * file[/var/www/html/index.html] action create
    - change mode from '0666' to '0755'
    - restore selinux security context

Running handlers:
Running handlers complete
Chef Client finished, 1/4 resources updated in 06 seconds
[2018-04-01T20:04:14+00:00] WARN: No config file found or specified on command line, using command line options.

Repare na linha – change mode from ‘0666’ to ‘0755’. O Chef acabou de corrigir automaticamente sem que nós tenhamos de vasculhar cada componente e arquivo em nosso servidor para saber o que foi alterado ou o que está diferente do estado desejado. Novamente, aqui tratamos de um servidor único, com apenas um arquivo. Imagine ter que varrer manualmente diversos servidores e diversos arquivos e diretórios?

Neste exemplo, provavelmente o site poderia continuar funcionando, pois foi alterada apenas a permissão de um único arquivo, certo? Mas imagine que sem querer ele acabou parando o serviço httpd, ou até desinstalou o mesmo? Em uma instalação padrão com Chef Server, existem agendamentos que fazem com que o Chef execute as recipes a cada X minutos, portanto o serviço seria inicializado novamente automaticamente, ou mesmo instalado caso necessário.

É fácil imaginar diversos cenários nos quais seria útil ter a sua infraestrutura em formato de código.

Imagine uma catástrofe em que seu servidor simplesmente parou de funcionar e você precisará criar outro. Novamente você teria que executar aqueles comandos. Se desde o início tivesse utilizado Chef, ou outra ferramenta de Gerenciamento de Configuração, você poderia ter a sua recipe armazenada em um repositório Git, por exemplo, conforme mencionado no início deste post, e bastaria apenas executar o seu chef-client para instalar os pacotes necessários, habilitar e inicializar serviços, criar arquivos, etc.

A imaginação é o seu limite. ;]

Em posts futuros pretendo explorar mais a fundo o Chef, bem como outras ferramentas.

Happy Hacking!

Conhecendo O Kubernetes - Clusters De Containers

| Comments

O que é Kubernetes

Kubernetes é uma solução Open Source desenvolvida pelo Google, originalmente chamada de K8s, como uma ferramenta para gerenciar clusters de containers (ou containeres, como prefira). Em 2005, quando a ferramenta foi desenvolvida, originalmente para uso interno, o Google doou o código à recém fundada Cloud Native Computing Foundation, em parceria com a The Linux Foundation.

O motivo do leme em sua logomarca é devido à origem grega da palavra, que vem de Kuvernetes, que representa a pessoa que pilota o navio, timoneiro.

Como objetivo primário o Kubernetes provê uma plataforma de automação para deployments, escalonamento e operações de containers de aplicações em um cluster de hosts ou nodes.

Antes de seguir com a explicação, instalação e configuração do Kubernetes, estou supondo que você já possui algum conhecimento básico sobre o que sejam containers e tenha alguma familiaridade com o Docker. Caso não possua um entendimento básico sobre containers e Docker, sugiro que leia algo antes de seguir com este artigo. Possuo um post introdutório sobre containers com um exemplo básico e prático sobre como criar containers com Docker, bem como iniciar uma simples aplicação web – aqui.

O Kubernetes é formado por uma série de componentes ou blocos que, quando utilizados coletivamente, fornecem um método de deployment, manutenção e escalonamento de clusters de aplicações baseadas em containers. Estes componentes, ou primitives como o Kubernetes os chama, foram desenvolvidos com o intuito de serem independentes, de forma que quase não se faz necessário ter conhecimento entre si para que possam funcionar e trabalhar juntos, visto que todos se comunicam e interligam através de uma API, sejam componentes internos do Kubernetes ou mesmo extensões e containers.

Embora tenha sido inicialmente desenvolvido para o deployment e utilização de bilhões de containers internamente no Google, desde que seu código passou a ser distribuído abertamente com a licença Apache Commons o Kubernetes tem sido adotado formalmente por praticamente todos os grandes provedores de serviços em nuvem.

Arquitetura do Kubernetes

Dentre os principais componentes do Kubernetes, vamos destacar os seguintes:

  • Master ou Master Controller – Host que será o gerenciador principal do Kubernetes, responsável por gerenciar os Minions ou Nodes do Cluster;

  • Nodes ou Minions – Embora normalmente a nomenclatura em diversos serviços de tecnolocia seja Node, o Kubernetes prefere chamar de Minions os hosts que fazem parte de um Cluster gerenciado pelo próprio Kubernetes. Este minion pode ser um servidor físico ou virtual, necessitando possuir um serviço de gerenciamento de containers, como o Docker, por exemplo;

  • ETCD – Embora este seja um serviço independente, estou listando-o aqui pois este será fundamental em seu ciclo de desenvolvimento com o Kubernetes. Cada Minion deverá rodar o ETCD (serviço de comunicação e gerenciamento de configurações no formato par de Chave/Valor). O ETCD é utilizado para troca e armazenamento de informações sobre os containers, pods, minions, etc.

  • Pods – São grupos de containers (um ou mais) rodando em um único minion do cluster. Cada Pod receberá um endereço IP único no Cluster como forma de possibilitar a utilização de portas sem a necessidade de se preocupar com conflitos;

  • Labels – São informações de identificação na configuração e gerenciamento dos objetos (como Pods ou Minions) formados de pares “chave:valor”;

  • Controllers – Além do Master Controller, dependendo do tamanho de sua infraestrutura e quantidade de Pods e Minions, você pode optar por ter mais de um Controller para dividir a carga e tarefas de gerenciamento. Os Controllers gerenciam um grupo de pods e, dependendo do estado de configuração desejada, podem acionar outros Controllers para lidar com as replicações e escalonamento. Os Controllers também são responsáveis pela substituiçao de Pods, caso um entre em estado de falha.

Instalação

Vamos ao que interessa…

Novamente estou supondo que você já possui alguma familiaridade com Containers, Docker e, por consequência, com GNU/Linux.

Eestarei utilizando 4 servidores virtuais rodando CentOS 7 nos exemplos a seguir, mas fica a seu critério decidir quantos utilizar.

Certamente você optar por utilizar outra distribuição, seja Debian, Ubuntu, etc.. Uma vez que optei pelo CentOS 7, estarei utilizando comandos voltados para esta distro, mas sinta-se livre para adaptar seus comandos, como substituir o “yum” pelo “apt-get”, “pacman”, etc..

Em minha configuração chamarei os servidores da seguinte forma:

  • centos-master
  • centos-minion1
  • centos-minion2
  • centos-minion3

A primeira coisa que se deve fazer sempre que se pensa em trabalhar com clusters, independente de ser um cluster de containers ou não, é ter a certeza de que os servidores terão uma correta sincronização de relógios entre si. A forma mais simples e eficiente no nosso contexto é com a utilização do NTP, portanto comece instalando o NTP nos 4 servidores, bem como habilitando o serviço e iniciando-o:

1
# yum install -y ntp
1
# systemctl enable ntpd && systemctl start ntpd

Caso queira certificar-se de que o serviço está realmente rodando:

1
2
3
4
5
6
7
8
9
10
# systemctl status ntpd

● ntpd.service - Network Time Service
   Loaded: loaded (/usr/lib/systemd/system/ntpd.service; enabled; vendor preset: disabled)
   Active: active (running) since Sat 2017-06-24 17:46:02 UTC; 3s ago
  Process: 1586 ExecStart=/usr/sbin/ntpd -u ntp:ntp $OPTIONS (code=exited, status=0/SUCCESS)
 Main PID: 1587 (ntpd)
   Memory: 2.1M
   CGroup: /system.slice/ntpd.service
           └─1587 /usr/sbin/ntpd -u ntp:ntp -g

Pinga?

É importante nos certificarmos de que os servidores conseguem se comunicar e de que conseguem resolver nomes corretamente.

Neste exemplo, conforme informado mais acima, estamos utilizando 4 servidores com os seguintes nomes: centos-master, centos-minion1, centos-minion2 e centos-minion3, portanto vamos editar o arquivo /etc/hosts de cada um deles para que possam se comunicar pelos nomes que desejamos:

Insira as seguintes linhas no arquivo /etc/hosts dos 4 servidores:

Lembre-se de substituir os IPs pelos IPs dos servidores em seu ambiente

1
2
3
4
5
6
7
8
9
10
11
# Ip local do servidor master
172.31.22.126   centos-master

# Ip local do minion1
172.31.120.16   centos-minion1

# Ip local do minion2
172.31.25.6     centos-minion2

# Ip local do minion3
172.31.123.22   centos-minion3

Feito isto, tente pingar do master para os 3 minions utilizando os nomes especificados no /etc/hosts:

1
2
3
4
5
6
7
8
9
10
11
[root@kalib1 ~]# ping centos-minion1
PING centos-minion1 (172.31.120.16) 56(84) bytes of data.
64 bytes from centos-minion1 (172.31.120.16): icmp_seq=1 ttl=64 time=1.06 ms

[root@kalib1 ~]# ping centos-minion2
PING centos-minion2 (172.31.25.6) 56(84) bytes of data.
64 bytes from centos-minion2 (172.31.25.6): icmp_seq=1 ttl=64 time=0.588 ms

[root@kalib1 ~]# ping centos-minion3
PING centos-minion3 (172.31.123.22) 56(84) bytes of data.
64 bytes from centos-minion3 (172.31.123.22): icmp_seq=1 ttl=64 time=1.24 ms

Você pode realizar o mesmo teste a partir dos minions, pingando entre si e também para o centos-master.

Uma vez que tenhamos certeza de que todos os hosts se comunicam, é hora de instalar mais alguns pacotes necessários.

Primeiramente, vamos configurar o repositório do Docker para o CentOS 7:

Vamos criar o seguinte arquivo de repositório:

1
# vim /etc/yum.repos.d/virt7-docker-common-release.repos

O conteúdo deste arquivo será o seguinte:

1
2
3
4
[virt7-docker-common-release]
name=virt7-docker-common-release
baseurl=https://cbs.centos.org/repos/virt7-docker-common-release/x86_64/os/
gpgcheck=0

Este arquivo deverá ser criado nos 4 servidores.

Em seguida, vamos atualizar a nossa base de repositórios e pacotes, também nos 4 servidores, bem como habilitar o novo repositório para instalar os pacotes docker e kubernetes:

1
# yum update
1
# yum install -y --enablerepo=virt7-docker-common-release docker kubernetes

Como dito na introdução, também precisaremos do etcd para o armazenamento e troca de configurações, portanto vamos instalá-lo também nos 4 hosts:

1
# yum instal -y etcd

Configuração

Vamos começar com a configuração básica dos serviços envolvidos. Primeiramente, vamos abrir o arquivo de configuração do kubernetes e fazer algumas alterações:

/etc/kubernetes/config

No arquivo config altere as seguintes linhas:

Edite o valor do parâmetro KUBE_MASTER, de forma que nosso master possa ser encontrado pelo nome que definimos no hosts file. O valor original é “—master=https://127.0.0.1:8080”, portanto mudaremos para o seguinte:

1
KUBE_MASTER="--master=https://centos-master:8080"

Ainda neste arquivo de configuração, vamos inserir a configuração do serviço ETCD, portanto inclua a seguinte linha ao final do arquivo:

1
KUBE_ETCD_SERVERS="--etcd-servers=https://centos-master:2379"

Seu arquivo de configuração deverá estar similar a este:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
###
# kubernetes system config
#
# The following values are used to configure various aspects of all
# kubernetes services, including
#
#   kube-apiserver.service
#   kube-controller-manager.service
#   kube-scheduler.service
#   kubelet.service
#   kube-proxy.service
# logging to stderr means we get it in the systemd journal
KUBE_LOGTOSTDERR="--logtostderr=true"

# journal message level, 0 is debug
KUBE_LOG_LEVEL="--v=0"

# Should this cluster be allowed to run privileged docker containers
KUBE_ALLOW_PRIV="--allow-privileged=false"

# How the controller-manager, scheduler, and proxy find the apiserver
KUBE_MASTER="--master=https://centos-master:8080"

KUBE_ETCD_SERVERS="--etcd-servers=https://centos-master:2379"

Repita esta mesma configuração nos 4 hosts. Todos eles devem utilizar exatamente os mesmos valores utilizados aqui, apontando KUBE_MASTER e KUBE_ETCD_SERVERS para centos-master, visto que este será o responsável por gerenciar todos os nossos minions.

Uma vez que o arquivo de configuração do kubernetes esteja pronto nos 4 hosts, vamos configurar o serviço de API do kubernetes:

/etc/kubernetes/apiserver

Esta configuração abaixo será apenas para o Master.

Edite o valor do parâmetro KUBE_API_ADDRESS, que originalmente é “—insecure-bind-address=127.0.0.1”, de forma que possamos novamente receber comunicação dos demais hosts.

1
KUBE_API_ADDRESS="--address=0.0.0.0"

Descomente as linhas KUBE_API_PORT e KUBELET_PORT, para que possamos estabelecer as portas de comunicação com a API:

1
2
3
4
5
# The port on the local server to listen on.
KUBE_API_PORT="--port=8080"

# Port minions listen on
KUBELET_PORT="--kubelet-port=10250"

Em nosso exemplo não utilizaremos o parâmetro KUBE_ADMISSION_CONTROL, o qual nos permite ter mais controles e restrições sobre quais nodes ou minios podem entrar em nosso ambiente, portanto vamos apenas comentar esta linha por enquanto:

1
2
# default admission control policies
# KUBE_ADMISSION_CONTROL="--admission-control=NamespaceLifecycle,NamespaceExists,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota"

Nosso arquivo /etc/kubernetes/apiserver deverá 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
##
# kubernetes system config
#
# The following values are used to configure the kube-apiserver
#

# The address on the local server to listen to.
KUBE_API_ADDRESS="--address=0.0.0.0"

# The port on the local server to listen on.
KUBE_API_PORT="--port=8080"

# Port minions listen on
KUBELET_PORT="--kubelet-port=10250"

# Comma separated list of nodes in the etcd cluster
KUBE_ETCD_SERVERS="--etcd-servers=https://127.0.0.1:2379"

# Address range to use for services
KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=10.254.0.0/16"

# default admission control policies
# KUBE_ADMISSION_CONTROL="--admission-control=NamespaceLifecycle,NamespaceExists,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota"

# Add your own!
KUBE_API_ARGS=""

Salve e feche o arquivo. Novamente, esta configuração deve ser feita apenas para o Master.

Agora vamos configurar o serviço ETCD:

/etc/etcd/etcd.conf

Esta configuração abaixo será apenas para o Master.

Edite os valores dos parâmetros ETCD_LISTEN_CLIENT_URLS e ETCD_ADVERTISE_CLIENT_URLS, que originalmente apontam para localhost. Como desejamos que nosso etcd escute requisições dos demais hosts, altere para o seguinte:

1
2
3
4
ETCD_LISTEN_CLIENT_URLS="https://0.0.0.0:2379"
...
...
ETCD_ADVERTISE_CLIENT_URLS="https://0.0.0.0:2379"

Novamente, não é necessário alterar a configuração do etcd nos demais hosts, apenas no Master.

Uma vez que as configurações iniciais foram feitas, vamos habilitar e iniciar os serviços necessários no Master, sendo eles:

  • etcd
  • kube-apiserver
  • kube-controller-manager
  • kube-scheduler
1
# systemctl enable etcd kube-apiserver kube-controller-manager kube-scheduler
1
# systemctl start etcd kube-apiserver kube-controller-manager kube-scheduler

Os 4 serviços devem estar rodando. Para termos certeza, vamos checar o status dos mesmos:

1
2
3
4
5
# systemctl status etcd kube-apiserver kube-controller-manager kube-scheduler | grep "(running)"
   Active: active (running) since Sat 2017-06-24 21:45:37 UTC; 1min ago
   Active: active (running) since Sat 2017-06-24 21:46:13 UTC; 1min ago
   Active: active (running) since Sat 2017-06-24 21:44:25 UTC; 1min ago
   Active: active (running) since Sat 2017-06-24 21:44:25 UTC; 1min ago

Novamente, estes serviços serão iniciados no Master, e não nos nodes/minions, visto que estes utilizarão outros serviços.

Agora vamos configurar o seguinte arquivo nos nodes/minions:

/etc/kubernetes/kubelet

Este arquivo apenas deverá ser editado nos nodes/minions, não no Master.

Vamos alterar o valor do parâmetro KUBELET_ADDRESS para que aceite comunicação não apenas do localhost:

1
KUBELET_ADDRESS="--address=0.0.0.0"

Descomentaremos também a linha KUBELET_PORT, para que possamos ter uma porta definida para a comunicação do kubelet:

1
2
# The port for the info server to serve on
KUBELET_PORT="--port=10250"

Vamos alterar o valor do parâmetro KUBELET_HOSTNAME para o nome que definimos para cada um dos minions, portanto em cada um deles este será um valor diferente. Supondo que este seja o minion1, utilizaremos:

1
KUBELET_HOSTNAME="--hostname-override=centos-minion1"

Vamos também alterar o valor para KUBELET_API_SERVER, apontando para o nosso Master:

1
KUBELET_API_SERVER="--api-servers=https://centos-master:8080"

Vamos comentar a linha KUBELET_POD_INFRA_CONTAINER, visto que não utilizaremos uma infraestrutura de containers externa, pois estaremos utilizando nossos próprios PODs e containers:

1
2
# pod infrastructure container
#KUBELET_POD_INFRA_CONTAINER="--pod-infra-container-image=registry.access.redhat.com/rhel7/pod-infrastructure:latest"

Nosso arquivo deverá estar assim: (Lembrando que o parâmetro KUBELET_HOSTNAME deverá ser diferente para cada um dos 3 minions, respectivamente: centos-minion1, centos-minion2 e centos-minion3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
###
# kubernetes kubelet (minion) config

# The address for the info server to serve on (set to 0.0.0.0 or "" for all interfaces)
KUBELET_ADDRESS="--address=0.0.0.0"

# The port for the info server to serve on
KUBELET_PORT="--port=10250"

# You may leave this blank to use the actual hostname
KUBELET_HOSTNAME="--hostname-override=centos-minion1"

# location of the api-server
KUBELET_API_SERVER="--api-servers=https://centos-master:8080"

# pod infrastructure container
#KUBELET_POD_INFRA_CONTAINER="--pod-infra-container-image=registry.access.redhat.com/rhel7/pod-infrastructure:latest"

# Add your own!
KUBELET_ARGS=""

Uma vez que estas configurações também estão feitas nos 3 minions, vamos habilitar e iniciar os serviços necessários nos minions:

  • kube-proxy
  • kube-kubelet
  • docker
1
# systemctl enable kube-proxy kubelet docker
1
# systemctl start kube-proxy kubelet docker

Novamente, vamos ter certeza de que os 3 serviços estão rodando:

1
2
3
4
# systemctl status kube-proxy kubelet docker | grep "(running)"
   Active: active (running) since Sat 2017-06-24 21:44:23 UTC; 1h 16min ago
   Active: active (running) since Sat 2017-06-24 21:44:27 UTC; 1h 16min ago
   Active: active (running) since Sat 2017-06-24 21:44:27 UTC; 1h 16min ago

Novamente, estes 3 serviços devem ser habilitados e iniciados nos 3 minions.

Neste momento já temos nosso cluster rodando, com um master e 3 minions. :D

Testando o Cluster com o Kubernetes

Agora que temos a configuração básica de nosso Master Controller e de 3 minions, vamos testar nosso cluster.

Utilizaremos o utilitário kubectl (KubeControl) disponível com o kubernetes. Caso tenha interesse em ver os parâmetros e funções do mesmo… $ man kubectl

Vamos verificar a lista dos nodes ou minions que temos neste momento registrados em nosso Cluster. Vamos digitar alguns comandos em nosso Master Controller (centos-master):

1
2
3
4
5
[root@kalib1 ~]# kubectl get nodes
NAME             STATUS    AGE
centos-minion1   Ready     17m
centos-minion2   Ready     15m
centos-minion3   Ready     10m

Os três nodes criados e configurados anteriormente já são reconhecidos pelo nosso Kubernetes através do Master Controller. Além de registrados, estão com o status Ready, o que indica que estão prontos para funcionar e executar o que precisarmos.

Caso deseje conhecer mais parâmetros que a função get do kubectl possui, podemos invocar o manual desta função: $ man kubectl-get

Além do status, podemos conseguir diversas outras informações dos nodes através do kubectl: (Ex: kubectl describe nodes) Isto lhe daria informações sobre todos os nodes. Vamos experimentar com um node em específico.

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
[root@kalib1 ~]# kubectl describe node centos-minion1
Name:                   centos-minion1
Role:
Labels:                 beta.kubernetes.io/arch=amd64
                        beta.kubernetes.io/os=linux
                        kubernetes.io/hostname=centos-minion1
Taints:                 <none>
CreationTimestamp:      Tue, 20 Jun 2017 19:27:31 +0000
Phase:
Conditions:
  Type                  Status  LastHeartbeatTime                       LastTransitionTime                      Reason     Message
  ----                  ------  -----------------                       ------------------                      ------     -------
  OutOfDisk             False   Sun, 25 Jun 2017 01:39:38 +0000         Fri, 23 Jun 2017 17:31:44 +0000         KubeletHasSufficientDisk    kubelet has sufficient disk space available
  MemoryPressure        False   Sun, 25 Jun 2017 01:39:38 +0000         Tue, 20 Jun 2017 19:27:31 +0000         KubeletHasSufficientMemory  kubelet has sufficient memory available
  DiskPressure          False   Sun, 25 Jun 2017 01:39:38 +0000         Tue, 20 Jun 2017 19:27:31 +0000         KubeletHasNoDiskPressure    kubelet has no disk pressure
  Ready                 True    Sun, 25 Jun 2017 01:39:38 +0000         Fri, 23 Jun 2017 17:31:54 +0000         KubeletReady                        kubelet is posting ready status
Addresses:              172.31.120.16,172.31.120.16,centos-minion1
Capacity:
 alpha.kubernetes.io/nvidia-gpu:        0
 cpu:                                   1
 memory:                                1015348Ki
 pods:                                  110
Allocatable:
 alpha.kubernetes.io/nvidia-gpu:        0
 cpu:                                   1
 memory:                                1015348Ki
 pods:                                  110
System Info:
 Machine ID:                    f9afeb75a5a382dce8269887a67fbf58
 System UUID:                   EC2C8A0E-91D6-F54E-5A49-534A6A903FDA
 Boot ID:                       20961efd-c946-481a-97cb-7788209551ae
 Kernel Version:                3.10.0-327.28.2.el7.x86_64
 OS Image:                      CentOS Linux 7 (Core)
 Operating System:              linux
 Architecture:                  amd64
 Container Runtime Version:     docker://1.12.6
 Kubelet Version:               v1.5.2
 Kube-Proxy Version:            v1.5.2
ExternalID:                     centos-minion1
Non-terminated Pods:            (0 in total)
  Namespace                     Name            CPU Requests    CPU Limits      Memory Requests Memory Limits
  ---------                     ----            ------------    ----------      --------------- -------------
Allocated resources:
  (Total limits may be over 100 percent, i.e., overcommitted.
  CPU Requests  CPU Limits      Memory Requests Memory Limits
  ------------  ----------      --------------- -------------
  0 (0%)        0 (0%)          0 (0%)          0 (0%)
Events:
  FirstSeen     LastSeen        Count   From                            SubObjectPath   Type            Reason             Message
  ---------     --------        -----   ----                            -------------   --------        ------             -------
  15m           15m             1       {kubelet centos-minion1}                        Normal          Starting           Starting kubelet.
  15m           15m             1       {kubelet centos-minion1}                        Warning         ImageGCFailed      unable to find data for container /
  15m           15m             2       {kubelet centos-minion1}                        Normal          NodeHasSufficientDisk       Node centos-minion1 status is now: NodeHasSufficientDisk
  15m           15m             2       {kubelet centos-minion1}                        Normal          NodeHasSufficientMemory     Node centos-minion1 status is now: NodeHasSufficientMemory
  15m           15m             2       {kubelet centos-minion1}                        Normal          NodeHasNoDiskPressure       Node centos-minion1 status is now: NodeHasNoDiskPressure
  15m           15m             1       {kubelet centos-minion1}                        Warning         Rebooted           Node centos-minion1 has been rebooted, boot id: 20961efd-c946-481a-97cb-7788209551ae

Obviamente recebemos um retorno com muitas informações em formato Json, o que nem sempre é como esperamos. Existem formas de filtrar os resultados e conseguir informações mais precisas, como o bom e velho grep:

1
2
[root@kalib1 ~]# kubectl describe node centos-minion1 | grep Addresses
Addresses:              172.31.120.16,172.31.120.16,centos-minion1

Você também pode utilizar expressões regulares e a sintaxe do próprio Kubernetes para consultas mais complexas, como por exemplo, formatar a minha saída Json de forma a pegar apenas a listagem de status dos meus nodes que estão com Ready = True:

1
2
3
4
5
[root@kalib1 ~]# kubectl get nodes -o jsonpath='{range .items[*]}{@.metadata.name}:{range @.status.conditions[*]}{@.type}={@.status};{end}{end}'| tr ';' "\n" | grep "Ready=True"

Ready=True
Ready=True
Ready=True

A sua criatividade é o limite. ;]

Não temos nenhum pod configurado, mas também poderíamos utilizar kubectl get para conseguir a listagem de nossos pods:

1
2
3
[root@kalib1 ~]# kubectl get pods

No resources found.

Criando pods

Assim como com o Docker, Ansible e algumas outras ferramentas, utilizaremos a linguagem YAML para criar nossos arquivos de configuração.

Criaremos um diretório chamado Builds em nosso Master Controller apenas para melhor organizar nossos arquivos de configuração e ficar mais fácil encontrá-los no futuro:

1
2
3
# mkdir Builds

# cd Builds

Para criarmos Pods, o que fazemos na verdade é criar arquivos de configuração que vão dizer ao Kubernetes qual o estado em que desejamos nossa infraestrutura. O papel do Kubernetes é ler esta configuração e assegurar que o estado de nossa infraestrutura reflita o estado desejado.

Para facilitar, vamos utilizar exemplos encontrados na própria documentação do Kubernetes. Comecemos com a criação de um Pod para um servidor web Nginx.

Vamos criar um arquivo chamado nginx.yaml dentro do diretório Builds que criamos anteriormente:

1
# vim nginx.yaml

No arquivo indicaremos alguns atributos ou variáveis, bem como seus respectivos valores:

  • apiVersion – Indica a versão da API do kubernetes utilizada
  • kind – o tipo de recurso que desejamos
  • metadata – dados referentes ao recurso desejado
  • spec – especificações sobre o que este recurso irá conter

Vamos criar um Pod contendo um único container rodando a versão 1.7.9 do nginx bem como disponibilizando a porta 80 para receber conexões. Este deverá ser o conteúdo do arquivo nginx.yaml:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
    - name: nginx
      image: nginx:1.7.9
      ports:
      - containerPort: 80

Antes de executarmos, vamos nos certificar novamente de duas coisas:

  • Que realmente não temos nenhum Pod criado e ativo;
  • Que não temos nenhum container rodando em nossos nodes.

No centos-master:

1
2
3
[root@kalib1 Builds]# kubectl get pods

No resources found.

No centos-minion1: (Execute o mesmo comando nos demais nodes (centos-minion2 e centos-minion3))

1
2
3
[root@kalib2 ~]# docker ps

CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

Novamente: Se você não faz ideia do que acabei de digitar, (docker ps) volte e leia um pouco sobre Docker antes de seguir com este artigo.

Como podemos ver, não temos nenhum Pod, bem como nenhum container rodando em nossos nodes.

Vamos utilizar kubectl create para criar o Pod utilizando o arquivo que criamos nginx.yaml: Executaremos este comando no Master Controller – centos-master

1
2
3
[root@kalib1 Builds]# kubectl create -f nginx.yaml

pod "nginx" created

O Kubernetes está dizendo que nosso Pod “nginx” foi criado. Vamos verificar:

1
2
3
4
[root@kalib1 Builds]# kubectl get pods

NAME      READY     STATUS    RESTARTS   AGE
nginx     1/1       Running   0          1m

O pod está criado e rodando. Agora, execute novamente docker ps nos 3 nodes para identificar em qual deles o container foi criado. Sim, como não especificamos nada, o Kubernetes vai verificar os recursos disponíveis no momento e vai lançar onde ele achar mais adequado.

1
2
3
4
5
[root@kalib4 ~]# docker ps

CONTAINER ID        IMAGE                                      COMMAND                  CREATED             STATUS              PORTS               NAMES
6de8e22e1536        nginx:1.7.9                                "nginx -g 'daemon off"   2 minutes ago       Up 2 minutes                            k8s_nginx.b0df00ef_nginx_default_d4debd3a-594c-11e7-b587-06827a5b32d4_583881e0
ae49b36ae11b        gcr.io/google_containers/pause-amd64:3.0   "/pause"                 2 minutes ago       Up 2 minutes                            k8s_POD.b2390301_nginx_default_d4debd3a-594c-11e7-b587-06827a5b32d4_fb5c834f

Sim, existem dois containers rodando. Um deles é o nosso “nginx”, enquanto que o outro é um container padrão do google chamado “/pause”, o qual será responsável pela manutenção de alguns recursos de nosso cluster.

Podemos novamente pedir a descrição deste pod que acabamos de criar:

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
[root@kalib1 Builds]# kubectl describe pod nginx

Name:           nginx
Namespace:      default
Node:           centos-minion3/172.31.123.22
Start Time:     Sun, 25 Jun 2017 02:20:18 +0000
Labels:         <none>
Status:         Running
IP:             172.17.0.2
Controllers:    <none>
Containers:
  nginx:
    Container ID:               docker://6de8e22e153618271bb6e8095c68070126541331c8acfc3f5d1a654f4b978454
    Image:                      nginx:1.7.9
    Image ID:                   docker-pullable://docker.io/nginx@sha256:e3456c851a152494c3e4ff5fcc26f240206abac0c9d794affb40e0714846c451
    Port:                       80/TCP
    State:                      Running
      Started:                  Sun, 25 Jun 2017 02:20:27 +0000
    Ready:                      True
    Restart Count:              0
    Volume Mounts:              <none>
    Environment Variables:      <none>
Conditions:
  Type          Status
  Initialized   True
  Ready         True
  PodScheduled  True
No volumes.
QoS Class:      BestEffort
Tolerations:    <none>
Events:
  FirstSeen     LastSeen        Count   From                            SubObjectPath           Type            Reason     Message
  ---------     --------        -----   ----                            -------------           --------        ------     -------
  6m            6m              1       {default-scheduler }                                    Normal          Scheduled  Successfully assigned nginx to centos-minion3
  6m            6m              1       {kubelet centos-minion3}        spec.containers{nginx}  Normal          Pulling    pulling image "nginx:1.7.9"
  6m            6m              2       {kubelet centos-minion3}                                Warning         MissingClusterDNS   kubelet does not have ClusterDNS IP configured and cannot create Pod using "ClusterFirst" policy. Falling back to DNSDefault policy.
  6m            6m              1       {kubelet centos-minion3}        spec.containers{nginx}  Normal          Pulled     Successfully pulled image "nginx:1.7.9"
  6m            6m              1       {kubelet centos-minion3}        spec.containers{nginx}  Normal          Created    Created container with docker id 6de8e22e1536; Security:[seccomp=unconfined]
  6m            6m              1       {kubelet centos-minion3}        spec.containers{nginx}  Normal          Started    Started container with docker id 6de8e22e1536

Obviamente que isto é apenas a configuração mais básica que se possa imaginar, sem storage, mapeamentos de portas, redirecionamentos, rotas, etc. A ideia é apenas uma apresentação inicial..o que é o Kubernetes.

Happy Hacking