Já falamos sobre a ferramenta GitOps do werf mais
de uma vez , mas desta vez gostaríamos de compartilhar a experiência de criar o site com a documentação do projeto -
werf.io (a versão russa é
ru.werf.io ). Este é um site estático comum, mas sua montagem é interessante porque é construída usando um número dinâmico de artefatos.

Entre nas nuances da estrutura do site: gerando um menu geral para todas as versões, páginas com informações sobre lançamentos, etc. - nós não vamos. Em vez disso, focamos nos problemas e recursos da montagem dinâmica e um pouco nos processos de CI / CD que o acompanham.
Introdução: como o site está organizado
Para começar, a documentação do werf é armazenada junto com seu código. Isso cria certos requisitos de desenvolvimento que geralmente vão além do escopo deste artigo, mas pelo menos podemos dizer que:
- As novas funções do werf não devem ser liberadas sem atualizar a documentação e, inversamente, quaisquer alterações na documentação implicam o lançamento de uma nova versão do werf;
- O projeto tem um desenvolvimento bastante intenso: novas versões podem sair várias vezes ao dia;
- Qualquer implantação manual de um site com uma nova versão da documentação é pelo menos entediante;
- O projeto adotou a abordagem de versionamento semântico, com 5 canais de estabilidade. O processo de liberação envolve a passagem seqüencial de versões pelos canais, a fim de aumentar a estabilidade: do alfa ao sólido;
- O site possui uma versão em russo, que "vive e se desenvolve" (ou seja, cujo conteúdo é atualizado) em paralelo com a versão principal (ou seja, em inglês).
Para ocultar ao usuário toda essa "cozinha interna", oferecendo a ele algo que "simplesmente funciona", fizemos uma
ferramenta separada de instalação e atualização do werf - este é o
multiwerf . Basta indicar o número do release e o canal de estabilidade que você está pronto para usar, e o multiwerf verificará se há uma nova versão no canal e fará o download, se necessário.
A versão mais recente do werf em cada canal está disponível no menu de seleção de versão no site. Por padrão, a versão do canal mais estável para a versão mais recente é aberta em
werf.io/documentation - também é indexada pelos mecanismos de pesquisa. A documentação do canal está disponível em endereços individuais (por exemplo,
werf.io/v1.0-beta/documentation para a versão beta 1.0).
Total, o site tem as seguintes versões:
- raiz (abre por padrão)
- para cada canal de atualização ativo para cada versão (por exemplo, werf.io/v1.0-beta ).
Para gerar uma versão específica de um site no caso geral, basta compilá-lo usando as ferramentas
Jekyll executando o comando apropriado (
jekyll build
) no diretório
/docs
do repositório werf, após alternar para a tag Git da versão requerida.
Resta apenas acrescentar que:
- o próprio utilitário (werf) é usado para montagem;
- Os processos de IC / CD são baseados no IC do GitLab;
- e tudo isso, é claro, funciona em Kubernetes.
As tarefas
Agora, formulamos tarefas que levam em conta todas as especificidades descritas:
- Após alterar a versão do werf em qualquer canal de atualização, a documentação no site deve ser atualizada automaticamente .
- Para o desenvolvimento, você precisa poder ocasionalmente visualizar versões preliminares do site .
A recompilação do site deve ser realizada após a alteração da versão em qualquer canal das tags Git correspondentes, mas no processo de criação da imagem, obteremos os seguintes recursos:
- Como a lista de versões nos canais está mudando, é necessário remontar a documentação dos canais nos quais a versão foi alterada. Afinal, remontar tudo de novo não é muito bonito.
- O conjunto de canais para lançamentos pode variar. Em algum momento, por exemplo, a versão nos canais pode não ser mais estável que a versão 1.1 do acesso antecipado, mas com o tempo eles aparecerão - não altere o conjunto manualmente neste caso?
Acontece que a
montagem depende da alteração de dados externos .
Implementação
Escolha da abordagem
Como alternativa, você pode executar cada versão necessária com um pod separado no Kubernetes. Essa opção implica em um número maior de objetos no cluster, que crescerá com um aumento no número de liberações de werf estáveis. E isso, por sua vez, implica um serviço mais complexo: cada versão possui seu próprio servidor HTTP e com uma pequena carga. Obviamente, isso implica custos mais altos para os recursos.
Nós seguimos o caminho de
reunir todas as versões necessárias em uma imagem . As estáticas compiladas de todas as versões do site estão em um contêiner com o NGINX, e o tráfego para a Implantação correspondente é feito pelo NGINX Ingress. Uma estrutura simples - um aplicativo sem estado - facilita o dimensionamento da implantação (dependendo da carga) usando o próprio Kubernetes.
Para ser mais preciso, coletamos duas imagens: uma para o circuito de produção e outra para o circuito de desenvolvimento. Uma imagem adicional é usada (lançada) apenas no circuito dev juntamente com a principal e contém a versão do site a partir da confirmação de revisão, e o roteamento entre eles é realizado usando os recursos do Ingress.
artefatos e clone werf vs git
Como já mencionado, para gerar estática do site para uma versão específica da documentação, você precisa construir alternando para a tag do repositório correspondente. Também é possível fazer isso clonando o repositório todas as vezes durante a montagem, selecionando as tags apropriadas na lista. No entanto, essa é uma operação que consome bastante recursos e, além disso, requer a gravação de instruções não triviais ... Outro ponto negativo grave - com essa abordagem, não há como armazenar algo em cache durante a montagem.
Aqui, o utilitário werf vem em nosso auxílio, o que implementa o
cache inteligente e permite o uso de
repositórios externos . O uso do werf para adicionar código do repositório acelerará significativamente a compilação, pois O werf essencialmente faz a clonagem de repositório uma vez e, em seguida,
apenas fetch
se necessário. Além disso, ao adicionar dados do repositório, podemos selecionar apenas os diretórios necessários (no nosso caso, este é o diretório
docs
), o que reduzirá significativamente a quantidade de dados adicionados.
Como Jekyll é uma ferramenta projetada para compilar estática e não é necessária na imagem final, seria lógico compilar no
artefato werf e
importar apenas o resultado da compilação na imagem final.
Escrevendo werf.yaml
Então, decidimos que iremos compilar cada versão em um artefato werf separado. No entanto,
não sabemos quantos desses artefatos serão durante a montagem ; portanto, não podemos escrever uma configuração de montagem fixa (a rigor, ainda podemos, mas não será completamente eficaz).
O werf permite que você use
Go-templates no seu arquivo de configuração (
werf.yaml
), e isso possibilita
gerar uma configuração "on the fly", dependendo dos dados externos (o que você precisa!). Dados externos em nosso caso são informações sobre versões e liberações, com base nas quais coletamos o número necessário de artefatos e, como resultado, obtemos duas imagens:
werf-doc
e
werf-dev
para rodar em caminhos diferentes.
Dados externos são passados por variáveis de ambiente. Aqui está sua composição:
RELEASES
- uma linha com uma lista de lançamentos e a versão atual correspondente do werf, na forma de uma lista, separada por um espaço, no formato <_>%<_>
. Exemplo: 1.0%v1.0.4-beta.20
CHANNELS
- uma linha com uma lista de canais e a versão atual correspondente do werf, na forma de uma lista com um espaço de valores no formato <>%<_>
. Exemplo: 1.0-beta%v1.0.4-beta.20 1.0-alpha%v1.0.5-alpha.22
ROOT_VERSION
- A versão do werf release para exibição por padrão no site (nem sempre é necessário exibir a documentação para o número de release mais alto). Exemplo: v1.0.4-beta.20
REVIEW_SHA
- hash do commit de revisão a partir do qual você precisa coletar a versão para o loop de teste.
Essas variáveis serão preenchidas no IC do GitLab do pipeline e como exatamente é descrito abaixo.
Primeiro, por conveniência, definimos variáveis de modelo Go em
werf.yaml
atribuindo valores a partir de variáveis de ambiente:
{{ $_ := set . "WerfVersions" (cat (env "CHANNELS") (env "RELEASES") | splitList " ") }} {{ $Root := . }} {{ $_ := set . "WerfRootVersion" (env "ROOT_VERSION") }} {{ $_ := set . "WerfReviewCommit" (env "REVIEW_SHA") }}
A descrição do artefato para compilar a estática da versão do site geralmente é a mesma para todos os casos que precisamos (incluindo a geração da versão raiz, bem como a versão do circuito dev). Portanto, nós o colocaremos em um bloco separado usando a função
define
- para reutilização subsequente com
include
. Passaremos os seguintes argumentos para o modelo:
Version
gerada por versão (nome da tag);Channel
- o nome do canal de atualização para o qual o artefato é gerado;Commit
- hash de confirmação se artefato for gerado para confirmação de revisão;- contexto.
Descrição do Modelo de Artefato {{- define "doc_artifact" -}} {{- $Root := index . "Root" -}} artifact: doc-{{ .Channel }} from: jekyll/builder:3 mount: - from: build_dir to: /usr/local/bundle ansible: install: - shell: | export PATH=/usr/jekyll/bin/:$PATH - name: "Install Dependencies" shell: bundle install args: executable: /bin/bash chdir: /app/docs beforeSetup: {{- if .Commit }} - shell: echo "Review SHA - {{ .Commit }}." {{- end }} {{- if eq .Channel "root" }} - name: "releases.yml HASH: {{ $Root.Files.Get "releases.yml" | sha256sum }}" copy: content: | {{ $Root.Files.Get "releases.yml" | indent 8 }} dest: /app/docs/_data/releases.yml {{- else }} - file: path: /app/docs/_data/releases.yml state: touch {{- end }} - file: path: "{{`{{ item }}`}}" state: directory mode: 0777 with_items: - /app/main_site/ - /app/ru_site/ - file: dest: /app/docs/pages_ru/cli state: link src: /app/docs/pages/cli - shell: | echo -e "werfVersion: {{ .Version }}\nwerfChannel: {{ .Channel }}" > /tmp/_config_additional.yml export PATH=/usr/jekyll/bin/:$PATH {{- if and (ne .Version "review") (ne .Channel "root") }} {{- $_ := set . "BaseURL" ( printf "v%s" .Channel ) }} {{- else if ne .Channel "root" }} {{- $_ := set . "BaseURL" .Channel }} {{- end }} jekyll build -s /app/docs -d /app/_main_site/{{ if .BaseURL }} --baseurl /{{ .BaseURL }}{{ end }} --config /app/docs/_config.yml,/tmp/_config_additional.yml jekyll build -s /app/docs -d /app/_ru_site/{{ if .BaseURL }} --baseurl /{{ .BaseURL }}{{ end }} --config /app/docs/_config.yml,/app/docs/_config_ru.yml,/tmp/_config_additional.yml args: executable: /bin/bash chdir: /app/docs git: - url: https:
O nome do artefato deve ser exclusivo. Podemos conseguir isso, por exemplo, adicionando o nome do canal (valor da variável
.Channel
) como um sufixo para o nome do artefato:
artifact: doc-{{ .Channel }}
. Mas você precisa entender que, ao importar de artefatos, será necessário consultar os mesmos nomes.
Ao descrever um artefato, um recurso werf como
mount é usado . A montagem com o diretório de serviço
build_dir
permite salvar o cache Jekyll entre as partidas do pipeline, o que
acelera bastante a reconstrução .
Você também deve ter notado o uso do arquivo
releases.yml
- este é o arquivo YAML com os dados da liberação solicitados no
github.com (o artefato obtido pela execução do pipeline). É necessário ao compilar o site, mas no contexto do artigo, estamos interessados no fato de que
apenas um artefato , o
artefato raiz da versão do site
, depende de seu estado (em outros artefatos não é necessário).
Isso é implementado usando o operador condicional para modelos
{{ $Root.Files.Get "releases.yml" | sha256sum }}
go e o
{{ $Root.Files.Get "releases.yml" | sha256sum }}
{{ $Root.Files.Get "releases.yml" | sha256sum }}
no
palco . Isso funciona da seguinte maneira: ao montar um artefato para a versão raiz (a variável
.Channel
é
root
), o hash do arquivo
releases.yml
afeta a assinatura de todo o estágio, pois é um componente do nome do trabalho Ansible (parâmetro
name
). Portanto, ao alterar o
conteúdo do arquivo
releases.yml
, o artefato correspondente será reconstruído.
Preste atenção também ao trabalho com um repositório externo. Somente o diretório
/docs
é incluído na imagem do artefato do
repositório werf e, dependendo dos parâmetros passados, os dados da tag ou confirmação necessária são adicionados imediatamente.
Para usar o modelo de artefato para gerar uma descrição de artefato das versões transferidas de canais e liberações, organizamos um loop na variável
.WerfVersions
em
werf.yaml
:
{{ range .WerfVersions -}} {{ $VersionsDict := splitn "%" 2 . -}} {{ dict "Version" $VersionsDict._1 "Channel" $VersionsDict._0 "Root" $Root | include "doc_artifact" }} --- {{ end -}}
Porque o loop irá gerar vários artefatos (esperamos que sim), é necessário levar em consideração o separador entre eles - a sequência
---
(para obter mais informações sobre a sintaxe do arquivo de configuração, consulte a
documentação ). Conforme determinado anteriormente, quando você chama o modelo em um loop, passamos os parâmetros de versão, URL e contexto raiz.
Da mesma forma, mas já sem um loop, chamamos o modelo de artefato para "casos especiais": para a versão raiz, bem como a versão do commit de revisão:
{{ dict "Version" .WerfRootVersion "Channel" "root" "Root" $Root | include "doc_artifact" }} --- {{- if .WerfReviewCommit }} {{ dict "Version" "review" "Channel" "review" "Commit" .WerfReviewCommit "Root" $Root | include "doc_artifact" }} {{- end }}
Observe que o artefato para a confirmação de revisão será coletado apenas se a variável
.WerfReviewCommit
estiver
.WerfReviewCommit
.
Os artefatos estão prontos - é hora de importar!
A imagem final, projetada para ser executada no Kubernetes, é um NGINX regular, no qual o arquivo de configuração do servidor
nginx.conf
e as estáticas dos artefatos são adicionados. Além do artefato da versão raiz do site, precisamos repetir o loop na variável
.WerfVersions
para importar artefatos das versões dos canais e releases + observe a regra de nomenclatura do artefato que adotamos anteriormente. Como cada artefato armazena versões do site para dois idiomas, nós os importamos para os locais fornecidos pela configuração.
Descrição da imagem final do werf-doc image: werf-doc from: nginx:stable-alpine ansible: setup: - name: "Setup /etc/nginx/nginx.conf" copy: content: | {{ .Files.Get ".werf/nginx.conf" | indent 8 }} dest: /etc/nginx/nginx.conf - file: path: "{{`{{ item }}`}}" state: directory mode: 0777 with_items: - /app/main_site/assets - /app/ru_site/assets import: - artifact: doc-root add: /app/_main_site to: /app/main_site before: setup - artifact: doc-root add: /app/_ru_site to: /app/ru_site before: setup {{ range .WerfVersions -}} {{ $VersionsDict := splitn "%" 2 . -}} {{ $Channel := $VersionsDict._0 -}} {{ $Version := $VersionsDict._1 -}} - artifact: doc-{{ $Channel }} add: /app/_main_site to: /app/main_site/v{{ $Channel }} before: setup {{ end -}} {{ range .WerfVersions -}} {{ $VersionsDict := splitn "%" 2 . -}} {{ $Channel := $VersionsDict._0 -}} {{ $Version := $VersionsDict._1 -}} - artifact: doc-{{ $Channel }} add: /app/_ru_site to: /app/ru_site/v{{ $Channel }} before: setup {{ end -}}
A imagem adicional, que, juntamente com a imagem principal, é lançada no circuito de desenvolvimento, contém apenas duas versões do site: a versão do commit de revisão e a versão raiz do site (existem ativos gerais e, se você se lembrar, libera dados). Assim, a imagem adicional da imagem principal diferirá apenas na seção de importação (e, é claro, no nome):
image: werf-dev ... import: - artifact: doc-root add: /app/_main_site to: /app/main_site before: setup - artifact: doc-root add: /app/_ru_site to: /app/ru_site before: setup {{- if .WerfReviewCommit }} - artifact: doc-review add: /app/_main_site to: /app/main_site/review before: setup - artifact: doc-review add: /app/_ru_site to: /app/ru_site/review before: setup {{- end }}
Como já mencionado acima, o artefato para a confirmação de revisão será gerado apenas quando o werf iniciar com a variável de ambiente
REVIEW_SHA
. Seria possível não gerar uma imagem werf-dev, se não
REVIEW_SHA
ambiente
REVIEW_SHA
, mas, para que a
limpeza de imagem baseada em werf,
para que as imagens do Docker funcionem para a imagem werf-dev, deixamos que seja coletada apenas com o artefato da versão raiz (de qualquer forma, já montado), para simplificar a estrutura do oleoduto.
Montagem está pronta! Passamos para o CI / CD e nuances importantes.
Pipeline no IC do GitLab e recursos de montagem dinâmica
Ao iniciar a montagem, precisamos definir as variáveis de ambiente usadas no
werf.yaml
. Isso não se aplica à variável REVIEW_SHA, que definiremos quando o pipeline for chamado a partir do gancho do GitHub.
Geraremos os dados externos necessários no script Bash
generate_artifacts
, que gerará dois artefatos do GitLab de pipeline:
- arquivo
releases.yml
com dados da liberação, - arquivo
common_envs.sh
contendo variáveis de ambiente para exportação.
Você encontrará o conteúdo do arquivo
generate_artifacts
em nosso
repositório de exemplo . A obtenção de dados não é o assunto do artigo, mas o arquivo
common_envs.sh
é importante para nós, porque o trabalho de werf depende disso. Um exemplo de seu conteúdo:
export RELEASES='1.0%v1.0.6-4' export CHANNELS='1.0-alpha%v1.0.7-1 1.0-beta%v1.0.7-1 1.0-ea%v1.0.6-4 1.0-stable%v1.0.6-4 1.0-rock-solid%v1.0.6-4' export ROOT_VERSION='v1.0.6-4'
Você pode usar a saída desse script, por exemplo, usando a função Bash de
source
.
E agora a parte divertida. Para que os aplicativos de compilação e implantação funcionem corretamente, você deve tornar o
werf.yaml
mesmo para pelo menos
um pipeline . Se essa condição não for atendida, as assinaturas dos estágios que foram calculados durante a montagem e, por exemplo, a implantação, serão diferentes. Isso levará a um erro de implantação, pois a imagem necessária para implantação estará ausente.
Em outras palavras, se durante a montagem da imagem do site as informações sobre releases e versões forem uma, e no momento do release uma nova versão for lançada e as variáveis de ambiente tiverem valores diferentes, a implantação falhará: afinal, o artefato da nova versão ainda não foi coletado.
Se a geração do
werf.yaml
depender de dados externos (por exemplo, uma lista de versões atuais, como no nosso caso), a composição e os valores desses dados deverão ser registrados no pipeline. Isso é especialmente importante se os parâmetros externos mudarem com bastante frequência.
Receberemos e capturaremos dados externos no primeiro estágio do pipeline no
GitLab (
Prebuild ) e os transferiremos ainda mais como um
artefato de IC do GitLab . Isso permitirá que você inicie e reinicie as tarefas de pipeline (compilação, implantação, limpeza) com a mesma configuração no
werf.yaml
.
O conteúdo do estágio
Prebuild do arquivo
.gitlab -
ci.yml :
Prebuild: stage: prebuild script: - bash ./generate_artifacts 1> common_envs.sh - cat ./common_envs.sh artifacts: paths: - releases.yml - common_envs.sh expire_in: 2 week
Ao capturar dados externos em um artefato, você pode construir e implantar usando os estágios de pipeline do IC do GitLab padrão: Build and Deploy. Iniciamos o pipeline por ganchos do repositório gerHub werf (ou seja, ao alterar o repositório no GitHub). Os dados para eles podem ser obtidos nas propriedades do projeto GitLab na seção
Configurações de CI / CD -> gatilhos de pipeline e, em seguida, criar o Webhook correspondente (
Configurações -> Webhooks ) no GitHub.
O estágio de construção terá a seguinte aparência:
Build: stage: build script: - type multiwerf && . $(multiwerf use 1.0 alpha --as-file) - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose) - source common_envs.sh - werf build-and-publish --stages-storage :local except: refs: - schedules dependencies: - Prebuild
O GitLab adicionará dois artefatos do estágio
Pré -construção à fase de construção, portanto exportamos as variáveis com entrada preparada usando a
source common_envs.sh
. Iniciamos a fase de montagem em todos os casos, exceto no lançamento do pipeline dentro do prazo. De acordo com o cronograma, o oleoduto será lançado para limpeza - não precisamos construir neste caso.
No estágio de implantação, descrevemos duas tarefas - separadamente para a implantação nos circuitos de produção e desenvolvimento, usando o modelo YAML:
.base_deploy: &base_deploy stage: deploy script: - type multiwerf && . $(multiwerf use 1.0 alpha --as-file) - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose) - source common_envs.sh - werf deploy --stages-storage :local dependencies: - Prebuild except: refs: - schedules Deploy to Production: <<: *base_deploy variables: WERF_KUBE_CONTEXT: prod environment: name: production url: werf.io only: refs: - master except: variables: - $REVIEW_SHA refs: - schedules Deploy to Test: <<: *base_deploy variables: WERF_KUBE_CONTEXT: dev environment: name: test url: werf.test.flant.com except: refs: - schedules only: variables: - $REVIEW_SHA
As tarefas essencialmente diferem apenas indicando o contexto do cluster no qual o werf deve executar a implantação (
WERF_KUBE_CONTEXT
) e configurando as variáveis de ambiente do contorno (
environment.name
e
environment.url
), que são usadas nos modelos de gráfico Helm. O conteúdo dos modelos não será fornecido, porque não há nada interessante para este tópico, mas você pode encontrá-los no
repositório do artigo .
Toque final
Como as versões werf são lançadas com bastante frequência, novas imagens geralmente são coletadas e o Docker Registry cresce constantemente. Portanto, é necessário configurar a limpeza automática de imagens por política. É muito fácil de fazer.
Para implementação, você precisará de:
- Adicione uma etapa de purificação ao
.gitlab-ci.yml
; - Adicione tarefas de limpeza periódicas;
- Defina a variável de ambiente com token de acesso de gravação.
Adicione o estágio de limpeza ao
.gitlab-ci.yml
:
Cleanup: stage: cleanup script: - type multiwerf && . $(multiwerf use 1.0 alpha --as-file) - type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose) - source common_envs.sh - docker login -u nobody -p ${WERF_IMAGES_CLEANUP_PASSWORD} ${WERF_IMAGES_REPO} - werf cleanup --stages-storage :local only: refs: - schedules
Quase todos nós já vimos isso um pouco mais alto - apenas para a limpeza, é necessário primeiro fazer logon no Docker Registry com um token com direitos para excluir imagens no Docker Registry (o token de tarefa do GitLab CI emitido automaticamente não possui esses direitos). O token deve ser inserido no GitLab antecipadamente e seu valor deve ser especificado na variável de ambiente
WERF_IMAGES_CLEANUP_PASSWORD
projeto
(Configurações de CI / CD -> Variáveis) .
A adição de uma tarefa de limpeza com a programação necessária é feita em
CI / CD ->
HoráriosÉ isso: o projeto no Docker Registry não crescerá mais constantemente a partir de imagens não utilizadas.
No final da parte prática, lembro que as listagens completas do artigo estão disponíveis no
Git :
Resultado
- Temos uma estrutura de construção lógica: um artefato por versão.
- A montagem é universal e não requer alterações manuais quando novas versões do werf são lançadas: a documentação no site é atualizada automaticamente.
- São coletadas duas imagens para contornos diferentes.
- Funciona rápido porque o armazenamento em cache é usado ao máximo - quando uma nova versão do werf é lançada ou um gancho do GitHub é chamado para uma confirmação de revisão, apenas o artefato correspondente com uma versão modificada é reconstruído.
- Não é necessário pensar em excluir imagens não utilizadas: a limpeza da política werf manterá a ordem no Docker Registry.
Conclusões
- O uso do werf permite que o assembly trabalhe rapidamente, graças ao armazenamento em cache do próprio assembly e ao trabalhar com repositórios externos.
- Git- . werf ,
fetch
. - Go-
werf.yaml
, . - werf — , pipeline.
- werf , .
PS
: