DevSecOps

14 out, 2020

GitLab CI — Seu próprio runner privado com docker – compose

Publicidade

Vamos lá nessa jornada do conhecimento contínuo para que possamos entregar cada vez mais e melhor. Dispor de um runner privado para ser utilizado em conjunto com o GitLab CI, publico ou privado que bem pensado será possível o melhor resultados dos dois mundos. O texto abaixo conta os meus passos até o final.

No primeiro momento foi somente entender e fazer funcionar uma pipeline construída utilizando o conceito: docker in docker, foi utilizado o próprio assistente do gitlab-ci, conforme apresentação abaixo:

criar arquivo: .gitlab-ci
criar arquivo: .gitlab-ci# This file is a template, and might need editing before it works on your project.
docker-build-master:
# Official docker image.
image: docker:latest
stage: build
services:
- docker:dind
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- docker build --pull -t "$CI_REGISTRY_IMAGE" .
- docker push "$CI_REGISTRY_IMAGE"
only:
- master

docker-build:
# Official docker image.
image: docker:latest
stage: build
services:
– docker:dind
before_script:
– docker login -u “$CI_REGISTRY_USER” -p “$CI_REGISTRY_PASSWORD” $CI_REGISTRY
script:
– docker build –pull -t “$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG” .
– docker push “$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG”
except:
– master

Ai ficou fácil, foi só preencher as variáveis em CI / CD Settings, segue abaixo:

ci/cd variables

Com pequenos ajustes concluído o processo de build docker utilizando um runner público. A partir desse ponto que percebemos que o objetivo tem uma distância maior do cenário real.

CENÁRIO

No cenário real se apresentou distante da utilização como serviço em cloud, A realidade é diferente não esta sendo utilizado o gitlab.com ou runner público, e sim o gitlab ceon premises e provavelmente bare metal, mas não tenho essa visão. Além de apresentar os desafios iniciais ainda existem as camadas de infra-estrutura tradicional, como network (routes, acl, QoS) e segurança (liberação de regras firewall), que não precisaremos conhecer todos os detalhes para entregar nosso objetivo.

Mesmo assim ainda precisamos ligar o ponto ao ponto B, então precisamos construir uma ponte. Quando menciono pontes, gosto dessa imagem como exemplo, não sei se é real. Mas o fato que não desejamos esse cenário.

Falha de construção de ponte

Por esse motivo vamos remover a problemática do desconhecido e realizar todas as conexões com um runner privado que funcione com o conceito docker in docker para que possamos construir as imagens dentro do próprio CI-CD e assim permitir a reutilização e principalmente portabilidade.

A abordagem com docker traz a vantagem de isolar totalmente os ambientes e o contêiner é a melhor forma de agrupar todas as dependências do projeto, incluindo as bibliotecas que se comunicam com a versão do sistema operacional. Tudo bem casado.

PROPOSTA

A ideia era simples, até demais começar a entender melhor e percebendo que não era tão simples assim, queria me dedicar somente algumas horas, mas não foi bem isso que aconteceu. Já que se sentiu desafiado, vamos para cima. Eu chamo isso de melhor momento.

Todas as informações obtive com a ajuda de colegas e o amigo que esta sempre ao alcance o Google, que me levou a muitos links, então o resultado disso é um compilado de diversas fontes de informação os mais relevantes deixo no final como referências. Nada de novo, mas sim organizado para uma melhor compreensão.

Para funcionar bem teria que ser portável e na minha opinião a melhor ferramenta para prototipagem de ambientes, principalmente quando você não nenhum ambiente é mesmo o **docker-compose**, construir todos os contêineres com todas o apontamentos necessários entre eles e com um único comando, _sobe_ tudo de uma vez e _desce_ tudo de uma vez. Não tem preço.

Dependências

Técniquês solto daqui em diante, então chegou a hora de arregaçar as mangas.

Aqui no meu note utilizo as seguintes versões de ferramental:

  • Sistema Operacional: Ubuntu 18.04.4 LTS
  • Kernel: Linux 5.3.0–40-generic
  • Docker version: 19.03.6, build 369ce74a3c
  • docker-compose: version 1.21.2, build a133471

Antes de inciarmos, kernel superior ao 4.x que já possui o suporte ao módulo overlay2driver . Assim iremos configurar o modo daemon do Docker para utiliza-lo. Para conferir se possui o módulo carregado, comando abaixo:

