Criamos uma tarefa de implantação no GKE sem plugins, SMS ou registro. Vamos dar uma olhada embaixo da jaqueta de Jenkins

Tudo começou quando o líder de uma de nossas equipes de desenvolvimento nos pediu para testar seu novo aplicativo, que havia sido conteinerizado no dia anterior. Eu postei. Após cerca de 20 minutos, foi recebido um pedido de atualização do aplicativo, pois ali havia algo muito necessário. Eu renovei. Depois de mais algumas horas... bem, você pode adivinhar o que começou a acontecer a seguir...

Devo admitir que sou bastante preguiçoso (não admiti isso antes? Não?), e dado o fato dos líderes de equipe terem acesso ao Jenkins, no qual temos todos CI/CD, pensei: deixe-o implantar como quanto ele quiser! Lembrei-me de uma piada: dê um peixe a um homem e ele comerá por um dia; chame uma pessoa de Fed e ela será Fed por toda a vida. E foi pregar peças no trabalho, que seria capaz de implantar um contêiner contendo o aplicativo de qualquer versão construída com sucesso no Kuber e transferir quaisquer valores para ele ENV (meu avô, filólogo, ex-professor de inglês, agora girava o dedo na têmpora e me olhava de forma muito expressiva depois de ler esta frase).

Então, nesta nota vou contar como aprendi:

  1. Atualizar trabalhos dinamicamente no Jenkins a partir do próprio trabalho ou de outros trabalhos;
  2. Conecte-se ao console da nuvem (Cloud Shell) a partir de um nó com o agente Jenkins instalado;
  3. Implante a carga de trabalho no Google Kubernetes Engine.


Na verdade, estou, é claro, sendo um tanto hipócrita. Presume-se que você tenha pelo menos parte da infraestrutura na nuvem do Google e, portanto, seja seu usuário e, claro, tenha uma conta GCP. Mas não é disso que trata esta nota.

Esta é minha próxima folha de dicas. Só quero escrever essas notas em um caso: me deparei com um problema, inicialmente não sabia como resolvê-lo, a solução não estava pesquisada no Google pronta, então pesquisei em partes e acabei resolvendo o problema. E para que no futuro, quando eu esquecer como fiz isso, não precise pesquisar tudo de novo no Google, peça por peça, e compilá-lo, eu mesmo escrevo essas folhas de dicas.

Aviso Legal: 1. A nota foi escrita “para mim”, para o papel melhores práticas não se aplica. Fico feliz em ler as opções “teria sido melhor fazer assim” nos comentários.
2. Se a parte aplicada da nota for considerada sal, então, como todas as minhas notas anteriores, esta é uma solução salina fraca.

Atualizando dinamicamente as configurações do trabalho no Jenkins

Prevejo sua pergunta: o que a atualização dinâmica de empregos tem a ver com isso? Insira o valor do parâmetro string manualmente e pronto!

Eu respondo: sou muito preguiçoso, não gosto quando reclamam: Misha, a implantação está travando, acabou tudo! Você começa a procurar e há um erro de digitação no valor de algum parâmetro de inicialização de tarefa. Portanto, prefiro fazer tudo da forma mais eficiente possível. Se for possível evitar que o usuário insira dados diretamente, fornecendo uma lista de valores para escolher, então eu organizo a seleção.

O plano é este: criamos um job no Jenkins, no qual, antes de lançar, poderíamos selecionar uma versão da lista, especificar valores para parâmetros passados ​​​​ao container via ENV, ele coleta o contêiner e o envia para o Container Registry. Então, a partir daí, o contêiner é lançado no cuber como carga de trabalho com os parâmetros especificados no trabalho.

Não consideraremos o processo de criação e configuração de um emprego no Jenkins, isso está fora do assunto. Assumiremos que a tarefa está pronta. Para implementar uma lista atualizada com versões, precisamos de duas coisas: uma lista de fontes existente com números de versão válidos a priori e uma variável como Parâmetro de escolha na tarefa. Em nosso exemplo, deixe a variável ser nomeada BUILD_VERSION, não nos deteremos nisso em detalhes. Mas vamos dar uma olhada mais de perto na lista de fontes.

