Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

Olá! Recentemente, surgiram muitas ferramentas incríveis de automação tanto para a construção de imagens Docker quanto para o deploy no Kubernetes. Por isso, decidi experimentar o GitLab, estudar suas funcionalidades e, claro, configurar um pipeline.

A inspiração para este trabalho foi o site kubernetes.io, que é gerado a partir de códigos fontes automaticamente, e para cada pull request enviado, o robô gera automaticamente uma versão de pré-visualização do site com suas alterações e fornece um link para visualização.

Eu tentei construir um processo semelhante do zero, mas totalmente baseado no GitLab CI e ferramentas gratuitas que estou acostumado a usar para fazer deploy de aplicações no Kubernetes. Hoje, finalmente, vou falar sobre eles em detalhes.

No artigo, iremos abordar ferramentas como:
Hugo, qbec, kaniko, git-crypt e GitLab CI com a criação de ambientes dinâmicos.

Conteúdo

  1. Introdução ao Hugo
  2. Preparação do Dockerfile
  3. Introdução ao kaniko
  4. Introdução ao qbec
  5. Testando o GitLab-runner com o Kubernetes-executor
  6. Deploy de Helm-charts com o qbec
  7. Introdução ao git-crypt
  8. Criando a imagem toolbox
  9. Nosso primeiro pipeline e construção de imagens por tags
  10. Automação do deploy
  11. Artefatos e construção ao fazer push no master
  12. Ambientes dinâmicos
  13. Aplicativos de Revisão

1. Introdução ao Hugo

Como exemplo do nosso projeto, vamos tentar criar um site para publicação de documentação, construído em Hugo. Hugo é um gerador de conteúdo estático.

Para aqueles que não estão familiarizados com geradores de conteúdo estático, vou explicar um pouco mais. Diferente dos tradicionais motores de sites com bancos de dados e PHP que, ao receber uma solicitação de um usuário, geram páginas em tempo real, os geradores de conteúdo estático funcionam de maneira um pouco diferente. Eles permitem pegar arquivos de origem, geralmente um conjunto de arquivos em Markdown e templates de temas, e, em seguida, compilá-los em um site totalmente pronto.

Ou seja, ao final você obterá uma estrutura de diretórios e um conjunto de arquivos HTML gerados, que podem ser facilmente carregados em qualquer hospedagem barata para obter um site em funcionamento.

Você pode instalar o Hugo localmente e testá-lo:

Inicializando um novo site:

hugo new site docs.example.org

E também um repositório Git:

cd docs.example.org
git init

Por enquanto, nosso site está completamente limpo e, para que algo apareça nele, primeiro precisamos conectar um tema, que é apenas um conjunto de templates e regras definidas pelas quais nosso site é gerado.

Como tema, vamos usar Learn, que, na minha opinião, é a mais adequada para um site de documentação.

É importante ressaltar que não precisamos manter os arquivos do tema no repositório do nosso projeto; em vez disso, podemos simplesmente conectá-lo usando git submodule:

git submodule add https://github.com/matcornic/hugo-theme-learn themes/learn

Assim, nosso repositório conterá apenas os arquivos diretamente relacionados ao nosso projeto, enquanto o tema conectado permanecerá como um link para um repositório específico e um commit nele, ou seja, sempre poderemos recuperá-lo da fonte original sem temer mudanças incompatíveis.

Vamos ajustar a configuração config.toml:

baseURL = "http://docs.example.org/"
languageCode = "pt-br"
title = "Meu Site de Documentação"
theme = "learn"

Já nesta fase, podemos iniciar:

hugo server

E no endereço http://localhost:1313/ verificar nosso site recém-criado; todas as alterações feitas na pasta são automaticamente atualizadas na página aberta no navegador, muito conveniente!

Vamos tentar criar uma página de título em content/_index.md:

# My docs site

## Welcome to the docs!

You will be very smart :-)

Captura de tela da página recém-criada

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

Para gerar o site, basta executar:

hugo

O conteúdo do diretório public/ será o seu site.
Ah, e vamos adicioná-lo ao .gitignore:

echo /public > .gitignore

Não se esqueça de commitar nossas alterações:

git add .
git commit -m "Novo site criado"

2. Preparação do Dockerfile

É hora de definir a estrutura do nosso repositório. Geralmente, eu uso algo assim:

.
├── deploy
│   ├── app1
│   └── app2
└── dockerfiles
    ├── image1
    └── image2

  • dockerfiles/ — contém diretórios com Dockerfiles e tudo o que é necessário para construir nossas imagens docker.
  • deploy/ — contém diretórios para implantar nossas aplicações no Kubernetes.

Assim, nosso primeiro Dockerfile será criado no caminho dockerfiles/website/Dockerfile

FROM alpine:3.11 as builder
ARG HUGO_VERSION=0.62.0
RUN wget -O- https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_linux-64bit.tar.gz | tar -xz -C /usr/local/bin
ADD . /src
RUN hugo -s /src

FROM alpine:3.11
RUN apk add --no-cache darkhttpd
COPY --from=builder /src/public /var/www
ENTRYPOINT [ "/usr/bin/darkhttpd" ]
CMD [ "/var/www" ]

Como você pode notar, o Dockerfile contém duas FROM, essa funcionalidade é chamada de multi-stage build e permite excluir tudo o que não é necessário da imagem docker final.
Assim, a imagem final conterá apenas darkhttpd (um servidor HTTP leve) e public/ — o conteúdo do nosso site gerado estaticamente.

Não se esqueça de commitar nossas alterações:

git add dockerfiles/website
git commit -m "Adicionar Dockerfile para o site"

3. Introdução ao kaniko

Como construtor de imagens Docker, decidi usar kaniko, pois não requer a presença de um daemon Docker, e a montagem pode ser realizada em qualquer máquina, armazenando o cache diretamente no registry, eliminando assim a necessidade de ter um armazenamento persistente completo.

Para construir a imagem, basta iniciar um container com kaniko executor e passar o contexto atual da construção, o que pode ser feito localmente, através do docker:

docker run -ti --rm 
  -v $PWD:/workspace 
  -v ~/.docker/config.json:/kaniko/.docker/config.json:ro 
  gcr.io/kaniko-project/executor:v0.15.0 
  --cache 
  --dockerfile=dockerfiles/website/Dockerfile 
  --destination=registry.gitlab.com/kvaps/docs.example.org/website:v0.0.1

Onde registry.gitlab.com/kvaps/docs.example.org/website — é o nome da sua imagem Docker, que será automaticamente enviada ao registro Docker após a construção.

Parâmetro —cache permite armazenar camadas no registro Docker, para o exemplo fornecido, elas serão salvas em registry.gitlab.com/kvaps/docs.example.org/website/cache, mas você pode especificar outro caminho usando o parâmetro —cache-repo.

Captura de tela do docker-registry

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

4. Introdução ao qbec

Qbec — é uma ferramenta de deploy que permite descrever de forma declarativa os manifestos do seu aplicativo e implantá-los no Kubernetes. O uso do Jsonnet como sintaxe principal simplifica significativamente a descrição das diferenças para vários ambientes, além de quase eliminar a repetição de código.

Isso pode ser especialmente relevante nos casos em que você precisa implantar um aplicativo em vários clusters com diferentes parâmetros e deseja descrevê-los de forma declarativa no Git.

O Qbec também permite renderizar chart Helm passando os parâmetros necessários e, posteriormente, manipulá-los como manifestos comuns, inclusive podendo aplicar diversas mutações, o que, por sua vez, elimina a necessidade de usar o ChartMuseum. Ou seja, é possível armazenar e renderizar charts diretamente do git, que é onde eles realmente pertencem.

Como mencionado anteriormente, todos os deployments serão armazenados no diretório deploy/:

mkdir deploy
cd deploy

Vamos inicializar nosso primeiro aplicativo:

qbec init website
cd website

Atualmente, a estrutura do nosso aplicativo é assim:

.
├── components
├── environments
│   ├── base.libsonnet
│   └── default.libsonnet
├── params.libsonnet
└── qbec.yaml

Vamos dar uma olhada no arquivo qbec.yaml:

apiVersion: qbec.io/v1alpha1
kind: App
metadata:
  name: website
spec:
  environments:
    default:
      defaultNamespace: docs
      server: https://kubernetes.example.org:8443
  vars: {}

Aqui, estamos interessados principalmente em spec.environments, o qbec já criou para nós o ambiente default e pegou o endereço do servidor, assim como o namespace do nosso kubeconfig atual.
Agora, ao fazer o deploy em padrão ambiente, o qbec sempre fará o deploy apenas no cluster Kubernetes especificado e no namespace indicado, ou seja, você não precisará mais alternar entre contextos e namespaces para realizar o deploy.
Caso necessário, você sempre pode atualizar as configurações neste arquivo.

Todos os seus ambientes são descritos em qbec.yaml, e no arquivo params.libsonnet, onde está indicado de onde obter os parâmetros para eles.

Em seguida, vemos duas diretórios:

  • components/ — aqui estarão armazenados todos os manifestos para nossa aplicação, podendo ser descritos tanto em jsonnet quanto em arquivos yaml comuns.
  • environments/ — aqui descreveremos todas as variáveis (parâmetros) para nossos ambientes.

Por padrão, temos dois arquivos:

  • environments/base.libsonnet — ele conterá parâmetros comuns para todos os ambientes.
  • environments/default.libsonnet — contém parâmetros sobrepostos para o ambiente. padrão

Vamos abrir environments/base.libsonnet e adicionar os parâmetros para o nosso primeiro componente:

{
  components: {
    website: {
      name: 'example-docs',
      image: 'registry.gitlab.com/kvaps/docs.example.org/website:v0.0.1',
      replicas: 1,
      containerPort: 80,
      servicePort: 80,
      nodeSelector: {},
      tolerations: [],
      ingressClass: 'nginx',
      domain: 'docs.example.org',
    },
  },
}

Vamos também criar nosso primeiro componente components/website.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.website;

[
  {
    apiVersion: 'apps/v1',
    kind: 'Deployment',
    metadata: {
      labels: { app: params.name },
      name: params.name,
    },
    spec: {
      replicas: params.replicas,
      selector: {
        matchLabels: {
          app: params.name,
        },
      },
      template: {
        metadata: {
          labels: { app: params.name },
        },
        spec: {
          containers: [
            {
              name: 'darkhttpd',
              image: params.image,
              ports: [
                {
                  containerPort: params.containerPort,
                },
              ],
            },
          ],
          nodeSelector: params.nodeSelector,
          tolerations: params.tolerations,
          imagePullSecrets: [{ name: 'regsecret' }],
        },
      },
    },
  },
  {
    apiVersion: 'v1',
    kind: 'Service',
    metadata: {
      labels: { app: params.name },
      name: params.name,
    },
    spec: {
      selector: {
        app: params.name,
      },
      ports: [
        {
          port: params.servicePort,
          targetPort: params.containerPort,
        },
      ],
    },
  },
  {
    apiVersion: 'extensions/v1beta1',
    kind: 'Ingress',
    metadata: {
      annotations: {
        'kubernetes.io/ingress.class': params.ingressClass,
      },
      labels: { app: params.name },
      name: params.name,
    },
    spec: {
      rules: [
        {
          host: params.domain,
          http: {
            paths: [
              {
                backend: {
                  serviceName: params.name,
                  servicePort: params.servicePort,
                },
              },
            ],
          },
        },
      ],
    },
  },
]

Neste arquivo, descrevemos três entidades Kubernetes de uma vez: Deployment, Service e Ingress. Se quiséssemos, poderíamos separá-las em componentes diferentes, mas neste estágio, uma única é suficiente.

Sintaxe jsonnet é muito semelhante ao json comum, na verdade, json comum já é um jsonnet válido, então, inicialmente, pode ser mais fácil utilizar serviços online como yaml2json para converter seu yaml familiar em json, ou, se seus componentes não contiverem variáveis, podem ser descritos na forma de yaml normal.

Ao trabalhar com jsonnet eu recomendo fortemente que você instale um plugin para seu editor.

Por exemplo, para vim, existe o plugin vim-jsonnet, que inclui destaque de sintaxe e executa automaticamente jsonnet fmt a cada salvamento (requer que o jsonnet esteja instalado).

Tudo pronto, agora podemos começar o deploy:

Para ver o que conseguimos, vamos executar:

qbec show default

Na saída, você verá os manifests yaml renderizados, que serão aplicados no cluster default.

Ótimo, agora vamos aplicar:

qbec apply default

Na saída, você sempre verá o que será feito em seu cluster, qbec pedirá que você confirme as alterações, digitando y você poderá confirmar suas intenções.

Pronto, agora nosso aplicativo está implantado!

Se houver alterações, você sempre poderá executar:

qbec diff default

para ver como essas mudanças afetarão o deploy atual

Não se esqueça de commitar nossas alterações:

cd ..\/..
git add deploy\/website
git commit -m "Adicionar deploy para o website"

5. Testando Gitlab-runner com Kubernetes-executor

Até recentemente, eu usava apenas o gitlab-runner em uma máquina pré-configurada (container LXC) com executor shell ou docker. Inicialmente, tínhamos vários desses runners globalmente definidos em nosso GitLab. Eles construíam imagens docker para todos os projetos.

Mas como a prática mostrou — essa opção não é a mais ideal, tanto em termos de praticidade quanto de segurança. É muito melhor e ideologicamente correto ter runners separados deployados para cada projeto, ou até mesmo para cada ambiente.

Felizmente, isso não é um problema, pois agora estaremos deployando gitlab-runner diretamente como parte do nosso projeto no Kubernetes.

O Gitlab fornece um helm-chart pronto para o deploy do gitlab-runner no Kubernetes. Assim, tudo que você precisa é saber registration token para nosso projeto em Settings —> CI \/ CD —> Runners e transmiti-lo para o helm:

helm repo add gitlab https:\/\/charts.gitlab.io

helm install gitlab-runner 
  --set gitlabUrl=https:\/\/gitlab.com 
  --set runnerRegistrationToken=yga8y-jdCusVDn_t4Wxc 
  --set rbac.create=true 
  gitlab\/gitlab-runner

Onde:

  • https://gitlab.com — o endereço do seu servidor Gitlab.
  • yga8y-jdCusVDn_t4Wxc — registration token para o seu projeto.
  • rbac.create=true — oferece ao runner a quantidade necessária de privilégios para ser capaz de criar pods para executar nossas tarefas com o kubernetes-executor.