$ lsmod | grep overlay

Image for post
https://asciinema.org/a/308453

Segue a dica: No decorrer das próximas informações, aparecerá essa apresentação no formato gif, que demonstra o que foi feito e como foi feito, e logo abaixo o endereço que levará registro de tela/log original, que inclusive permitirá a cópia dos comandos, experimente. Acredito que ficará ainda mais claro para todas as pessoas.

Caso não obtiver o retorno esperado, poderá habilitar desta forma com sudo ou root. Essa parte eu não precisei realizar.

# modprobe overlay

Se sua resposta for positiva conforme apresentação, vamos seguir.

Em seguida, você precisará configurar o Docker para realmente utilizar o overlaymódulo. Versões recentes do Docker vêm com o overlay2driver de volume, mais rápido que o overlaydriver original . Certifique-se de que seu /etc/docker/daemon.jsonarquivo — talvez seja necessário criá-lo — tenha a seguinte aparência:

Arquivo: /etc/docker/daemon.json

{
"storage-driver": "overlay2"
}

Aqui no note não existia esse arquivo, então foi necessário a criação, não cheguei a realizar o teste sem essa parte.

Para criar pode ser dessa forma:

$ echo '{ "storage-driver": "overlay2" }' | sudo tee /etc/docker/daemon.json

Feito isso, reinicie o ambiente docker e leve em consideração para limpar e remover todos os contêiner, iniciados, parados e imagens desnecessárias:

https://asciinema.org/a/308460

https://asciinema.org/a/308460

Apresentação do funcionamento

Foi necessário a criação de 3 contêiner, sendo eles:

  • runner — Contêiner principal do runner, elo de ligação entre este ambiente e o gitlab-ci (a nossa ponte);
  • register-runner — Contêiner que realiza a ficha de inscrição deste ambiente para o ambiente gitlab-ci, tem a missão de montar a estrutura de conexão e dispor o arquivo de configuração: config.toml ao runner;
  • dind — Contêiner que executará as pipelines, o famoso docker dentro do docker, sim ele existe.

Diagrama de funcionamento docker

Diagrama de funcionamento do docker-compose

Caso tenha curiosidade e deseja utilizar essa ferramenta que gera esse tipo de diagrama, executar desta maneira no diretório que contém o docker-compose.yml

$ docker rum --rm -it --name dcv -v ${pwd}:/input pmsipilot/docker-compose-viz render -m image docker-compose.yml

Realizadas as devidas apresentações dos papéis vamos descer um pouco mais.

register-runner

O container register-runner, receberá a informação de token e endpoit do gitlab, em nosso caso agora estamos o utilizando o gitlab.com, as variáveis que devem ser preenchidas são:

- CI_SERVER_URL=https://gitlab.com/
- REGISTRATION_TOKEN=<***SEU-TOKEN***>

Se for sua necessidade também poderá deixar esses valore externo ao docker-compose em um arquivo externo, poderá ter uma conexão com a API do gitlab.com ir até lá, buscar esse valor e inserir neste arquivo, mas é um ponto de melhora, por hora ficou assim.

Onde ficará o arquivo: config.toml, foi criado um ponto de montagem comum entre os contêiner: runner e register-runner, a partir do diretório de referência que esta o arquivo: docker-compose.yml o caminho poder a ser observado no arquivo yaml, é este: ./gitlab/runner, será criado um diretório: gitlab e o subdiretório: runner

tree ./gitlab
./gitlab
└── runner
└── config.toml

1 directory, 1 file

ficha de inscrição

Vamos descer mais um pouquinho, agora vamos no detalhe da inscrição esse é o último nível, e para isso seremos cirúrgicos e inicializar somente o contêiner register-runner, através do docker-compose. É necessário todo o ambiente em pé, ou seja UP para realizar esse passo, que poderá ser útil em um momento de troca ou renovação do runner, ou quem sabe uma outra rotina de automação casada com essa principal, mas fica a ser definido.

Iremos acrescentar outros runner ao já existentes.

Foram adicionados os dois, mas demorou um pouco para reconhecer na interface gráfica, e confirmar o status como: on-line

Abaixo o procedimento de inclusão.

Image for post

https://asciinema.org/a/308503

Iremos realizar a inspeção do arquivo: config-toml referente a ficha de inscrição do runner.

Image for post

https://asciinema.org/a/308504

Podemos observar que a cada novo runner vinculado ao gitlab será concatenado a ficha de inscrição: **config-toml**

Image for post

Runner tag

Já existia um runner, realizamos a inscrição de dois novos e depois subi mais um .

Inspeção de token:

$ sudo grep token ./gitlab/runner/config.toml
token = "2V6D6_xdky8FHpHhp1KY"
token = "yLBZYU-uXzs2dyyMmFsm"
token = "SqWHCCPTsh3x_qjikWh-"
token = "PKy6j9rNXzyftskxsT2g"

Observamos que na interface gráfica por critério de segurança, é exibido os 8 primeiros caracteres. Armazene de forma segura essa chave. Esta irei invalidar até o fim deste tutorial.

As outras informações presente neste arqivo, você poderá conferir direto na documentação oficial, os links estão ai no final.

EXECUÇÃO

Normalmente lançamos para as nuvens no conceito de infra como código, mas nessa POC é o inverso, então vamos lá.

Image for post

DevOps modernos

Antes de subir o ambiente você poderá desativar os runner públicos, é uma opção, dessa forma você terá a certeza que essa atividade será executada em seu ambiente, mas não é obrigatório.

Image for post

Disable shared Runners

Outra opção é relacionar uma TAG, dessa forma:

Image for post

Runner tag

Ficará a critério a organização da melhor forma. Ainda será necessários muitos testes, com ou sem o runner desabilitar, habilitar, mapear todos os cenários a fim de se prevenir.

PROVISÃO DO AMBIENTE

Essa é parte mais simples de todas, mas é necessário entender as entradas e como tudo esta interligado

Abaixo segue o arquivo final, vale lembrar que será necessário realizar o apontamento do seu token referente ao runner, se ainda há duvidas segue o local onde obter:

Image for post

Apontamento manual do Runner — token

O botão abaixo Reset runners registation token, irá liberar este tokem e será fornecido outro aleatóriamente, com esse novo valor será gerado os tokens dos executores.

arquivo: docker-compose-yml na versão: 3

version: '3'

services:

dind:
image: docker:17.06.0-ce-dind
container_name: dind
restart: always
privileged: true
volumes:
– /var/lib/docker
command:
– –storage-driver=overlay2
networks:
– netrunner

runner:
image: gitlab/gitlab-runner:alpine
container_name: runner
restart: always
volumes:
– ./gitlab/runner:/etc/gitlab-runner:Z
– /var/run/docker.sock:/var/run/docker.sock
environment:
– DOCKER_HOST=tcp://dind:2375
links:
– dind
networks:
– netrunner

register-runner:
image: gitlab/gitlab-runner:alpine
container_name: register-runner
restart: ‘no’
volumes:
– ./gitlab/runner:/etc/gitlab-runner:Z
command:
– register
– –non-interactive
– –locked=false
– –name=Docker Runner
– –executor=docker
– –docker-image=docker:17.06.0-ce-dind
– –docker-volumes=/var/run/docker.sock:/var/run/docker.sock
environment:
– CI_SERVER_URL=https://gitlab.com/
– REGISTRATION_TOKEN=<***SEU-TOKEN***>
networks:
– netrunner

networks:
netrunner:
driver: bridge
driver_opts:
foo: “1”

Antes de lançar eu recomendo parar todos os contêiner, e remover os que não estão estão em utilização, será mais simples realizar um toubleshooting caso exista a necessidade.

$ docker-compose up -d

Image for post
https://asciinema.org/a/308532

Se utilizar a tag, deve ser definido também no código: .gitlab-ci.yml com a mesma referência. No exemplo utilizamos: dind.

PIPELINE

A tag foi incluída entre as chaves, services before_script.

services:
- docker:dind
tags:
- dind
before_script:

Para validar nosso caso de uso ficou assim:

docker-build-master:
# Official docker image.
image: docker:17.06.0-ce-dind
stage: build
services:
- docker:dind
tags:
- dind
before_script:
- docker info
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
script:
- docker build --pull -t "$CI_REGISTRY_IMAGE" .
- docker push "$CI_REGISTRY_IMAGE"
only:
- master

LOG

Após execução segue as informações de log.

Conforme o tag no runner e apontamento no código: .gitlab-ci.yml, segue o runner: yLBZYU-u onde referenciamos a TAG