Não há muitas opções. Duas coisas imediatamente me vieram à mente:

  • Utilize a API de acesso remoto que o Jenkins oferece aos seus usuários;
  • Solicite o conteúdo da pasta do repositório remoto (no nosso caso é JFrog Artifactory, o que não é importante).

API de acesso remoto Jenkins

De acordo com a excelente tradição estabelecida, preferiria evitar longas explicações.
Permitir-me-ei apenas uma tradução livre de um trecho do primeiro parágrafo primeira página da documentação da API:

Jenkins fornece uma API para acesso remoto legível por máquina à sua funcionalidade. <…> O acesso remoto é oferecido em estilo REST. Isso significa que não existe um ponto de entrada único para todos os recursos, mas sim um URL como ".../api/", Onde "..." significa o objeto ao qual os recursos da API são aplicados.

Em outras palavras, se a tarefa de implantação da qual estamos falando atualmente estiver disponível em http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, então os apitos da API para esta tarefa estarão disponíveis em http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/

A seguir, podemos escolher a forma de receber a saída. Vamos focar no XML, já que a API só permite filtragem neste caso.

Vamos apenas tentar obter uma lista de todas as execuções de trabalhos. Estamos interessados ​​apenas no nome do assembly (Nome em Exibição) e seu resultado (resultar):

http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]

Será que isso funciona?

Agora vamos filtrar apenas as execuções que terminam com o resultado SUCESSO. Vamos usar o argumento &excluir e como parâmetro passaremos o caminho para um valor diferente de SUCESSO. Sim Sim. Uma dupla negativa é uma afirmação. Excluímos tudo o que não nos interessa:

http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS']

Captura de tela da lista de sucessos
Criamos uma tarefa de implantação no GKE sem plugins, SMS ou registro. Vamos dar uma olhada embaixo da jaqueta de Jenkins

Bem, só por diversão, vamos ter certeza de que o filtro não nos enganou (os filtros nunca mentem!) e exibir uma lista dos “malsucedidos”:

http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS']

Captura de tela da lista de pessoas sem sucesso
Criamos uma tarefa de implantação no GKE sem plugins, SMS ou registro. Vamos dar uma olhada embaixo da jaqueta de Jenkins

Lista de versões de uma pasta em um servidor remoto

Existe uma segunda maneira de obter uma lista de versões. Gosto ainda mais do que acessar a API Jenkins. Bem, porque se o aplicativo foi construído com sucesso, significa que ele foi empacotado e colocado no repositório na pasta apropriada. Da mesma forma, um repositório é o armazenamento padrão de versões funcionais de aplicativos. Como. Bem, vamos perguntar a ele quais versões estão armazenadas. Vamos curl, grep e awk a pasta remota. Se alguém estiver interessado no oneliner, então ele está embaixo do spoiler.

Comando de uma linha
Observe duas coisas: passo os detalhes da conexão no cabeçalho e não preciso de todas as versões da pasta, e seleciono apenas aquelas que foram criadas dentro de um mês. Edite o comando para atender às suas realidades e necessidades:

curl -H "X-JFrog-Art-Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//' | grep "$(date +%b)-$(date +%Y)|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>K[^/]+' )

Configurando jobs e arquivo de configuração de jobs no Jenkins

Descobrimos a origem da lista de versões. Vamos agora incorporar a lista resultante na tarefa. Para mim, a solução óbvia foi adicionar uma etapa na tarefa de criação do aplicativo. A etapa que seria executada se o resultado fosse “sucesso”.

Abra as configurações da tarefa de montagem e vá até o final. Clique nos botões: Adicionar etapa de construção -> Etapa condicional (única). Nas configurações da etapa, selecione a condição Status atual da compilação, defina o valor SUCESSO, a ação a ser executada se for bem-sucedida Executar comando shell.

E agora a parte divertida. Jenkins armazena configurações de trabalho em arquivos. Em formato XML. Pelo caminho http://путь-до-задания/config.xml Assim, você pode baixar o arquivo de configuração, editá-lo conforme necessário e colocá-lo de volta onde o obteve.

Lembre-se, combinamos acima que criaremos um parâmetro para a lista de versões BUILD_VERSION?

Vamos baixar o arquivo de configuração e dar uma olhada nele. Apenas para ter certeza de que o parâmetro está no lugar e é do tipo desejado.

Captura de tela sob spoiler.

Seu fragmento config.xml deve ter a mesma aparência. Exceto que o conteúdo do elemento de escolhas ainda está faltando
Criamos uma tarefa de implantação no GKE sem plugins, SMS ou registro. Vamos dar uma olhada embaixo da jaqueta de Jenkins

Tem certeza? É isso, vamos escrever um script que será executado se a construção for bem-sucedida.
O script receberá uma lista de versões, baixará o arquivo de configuração, escreverá a lista de versões nele no local que precisamos e depois o colocará de volta. Sim. Isso mesmo. Escreva uma lista de versões em XML no local onde já existe uma lista de versões (será no futuro, após o primeiro lançamento do script). Eu sei que ainda existem fãs ferrenhos de expressões regulares no mundo. Eu não pertenço a eles. Por favor instale xmlstarler para a máquina onde a configuração será editada. Parece-me que este não é um preço tão alto a pagar para evitar a edição de XML usando sed.

Abaixo do spoiler, apresento o código que executa a sequência acima na íntegra.

Escreva uma lista de versões de uma pasta no servidor remoto para a configuração

#!/bin/bash
############## Скачиваем конфиг
curl -X GET -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml -o appConfig.xml

############## Удаляем и заново создаем xml-элемент для списка версий
xmlstarlet ed --inplace -d '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' appConfig.xml

xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]' --type elem -n a appConfig.xml

xmlstarlet ed --inplace --insert '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a' --type attr -n class -v string-array appConfig.xml

############## Читаем в массив список версий из репозитория
readarray -t vers < <( curl -H "X-JFrog-Art-Api:Api:VeryLongAPIKey" -s http://arts.myre.po/artifactory/awesomeapp/ | sed 's/a href=//' | grep "$(date +%b)-$(date +%Y)|$(date +%b --date='-1 month')-$(date +%Y)" | awk '{print $1}' | grep -oP '>K[^/]+' )

############## Пишем массив элемент за элементом в конфиг
printf '%sn' "${vers[@]}" | sort -r | 
                while IFS= read -r line
                do
                    xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' --type elem -n string -v "$line" appConfig.xml
                done

############## Кладем конфиг взад
curl -X POST -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml --data-binary @appConfig.xml

############## Приводим рабочее место в порядок
rm -f appConfig.xml

Se você preferir a opção de obter versões do Jenkins e for tão preguiçoso quanto eu, abaixo do spoiler está o mesmo código, mas uma lista do Jenkins:

Escreva uma lista de versões do Jenkins para a configuração
Apenas tenha isso em mente: meu nome de montagem consiste em um número de sequência e um número de versão, separados por dois pontos. Conseqüentemente, o awk corta a parte desnecessária. Para você, altere esta linha para atender às suas necessidades.

#!/bin/bash
############## Скачиваем конфиг
curl -X GET -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml -o appConfig.xml

############## Удаляем и заново создаем xml-элемент для списка версий
xmlstarlet ed --inplace -d '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' appConfig.xml

xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]' --type elem -n a appConfig.xml

xmlstarlet ed --inplace --insert '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a' --type attr -n class -v string-array appConfig.xml

############## Пишем в файл список версий из Jenkins
curl -g -X GET -u username:apiKey 'http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!=%22SUCCESS%22]&pretty=true' -o builds.xml

############## Читаем в массив список версий из XML
readarray vers < <(xmlstarlet sel -t -v "freeStyleProject/allBuild/displayName" builds.xml | awk -F":" '{print $2}')

############## Пишем массив элемент за элементом в конфиг
printf '%sn' "${vers[@]}" | sort -r | 
                while IFS= read -r line
                do
                    xmlstarlet ed --inplace --subnode '/project/properties/hudson.model.ParametersDefinitionProperty/parameterDefinitions/hudson.model.ChoiceParameterDefinition[name="BUILD_VERSION"]/choices[@class="java.util.Arrays$ArrayList"]/a[@class="string-array"]' --type elem -n string -v "$line" appConfig.xml
                done

############## Кладем конфиг взад
curl -X POST -u username:apiKey http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_k8s/config.xml --data-binary @appConfig.xml

############## Приводим рабочее место в порядок
rm -f appConfig.xml

Em teoria, se você testou o código escrito com base nos exemplos acima, na tarefa de implantação você já deverá ter uma lista suspensa com versões. É como na captura de tela abaixo do spoiler.

Lista de versões preenchida corretamente
Criamos uma tarefa de implantação no GKE sem plugins, SMS ou registro. Vamos dar uma olhada embaixo da jaqueta de Jenkins

Se tudo funcionou, copie e cole o script em Executar comando shell e salve as alterações.

Conectando-se ao Cloud Shell

Temos coletores em containers. Usamos Ansible como nossa ferramenta de entrega de aplicativos e gerenciador de configuração. Conseqüentemente, quando se trata de construir contêineres, três opções vêm à mente: instalar o Docker no Docker, instalar o Docker em uma máquina executando Ansible ou construir contêineres em um console de nuvem. Concordamos em permanecer calados sobre plug-ins para Jenkins neste artigo. Lembrar?

Eu decidi: bem, já que os contêineres “prontos para uso” podem ser coletados no console da nuvem, então por que se preocupar? Mantenha-o limpo, certo? Quero coletar contêineres Jenkins no console da nuvem e, a partir daí, iniciá-los no cuber. Além disso, o Google possui canais muito ricos em sua infraestrutura, o que terá um efeito benéfico na velocidade de implantação.

Para se conectar ao console em nuvem, você precisa de duas coisas: gcloud e direitos de acesso a Google Cloud API para a instância de VM com a qual essa mesma conexão será feita.

Para aqueles que planejam se conectar não pela nuvem do Google
O Google permite a possibilidade de desabilitar a autorização interativa em seus serviços. Isso permitirá que você se conecte ao console mesmo a partir de uma máquina de café, se ela estiver rodando *nix e tiver um console próprio.

Se houver necessidade de abordar esta questão com mais detalhes no âmbito desta nota, escreva nos comentários. Se conseguirmos votos suficientes, escreverei uma atualização sobre este tópico.

A maneira mais fácil de conceder direitos é através da interface web.

  1. Pare a instância de VM da qual você se conectará posteriormente ao console de nuvem.
  2. Abra Detalhes da Instância e clique em emendar.
  3. Na parte inferior da página, selecione o escopo de acesso da instância Acesso total a todas as APIs do Cloud.

    Capturas de tela
    Criamos uma tarefa de implantação no GKE sem plugins, SMS ou registro. Vamos dar uma olhada embaixo da jaqueta de Jenkins

  4. Salve suas alterações e inicie a instância.

Assim que a VM terminar de carregar, conecte-se a ela via SSH e certifique-se de que a conexão ocorra sem erros. Use o comando:

gcloud alpha cloud-shell ssh

Uma conexão bem-sucedida é mais ou menos assim
Criamos uma tarefa de implantação no GKE sem plugins, SMS ou registro. Vamos dar uma olhada embaixo da jaqueta de Jenkins

Implantar no GKE

Como estamos nos esforçando de todas as maneiras possíveis para mudar completamente para IaC (Infraestrutura como Código), nossos arquivos docker são armazenados no Git. Isto é por um lado. E a implantação no Kubernetes é descrita por um arquivo yaml, que é usado apenas por esta tarefa, que também é como um código. Isto é do outro lado. Em geral, quero dizer, o plano é este:

  1. Pegamos os valores das variáveis BUILD_VERSION e, opcionalmente, os valores das variáveis ​​que serão passadas ENV.
  2. Baixe o dockerfile do Git.
  3. Gere yaml para implantação.
  4. Carregamos esses dois arquivos via scp para o console da nuvem.
  5. Construímos um contêiner lá e o colocamos no registro do contêiner
  6. Aplicamos o arquivo de implantação de carga ao cuber.

Sejamos mais específicos. Assim que começamos a falar sobre ENV, então suponha que precisamos passar os valores de dois parâmetros: PARAM1 и PARAM2. Adicionamos sua tarefa para implantação, digite - Parâmetro de sequência.

Capturas de tela
Criamos uma tarefa de implantação no GKE sem plugins, SMS ou registro. Vamos dar uma olhada embaixo da jaqueta de Jenkins

Iremos gerar o yaml com um redirecionamento simples eco arquivar. Supõe-se, é claro, que você tenha em seu dockerfile PARAM1 и PARAM2que o nome da carga será aplicativo incrível, e o contêiner montado com o aplicativo da versão especificada está em Registro de contêiner ao longo do caminho gcr.io/awesomeapp/awesomeapp-$BUILD_VERSIONOnde $BUILD_VERSION acabou de ser selecionado na lista suspensa.

Listagem da equipe

touch deploy.yaml
echo "apiVersion: apps/v1" >> deploy.yaml
echo "kind: Deployment" >> deploy.yaml
echo "metadata:" >> deploy.yaml
echo "  name: awesomeapp" >> deploy.yaml
echo "spec:" >> deploy.yaml
echo "  replicas: 1" >> deploy.yaml
echo "  selector:" >> deploy.yaml
echo "    matchLabels:" >> deploy.yaml
echo "      run: awesomeapp" >> deploy.yaml
echo "  template:" >> deploy.yaml
echo "    metadata:" >> deploy.yaml
echo "      labels:" >> deploy.yaml
echo "        run: awesomeapp" >> deploy.yaml
echo "    spec:" >> deploy.yaml
echo "      containers:" >> deploy.yaml
echo "      - name: awesomeapp" >> deploy.yaml
echo "        image: gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION:latest" >> deploy.yaml
echo "        env:" >> deploy.yaml
echo "        - name: PARAM1" >> deploy.yaml
echo "          value: $PARAM1" >> deploy.yaml
echo "        - name: PARAM2" >> deploy.yaml
echo "          value: $PARAM2" >> deploy.yaml

Agente Jenkins após conectar usando gcloud alfa cloud-shell ssh o modo interativo não está disponível, então enviamos comandos para o console da nuvem usando o parâmetro --comando.

Limpamos a pasta inicial no console da nuvem do dockerfile antigo:

gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"

Coloque o dockerfile recém-baixado na pasta inicial do console da nuvem usando scp:

gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~

Coletamos, etiquetamos e enviamos o contêiner para o Container Registry:

gcloud alpha cloud-shell ssh --command="docker build -t awesomeapp-$BUILD_VERSION ./ --build-arg BUILD_VERSION=$BUILD_VERSION --no-cache"
gcloud alpha cloud-shell ssh --command="docker tag awesomeapp-$BUILD_VERSION gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION"
gcloud alpha cloud-shell ssh --command="docker push gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION"

Fazemos o mesmo com o arquivo de implantação. Observe que os comandos abaixo usam nomes fictícios do cluster onde ocorre a implantação (cluster awsm) e nome do projeto (projeto incrível), onde o cluster está localizado.

gcloud alpha cloud-shell ssh --command="rm -f deploy.yaml"
gcloud alpha cloud-shell scp localhost:./deploy.yaml cloudshell:~
gcloud alpha cloud-shell ssh --command="gcloud container clusters get-credentials awsm-cluster --zone us-central1-c --project awesome-project && 
kubectl apply -f deploy.yaml"

Executamos a tarefa, abrimos a saída do console e esperamos ver a montagem bem-sucedida do contêiner.

Capturas de tela
Criamos uma tarefa de implantação no GKE sem plugins, SMS ou registro. Vamos dar uma olhada embaixo da jaqueta de Jenkins

E então a implantação bem-sucedida do contêiner montado

Capturas de tela
Criamos uma tarefa de implantação no GKE sem plugins, SMS ou registro. Vamos dar uma olhada embaixo da jaqueta de Jenkins

Eu ignorei deliberadamente a configuração Ingresso. Por um motivo simples: depois de configurá-lo carga de trabalho com um determinado nome, ele permanecerá operacional, independentemente de quantas implantações com esse nome você realizar. Bem, em geral, isso está um pouco além do escopo da história.

Em vez de conclusões

Todas as etapas acima provavelmente não poderiam ter sido executadas, mas simplesmente instalado algum plugin para Jenkins, seu muuulion. Mas por algum motivo não gosto de plugins. Bem, mais precisamente, recorro a eles apenas por desespero.

E eu simplesmente gosto de escolher algum novo tópico para mim. O texto acima também é uma forma de compartilhar as descobertas que fiz ao resolver o problema descrito no início. Compartilhe com aqueles que, como ele, não são um lobo terrível em devops. Se minhas descobertas ajudarem pelo menos alguém, ficarei satisfeito.

Fonte: habr.com

Adicionar um comentário