Se tudo foi feito corretamente, você deve ver o runner registrado na seção Runners, nas configurações do seu projeto.

Captura de tela do runner adicionado

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

É tão simples assim? — sim, é tão simples! Chega de incômodos com o registro manual de runners, a partir de agora os runners serão criados e destruídos automaticamente.

6. Deploy de Helm-charts com QBEC

Como decidimos tratar gitlab-runner como parte do nosso projeto, é hora de descrevê-lo em nosso repositório Git.

Poderíamos descrevê-lo como um componente separado website, mas no futuro planejamos deployar diferentes cópias website com bastante frequência, ao contrário gitlab-runner, que será deployado apenas uma vez por cluster Kubernetes. Então vamos inicializar um aplicativo separado para ele:

cd deploy
qbec init gitlab-runner
cd gitlab-runner

Desta vez, não vamos descrever as entidades do Kubernetes manualmente, mas usaremos um gráfico Helm pronto. Uma das vantagens do qbec é a capacidade de renderizar gráficos Helm diretamente de um repositório Git.

Vamos conectá-lo usando git submodule:

git submodule add https://gitlab.com/gitlab-org/charts/gitlab-runner vendor/gitlab-runner

Agora o diretório vendor/gitlab-runner contém o repositório com o gráfico para gitlab-runner.

Da mesma forma, outros repositórios podem ser conectados, por exemplo, todo o repositório com os gráficos oficiais https://github.com/helm/charts

Vamos descrever o componente components/gitlab-runner.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.gitlabRunner;

std.native('expandHelmTemplate')(
  '../vendor/gitlab-runner',
  params.values,
  {
    nameTemplate: params.name,
    namespace: env.namespace,
    thisFile: std.thisFile,
    verbose: true,
  }
)

O primeiro argumento para expandHelmTemplate é o caminho para o gráfico, depois params.values, que pegaremos dos parâmetros de ambiente, em seguida temos um objeto com

  • nameTemplate — o nome do release
  • namespace — o namespace passado para o Helm
  • thisFile — um parâmetro obrigatório que passa o caminho do arquivo atual
  • verbose — exibe o comando helm template com todos os argumentos ao renderizar o gráfico

Agora vamos descrever os parâmetros para nosso componente em environments/base.libsonnet:

local secrets = import '../secrets/base.libsonnet';

{
  components: {
    gitlabRunner: {
      name: 'gitlab-runner',
      values: {
        gitlabUrl: 'https://gitlab.com/',
        rbac: {
          create: true,
        },
        runnerRegistrationToken: secrets.runnerRegistrationToken,
      },
    },
  },
}

Note que runnerRegistrationToken pegamos de um arquivo externo secrets/base.libsonnet, vamos criá-lo:

{
  runnerRegistrationToken: 'yga8y-jdCusVDn_t4Wxc',
}

Vamos testar se tudo está funcionando:

qbec show default

se tudo estiver certo, podemos remover nosso release anteriormente implantado via Helm:

helm uninstall gitlab-runner

e implantá-lo novamente, mas agora via qbec:

qbec apply default

7. Introdução ao git-crypt

Git-crypt é uma ferramenta que permite configurar criptografia transparente para seu repositório.

Atualmente, a estrutura do nosso diretório para gitlab-runner está assim:

.
├── components
│   ├── gitlab-runner.jsonnet
├── environments
│   ├── base.libsonnet
│   └── default.libsonnet
├── params.libsonnet
├── qbec.yaml
├── secrets
│   └── base.libsonnet
└── vendor
    └── gitlab-runner (submódulo)

Mas armazenar segredos no Git não é seguro, não é mesmo? Então precisamos criptografá-los adequadamente.

Normalmente, para uma única variável, isso nem sempre faz sentido. Você pode passar segredos em qbec e através de variáveis de ambiente do seu sistema CI.
Mas é importante notar que existem projetos mais complexos, que podem conter muitos segredos; transferi-los todos através de variáveis de ambiente seria extremamente difícil.

Além disso, nesse caso, eu não conseguiria falar sobre uma ferramenta incrível como git-crypt.

git-crypt que é conveniente porque permite manter todo o histórico de segredos, além de comparar, mesclar e resolver conflitos da mesma forma que estamos acostumados a fazer no caso do Git.

Primeiramente, após a instalação git-crypt , precisamos gerar chaves para nosso repositório:

git crypt init

Se você possui uma chave PGP, pode adicionar-se imediatamente como colaborador deste projeto:

git-crypt add-gpg-user kvapss@gmail.com

Dessa forma, você sempre poderá descriptografar este repositório usando sua chave privada.

Se você não tiver uma chave PGP e não for obter uma, pode seguir por outro caminho e exportar a chave do projeto:

git crypt export-key /path/to/keyfile

Assim, qualquer pessoa que possua o keyfile poderá descriptografar seu repositório.

É hora de configurar nosso primeiro segredo.
Lembre-se, ainda estamos na diretório deploy/gitlab-runner/, onde temos um diretório secrets/, vamos criptografar todos os arquivos nele, para isso, vamos criar um arquivo secrets/.gitattributes com este conteúdo:

* filter=git-crypt diff=git-crypt
.gitattributes !filter !diff

Como pode ser visto no conteúdo, todos os arquivos com a máscara * serão processados através de git-crypt, exceto o próprio .gitattributes

Podemos verificar isso executando:

git crypt status -e

Como resultado, obteremos uma lista de todos os arquivos no repositório para os quais a criptografia está ativada.

Isso é tudo; agora podemos com confiança adicionar nossas mudanças:

cd ../..
git add .
git commit -m "Add deploy for gitlab-runner"

Para bloquear o repositório, basta executar:

git crypt lock

e imediatamente todos os arquivos criptografados se transformarão em algo binário, tornando impossível sua leitura.
Para descriptografar o repositório, execute:

git crypt unlock

8. Criando a imagem toolbox

A imagem toolbox é uma imagem com todas as ferramentas que usaremos para o deploy do nosso projeto. Ela será utilizada pelo gitlab-runner para executar tarefas típicas de deploy.

Aqui é simples, crie um novo dockerfiles/toolbox/Dockerfile com este conteúdo:

FROM alpine:3.11

RUN apk add --no-cache git git-crypt

RUN QBEC_VER=0.10.3 
 && wget -O- https://github.com/splunk/qbec/releases/download/v${QBEC_VER}/qbec-linux-amd64.tar.gz 
     | tar -C /tmp -xzf - 
 && mv /tmp/qbec /tmp/jsonnet-qbec /usr/local/bin/

RUN KUBECTL_VER=1.17.0 
 && wget -O /usr/local/bin/kubectl 
      https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VER}/bin/linux/amd64/kubectl 
 && chmod +x /usr/local/bin/kubectl

RUN HELM_VER=3.0.2 
 && wget -O- https://get.helm.sh/helm-v${HELM_VER}-linux-amd64.tar.gz 
     | tar -C /tmp -zxf - 
 && mv /tmp/linux-amd64/helm /usr/local/bin/helm

Como você pode notar, nesta imagem estamos instalando todas as utilidades que usamos para implantar nosso aplicativo. Aqui não precisamos de kubectl, mas talvez você queira brincar com ele na fase de configuração do pipeline.

Além disso, para podermos interagir com o Kubernetes e fazer a implantação nele, precisamos configurar uma função para os pods gerados pelo gitlab-runner.

Para isso, vamos para o diretório do gitlab-runner:

cd deploy/gitlab-runner

e vamos adicionar um novo componente components/rbac.jsonnet:

local env = {
  name: std.extVar('qbec.io/env'),
  namespace: std.extVar('qbec.io/defaultNs'),
};
local p = import '../params.libsonnet';
local params = p.components.rbac;

[
  {
    apiVersion: 'v1',
    kind: 'ServiceAccount',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
  },
  {
    apiVersion: 'rbac.authorization.k8s.io/v1',
    kind: 'Role',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
    rules: [
      {
        apiGroups: [
          '*',
        ],
        resources: [
          '*',
        ],
        verbs: [
          '*',
        ],
      },
    ],
  },
  {
    apiVersion: 'rbac.authorization.k8s.io/v1',
    kind: 'RoleBinding',
    metadata: {
      labels: {
        app: params.name,
      },
      name: params.name,
    },
    roleRef: {
      apiGroup: 'rbac.authorization.k8s.io',
      kind: 'Role',
      name: params.name,
    },
    subjects: [
      {
        kind: 'ServiceAccount',
        name: params.name,
        namespace: env.namespace,
      },
    ],
  },
]

Vamos também descrever novos parâmetros em environments/base.libsonnet, que agora se parece com isso:

local secrets = import '../secrets/base.libsonnet';

{
  components: {
    gitlabRunner: {
      name: 'gitlab-runner',
      values: {
        gitlabUrl: 'https://gitlab.com/',
        rbac: {
          create: true,
        },
        runnerRegistrationToken: secrets.runnerRegistrationToken,
        runners: {
          serviceAccountName: $.components.rbac.name,
          image: 'registry.gitlab.com/kvaps/docs.example.org/toolbox:v0.0.1',
        },
      },
    },
    rbac: {
      name: 'gitlab-runner-deploy',
    },
  },
}

Note que $.components.rbac.name se refere a nome para o componente rbac

Vamos verificar o que mudou:

qbec diff default

e aplicar nossas mudanças no Kubernetes:

qbec apply default

E não se esqueça de fazer commit das suas alterações no git:

cd ../../
git add dockerfiles/toolbox
git commit -m "Add Dockerfile for toolbox"
git add deploy/gitlab-runner
git commit -m "Configure gitlab-runner to use toolbox"

9. Nosso primeiro pipeline e construção de imagens por tags

Na raiz do projeto, criaremos .gitlab-ci.yml com este conteúdo:

.build_docker_image:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:debug-v0.15.0
    entrypoint: [""]
  before_script:
    - echo "{"auths":{"$CI_REGISTRY":{"username":"$CI_REGISTRY_USER","password":"$CI_REGISTRY_PASSWORD"}}}" > /kaniko/.docker/config.json

build_toolbox:
  extends: .build_docker_image
  script:
    - /kaniko/executor --cache --context $CI_PROJECT_DIR/dockerfiles/toolbox --dockerfile $CI_PROJECT_DIR/dockerfiles/toolbox/Dockerfile --destination $CI_REGISTRY_IMAGE/toolbox:$CI_COMMIT_TAG
  only:
    refs:
      - tags

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_TAG
  only:
    refs:
      - tags

Observe que estamos usando GIT_SUBMODULE_STRATEGY: normal para aquelas jobs onde é necessário inicializar explicitamente os submódulos antes da execução.

Não se esqueça de commitar nossas alterações:

git add .gitlab-ci.yml
git commit -m "Automatizar construção do docker"

Acho que podemos chamar isso de versão v0.0.1 e adicionar a tag:

git tag v0.0.1

Adicionaremos tags sempre que precisarmos lançar uma nova versão. As tags nas imagens Docker estarão vinculadas às tags do Git. Cada push com uma nova tag iniciará a construção das imagens com essa tag.

Vamos executar git push —tags, e dar uma olhada em nosso primeiro pipeline:

Captura de tela do primeiro pipeline

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

Vale a pena notar que a construção utilizando tags é adequada para construir imagens Docker, mas não é adequada para implantar aplicativos no Kubernetes. Como novas tags podem ser atribuídas a commits antigos, nesse caso a inicialização do pipeline para eles resultará na implantação de uma versão antiga.

Para resolver esse problema, normalmente se vincula a construção das imagens Docker às tags, enquanto a implantação do aplicativo se vincula à branch master, onde as versões das imagens construídas estão hardcoded. Somente nesse caso você poderá iniciar o rollback simplesmente revertendo master-a branch.

10. Automação da implantação

Para que o Gitlab-runner possa decifrar nossos segredos, precisamos exportar a chave do repositório e adicioná-la às variáveis de ambiente do nosso CI:

git crypt export-key /tmp/docs-repo.key
base64 -w0 /tmp/docs-repo.key; echo

a string obtida deve ser armazenada no Gitlab, para isso vamos para as configurações do nosso projeto:
Settings —> CI / CD —> Variables

E vamos criar uma nova variável:

Tipo
Chave
Valor
Protegido
Mascarado
Escopo

Novo projeto
GITCRYPT_KEY
<your string>
verdadeiro (durante o período de aprendizado, você pode usar) falso)
verdadeiro
Todos os ambientes

Captura de tela da variável adicionada

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

Agora vamos atualizar nosso .gitlab-ci.yml adicionando a ele:

.deploy_qbec_app:
  stage: deploy
  only:
    refs:
      - master

deploy_gitlab_runner:
  extends: .deploy_qbec_app
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  before_script:
    - base64 -d "$GITCRYPT_KEY" | git-crypt unlock -
  script:
    - qbec apply default --root deploy/gitlab-runner --force:k8s-context __incluster__ --wait --yes

deploy_website:
  extends: .deploy_qbec_app
  script:
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes

Neste projeto, utilizamos várias novas opções para o qbec:

  • —root some/app — permite definir o diretório de um aplicativo específico
  • —force:k8s-context __incluster__ — essa é uma variável mágica que indica que o deploy ocorrerá no mesmo cluster onde o gitlab-runner está em execução. Isso é necessário porque, caso contrário, o qbec tentará encontrar um servidor Kubernetes apropriado em seu kubeconfig.
  • —wait — faz com que o qbec aguarde até que os recursos criados estejam no estado Ready e só então finalize com um código de saída bem-sucedido.
  • —yes — simplesmente desativa o shell interativo Você tem certeza? no deploy.

Não se esqueça de commitar nossas alterações:

git add .gitlab-ci.yml
git commit -m "Automatizar deploy"

E depois git push veremos como nossas aplicações foram implantadas:

Captura de tela do segundo pipeline

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

11. Artefatos e construção ao fazer push no master

Geralmente, os passos mencionados acima são suficientes para construir e entregar quase qualquer microserviço, mas não queremos marcar uma tag sempre que precisarmos atualizar o site. Portanto, seguiremos um caminho mais dinâmico e configuraremos o deploy por digest na branch master.