[0KRunning with gitlab-runner 12.8.0 (1b659122)
[0;m[0K on Docker Runner yLBZYU-u
[0;msection_start:1583714149:prepare_executor
[0K[0KUsing Docker executor with image docker:17.06.0-ce-dind ...
[0;m[0KStarting service docker:dind ...
[0;m[0KPulling docker image docker:dind ...
[0;m[0KUsing docker image sha256:14af3ba31e635475ec8f7fbe17470424514777621e627a91c41bbbe028dbae16 for docker:dind ...
[0;m[0KWaiting for services to be up and running...
[0;m
[0;33m*** WARNING:[0;m Service runner-yLBZYU-u-project-17314874-concurrent-0-docker-0 probably didn't start properly.

Health check error:
service “runner-yLBZYU-u-project-17314874-concurrent-0-docker-0-wait-for-service” timeout

Health check container logs:

Service container logs:
2020-03-09T00:36:02.302028232Z Generating RSA private key, 4096 bit long modulus (2 primes)
2020-03-09T00:36:02.397566737Z ………………..++++
2020-03-09T00:36:02.603626076Z …………………………………………………++++
2020-03-09T00:36:02.604081887Z e is 65537 (0x010001)
2020-03-09T00:36:02.616648884Z Generating RSA private key, 4096 bit long modulus (2 primes)
2020-03-09T00:36:02.629926753Z ..++++
2020-03-09T00:36:02.704958517Z ……………….++++
2020-03-09T00:36:02.705344171Z e is 65537 (0x010001)
2020-03-09T00:36:02.731027555Z Signature ok
2020-03-09T00:36:02.731042064Z subject=CN = docker:dind server
2020-03-09T00:36:02.731045054Z Getting CA Private Key
2020-03-09T00:36:02.742042753Z /certs/server/cert.pem: OK
2020-03-09T00:36:02.745350141Z Generating RSA private key, 4096 bit long modulus (2 primes)
2020-03-09T00:36:03.789475259Z ……………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………..++++
2020-03-09T00:36:03.970741605Z …………………………………………..++++
2020-03-09T00:36:03.971106769Z e is 65537 (0x010001)
2020-03-09T00:36:03.992422227Z Signature ok
2020-03-09T00:36:03.992438954Z subject=CN = docker:dind client
2020-03-09T00:36:03.992441863Z Getting CA Private Key
2020-03-09T00:36:04.003038010Z /certs/client/cert.pem: OK
2020-03-09T00:36:04.007079704Z mount: permission denied (are you root?)
2020-03-09T00:36:04.007101584Z Could not mount /sys/kernel/security.
2020-03-09T00:36:04.007107021Z AppArmor detection and –privileged mode might break.
2020-03-09T00:36:04.008064193Z mount: permission denied (are you root?)

[0;33m*********[0;m

[0KPulling docker image docker:17.06.0-ce-dind …
[0;m[0KUsing docker image sha256:5096e5a0cba00693905879b09e24a487dc244b56e8e15349fd5b71b432c6ec9f for docker:17.06.0-ce-dind …
[0;msection_end:1583714200:prepare_executor
[0Ksection_start:1583714200:prepare_script
[0KRunning on runner-yLBZYU-u-project-17314874-concurrent-0 via cf85862c86ae…
section_end:1583714202:prepare_script
[0Ksection_start:1583714202:get_sources
[0K[32;1mFetching changes with git depth set to 50…[0;m
Reinitialized existing Git repository in /builds/general-poc1/docker-in-docker/.git/
From https://gitlab.com/general-poc1/docker-in-docker
* [new ref] refs/pipelines/124375282 -> refs/pipelines/124375282
[32;1mChecking out a7401d05 as master…[0;m

[32;1mSkipping Git submodules setup[0;m
section_end:1583714206:get_sources
[0Ksection_start:1583714206:restore_cache
[0Ksection_end:1583714207:restore_cache
[0Ksection_start:1583714207:download_artifacts
[0Ksection_end:1583714209:download_artifacts
[0Ksection_start:1583714209:build_script
[0K[32;1m$ docker info[0;m
Containers: 8
Running: 1
Paused: 0
Stopped: 7
Images: 9
Server Version: 17.06.0-ce
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
Volume: local
Network: bridge host macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: cfb82a876ecc11b5ca0977d1733adbe58599088a
runc version: 2d41c047c83e09a6d61d464906feb2a2f3c52aa4
init version: 949e6fa
Security Options:
apparmor
seccomp
Profile: default
Kernel Version: 5.3.0-40-generic
Operating System: Alpine Linux v3.6 (containerized)
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 15.52GiB
Name: 884a7763156f
ID: SUJE:U7KG:TX7Y:PGUX:Y7LO:2Z7P:ILN5:HWCQ:VWCR:AATF:LT2A:YKSJ
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false

WARNING: No swap limit support
[32;1m$ docker login -u “$CI_REGISTRY_USER” -p “$CI_REGISTRY_PASSWORD” $CI_REGISTRY[0;m
Login Succeeded
[32;1m$ docker build –pull -t “$CI_REGISTRY_IMAGE” .[0;m
Sending build context to Docker daemon 69.86MB

Step 1/6 : FROM maven:3.5-jdk-11 as BUILD
3.5-jdk-11: Pulling from library/maven
Digest: sha256:ddef9abf83e3df522ca9339deed9198316946a51266b2675f8dc1e8d7d2076f3
Status: Image is up to date for maven:3.5-jdk-11
—> 68cb9afc527c
Step 2/6 : FROM openjdk:11-jdk
11-jdk: Pulling from library/openjdk
Digest: sha256:4c95a73f4808fec0f9c9ddf24570bb0b6e3a2ea47d4e00af9d566654d44ea08c
Status: Image is up to date for openjdk:11-jdk
—> d29dd615eaf4
Step 3/6 : ENV PORT 4567
—> Using cache
—> c5947f7fb4c7
Step 4/6 : EXPOSE 4567
—> Using cache
—> 997142d85a59
Step 5/6 : WORKDIR /opt/target
—> Using cache
—> 31475a344bcb
Step 6/6 : CMD /bin/bash -c find -type f -name ‘*-with-dependencies.jar’ | xargs java -jar
—> Using cache
—> 01ba0c7578cf
Successfully built 01ba0c7578cf
Successfully tagged williamalvessantos/poc-docker-in-docker:latest
[32;1m$ docker push “$CI_REGISTRY_IMAGE”[0;m
The push refers to a repository [docker.io/williamalvessantos/poc-docker-in-docker]
3dea42a4d46d: Preparing
4186db990701: Preparing
5b5fbf48ec75: Preparing
832f129ebea4: Preparing
6670e930ed33: Preparing
c7f27a4eb870: Preparing
e70dfb4c3a48: Preparing
1c76bd0dc325: Preparing
1c76bd0dc325: Waiting
c7f27a4eb870: Waiting
5b5fbf48ec75: Layer already exists
4186db990701: Layer already exists
6670e930ed33: Layer already exists
3dea42a4d46d: Layer already exists
832f129ebea4: Layer already exists
1c76bd0dc325: Layer already exists
e70dfb4c3a48: Layer already exists
c7f27a4eb870: Layer already exists
latest: digest: sha256:3d25f5c99e26c6b3d579ee4f5ce46addf51fa206a0792b917b1601148fdf7590 size: 2001
section_end:1583714267:build_script
[0Ksection_start:1583714267:after_script
[0Ksection_end:1583714269:after_script
[0Ksection_start:1583714269:archive_cache
[0Ksection_end:1583714270:archive_cache
[0Ksection_start:1583714270:upload_artifacts_on_success
[0Ksection_end:1583714272:upload_artifacts_on_success
[0K[32;1mJob succeeded
[0;m

Então é isso, esta funcionando conforme LOG. Assim que possível montarei uma nova execução mas com outro runner id.

Segue o git referente a tudo isso: https://gitlab.com/general-poc1/docker-in-docker.git

ROAD MAP

  • Cenário de testes;
  • Pegar token do runner do projeto via API e inserir no arquivo de configuração. Informar via parâmetro token principal, group_ID e projeto;
  • Rotina de auto inspeção dos runner com notificação ativa.
  • Migrar para o kubernetes, e deixar tudo ainda mais liso.

FIM

Espero ter contribuído e ajudado outras pessoas, sintam-se a vontade em contribuir afinal este projeto é público. Toda ajuda é sempre bem vinda.

Agora vamos aproveitar a vida com outras coisas.

Image for post

party

REFERÊNCIAS