A ideia é simples: agora a imagem do nosso website será recompilada a cada push em master, e após isso, será automaticamente implantada no Kubernetes.

Vamos atualizar esses dois jobs em nosso .gitlab-ci.yml:

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - mkdir -p $CI_PROJECT_DIR/artifacts
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_REF_NAME --digest-file $CI_PROJECT_DIR/artifacts/website.digest
  artifacts:
    paths:
      - artifacts/
  only:
    refs:
      - master
      - tags

deploy_website:
  extends: .deploy_qbec_app
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

Note que adicionamos a branch master para refs para o job build_website e agora estamos usando $CI_COMMIT_REF_NAME em vez de $CI_COMMIT_TAG, ou seja, nos desapegamos das tags no Git e agora vamos empurrar a imagem com o nome da branch do commit que iniciou o pipeline. Vale mencionar que isso também funcionará com as tags, o que nos permitirá manter snapshots do site com uma versão específica no docker-registry.

Quando o nome da tag do docker para a nova versão do site pode ser constante, ainda assim precisamos descrever as mudanças para o Kubernetes; caso contrário, ele simplesmente não fará o redeploy da aplicação a partir da nova imagem, pois não notará nenhuma alteração no manifesto de deployment.

Opção —vm:ext-str digest="$DIGEST" para qbec — permite passar uma variável externa para o jsonnet. Queremos que, a cada lançamento de nossa aplicação, ela seja redeployada no cluster. Não podemos mais usar um nome de tag que agora pode ser constante, pois precisamos amarrar a uma versão específica da imagem e disparar o deployment quando ela for alterada.

Aqui, a capacidade do Kaniko de salvar o digest da imagem em um arquivo (opção —digest-file)
Depois, passaremos esse arquivo e o leremos no momento do deploy.

Vamos atualizar os parâmetros para o nosso deploy/website/environments/base.libsonnet que agora ficará assim:

{
  components: {
    website: {
      name: 'example-docs',
      image: 'registry.gitlab.com/kvaps/docs.example.org/website@' + std.extVar('digest'),
      replicas: 1,
      containerPort: 80,
      servicePort: 80,
      nodeSelector: {},
      tolerations: [],
      ingressClass: 'nginx',
      domain: 'docs.example.org',
    },
  },
}

Pronto, agora qualquer commit em master inicializa a construção da imagem docker para website, e em seguida seu deploy no Kubernetes.

Não se esqueça de commitar nossas alterações:

git add .
git commit -m "Configure dynamic build"

Vamos verificar, após git push devemos ver algo assim:

Captura de tela do pipeline para master

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

Basicamente, não precisamos realizar o redeploy do gitlab-runner a cada push, a menos que, claro, nada tenha mudado em sua configuração; vamos corrigir isso em .gitlab-ci.yml:

deploy_gitlab_runner:
  extends: .deploy_qbec_app
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  before_script:
    - base64 -d "$GITCRYPT_KEY" | git-crypt unlock -
  script:
    - qbec apply default --root deploy/gitlab-runner --force:k8s-context __incluster__ --wait --yes
  only:
    changes:
      - deploy/gitlab-runner/**/*

mudanças permite monitorar alterações em deploy/gitlab-runner/ e acionará nossa job apenas na presença delas.

Não se esqueça de commitar nossas alterações:

git add .gitlab-ci.yml
git commit -m "Reduzir deploy do gitlab-runner"

git push, assim é melhor:

Captura de tela do pipeline atualizado

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

12. Ambientes dinâmicos

É hora de diversificar nosso pipeline com ambientes dinâmicos.

Para começar, vamos atualizar a job build_website no nosso .gitlab-ci.yml, removendo o bloco only, o que fará com que o Gitlab a acione em qualquer commit em qualquer branch:

build_website:
  extends: .build_docker_image
  variables:
    GIT_SUBMODULE_STRATEGY: normal
  script:
    - mkdir -p $CI_PROJECT_DIR/artifacts
    - /kaniko/executor --cache --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dockerfiles/website/Dockerfile --destination $CI_REGISTRY_IMAGE/website:$CI_COMMIT_REF_NAME --digest-file $CI_PROJECT_DIR/artifacts/website.digest
  artifacts:
    paths:
      - artifacts/

Em seguida, vamos atualizar a job deploy_website, vamos adicionar um bloco lá environment:

deploy_website:
  extends: .deploy_qbec_app
  environment:
    name: prod
    url: https://docs.example.org
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

Isso permitirá que o Gitlab associe o job com prod o ambiente e exiba o link correto para ele.

Agora adicionaremos mais duas jobs:

deploy_website:
  extends: .deploy_qbec_app
  environment:
    name: prod
    url: https://docs.example.org
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply default --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST"

deploy_review:
  extends: .deploy_qbec_app
  environment:
    name: review/$CI_COMMIT_REF_NAME
    url: http://$CI_ENVIRONMENT_SLUG.docs.example.org
    on_stop: stop_review
  script:
    - DIGEST="$(cat artifacts/website.digest)"
    - qbec apply review --root deploy/website --force:k8s-context __incluster__ --wait --yes --vm:ext-str digest="$DIGEST" --vm:ext-str subdomain="$CI_ENVIRONMENT_SLUG" --app-tag "$CI_ENVIRONMENT_SLUG"
  only:
    refs:
    - branches
  except:
    refs:
      - master

stop_review:
  extends: .deploy_qbec_app
  environment:
    name: review/$CI_COMMIT_REF_NAME
    action: stop
  stage: deploy
  before_script:
    - git clone "$CI_REPOSITORY_URL" master
    - cd master
  script:
    - qbec delete review --root deploy/website --force:k8s-context __incluster__ --yes --vm:ext-str digest="$DIGEST" --vm:ext-str subdomain="$CI_ENVIRONMENT_SLUG" --app-tag "$CI_ENVIRONMENT_SLUG"
  variables:
    GIT_STRATEGY: none
  only:
    refs:
    - branches
  except:
    refs:
      - master
  when: manual

Elas serão acionadas ao fazer push em quaisquer branches, exceto no master, e implantarão uma versão de pré-visualização do site.

Vemos uma nova opção para qbec: —app-tag — ela permite rotular as versões implantadas do aplicativo e operar apenas dentro desse rótulo; ao criar e destruir recursos no Kubernetes, o qbec operará apenas com eles.
Assim, não precisamos criar um ambiente separado para cada review, mas simplesmente reutilizar o mesmo.

Aqui também usamos qbec apply review, ao invés de qbec apply default — é exatamente neste momento que tentaremos descrever as diferenças para nossos ambientes (review e default):

Vamos adicionar o ambiente de review em deploy/website/qbec.yaml spec: environments: review: defaultNamespace: docs server: https://kubernetes.example.org:8443

Em seguida, vamos declará-lo em

deploy/website/params.libsonnet local env = std.extVar('qbec.io/env'); local paramsMap = { _: import './environments/base.libsonnet', default: import './environments/default.libsonnet', review: import './environments/review.libsonnet', };if std.objectHas(paramsMap, env) then paramsMap[env] else error 'environment ' + env + ' not defined in ' + std.thisFile:

E vamos gravar os parâmetros personalizados para ele em

deploy/website/environments/review.libsonnet Vamos também dar uma olhada mais detalhada no job:

// this file has the param overrides for the default environment
local base = import './base.libsonnet';
local slug = std.extVar('qbec.io/tag');
local subdomain = std.extVar('subdomain');

base {
  components+: {
    website+: {
      name: 'example-docs-' + slug,
      domain: subdomain + '.docs.example.org',
    },
  },
}

stop_review , ele será acionado ao excluir um branch e para que o gitlab não tente fazer checkout nele, usamosGIT_STRATEGY: none , mais tarde clonaremos, depois nós clonamos master-ramo e removemos a revisão através dele.
Um pouco complicado, mas ainda não encontrei uma maneira mais bonita.
Uma alternativa pode ser implantar cada revisão em um namespace separado, que pode ser totalmente removido a qualquer momento.

Não se esqueça de commitar nossas alterações:

git add .
git commit -m "Ativar revisão automática"

git push, git checkout -b teste, git push origin teste, verificamos:

Captura de tela dos ambientes criados no Gitlab

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

Está tudo funcionando? — ótimo, vamos remover nosso ramo de teste: git checkout master, git push origin :teste, verificamos se os trabalhos de exclusão de ambiente foram executados sem erros.

Aqui é importante observar que qualquer desenvolvedor no projeto pode criar ramos, ele também pode modificar .gitlab-ci.yml arquivos e acessar variáveis secretas.
Por isso, é fortemente recomendado permitir seu uso apenas em ramos protegidos, por exemplo, em master, ou criar um conjunto separado de variáveis para cada ambiente.

13. Aplicativos de Revisão

Aplicativos de Revisão é uma funcionalidade do Gitlab que permite adicionar um botão para visualização rápida em um ambiente implantado para cada arquivo no repositório.

Para que esses botões apareçam, é necessário criar um arquivo .gitlab/route-map.yml e descrever nele todas as transformações de caminhos, no nosso caso, será muito simples:

# Indices
- source: /content/(.+?)_index.(md|html)/ 
  public: '1'

# Pages
- source: /content/(.+?).(md|html)/ 
  public: '1/'

Não se esqueça de commitar nossas alterações:

git add .gitlab/
git commit -m "Ativar aplicativos de revisão"

git push, e verificamos:

Captura de tela do botão de Aplicativo de Revisão

Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

Tarefa concluída!

Fontes do projeto:

Obrigado pela atenção, espero que tenham gostado Experimentando novas ferramentas para construção e automação de deploy no Kubernetes

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster