Tudo começou quando o líder de uma das nossas equipes de desenvolvedores pediu para disponibilizar em teste seu novo aplicativo, que havia sido containerizado no dia anterior. Eu disponibilizei. Cerca de 20 minutos depois, recebi um pedido para atualizar o aplicativo, porque adicionaram uma funcionalidade muito necessária. Eu atualizei. Depois de algumas horas... bem, vocês já sabem o que começou a acontecer em seguida...
Confesso que sou um pouco preguiçoso (já havia confessado isso antes, não?). Considerando que os líderes de equipe têm acesso ao Jenkins, onde temos todo o CI/CD, pensei: que ele faça o deployment quantas vezes quiser! Lembrei de uma piada: dê um peixe a uma pessoa e ela ficará satisfeita por um dia; chame-a de Satisfeito e ela ficará satisfeita por toda a vida. E fui fazer um job, que soubesse fazer o deployment em um container do Kubernetes com qualquer versão do aplicativo que tivesse sido compilada com sucesso e passar para ele quaisquer valores ENV (meu avô - um filólogo, professor de inglês no passado - agora estaria rodando o dedo na têmpora e olhando para mim de forma muito expressiva ao ler esta frase).
Assim, neste artigo vou contar como aprendi a:
- Atualizar dinamicamente tarefas no Jenkins a partir da própria tarefa ou de outras tarefas;
- Conectar-se ao console em nuvem (Cloud shell) a partir de um nó com um agente do Jenkins instalado;
- Fazer o deployment de uma carga de trabalho (workload) no Google Kubernetes Engine.
Na verdade, estou sendo um pouco dissimulado. Supõe-se que pelo menos parte da infraestrutura esteja na nuvem do Google, e, portanto, você é um usuário dela e, claro, possui uma conta GCP. Mas este artigo não é sobre isso.
Este é mais um dos meus resumos. Gosto de escrever esse tipo de artigo apenas em uma situação: quando enfrento um problema, não sabia como resolvê-lo de início, a solução não apareceu no Google de forma pronta, então a pesquisei em partes e, no final, consegui resolver o problema. E, para que no futuro, quando eu esquecer como fiz isso, não precise pesquisar tudo novamente em partes e compilar novamente, escrevo esses resumos para mim mesmo.
Isenção de responsabilidade: 1. O artigo foi escrito "para mim", não tem a pretensão de ser um best practice. Ficarei feliz em ler sugestões de "seria melhor fazer assim" nos comentários.
2. Se considerarmos a parte prática deste artigo como sal, então, como todos os meus artigos anteriores, este é uma solução fraca de sal.
Atualização dinâmica das configurações de tarefas no Jenkins
Antevejo sua pergunta: qual a relação com a atualização dinâmica da tarefa? Eu só precisei digitar o valor do parâmetro e pronto!
Respondendo: sou realmente preguiçoso, não gosto quando as pessoas reclamam: Misha, o deployment está falhando, tudo está perdido! Você começa a investigar e encontra um erro de digitação em algum parâmetro da tarefa. Por isso, prefiro fazer tudo da maneira mais à prova de falhas possível. Se há uma possibilidade de impedir que o usuário insira dados diretamente e, em vez disso, fornecer uma lista de valores para seleção, eu organizo essa escolha.
O plano é o seguinte: criamos uma tarefa no Jenkins, onde, antes da execução, seria possível escolher a versão a partir de uma lista, especificando os valores dos parâmetros que serão passados para o contêiner através de ENV, em seguida, ele constrói o contêiner e o envia para o Container Registry. Depois, o contêiner é iniciado no Kubernetes como workload com os parâmetros definidos na tarefa.
Não vamos abordar o processo de criação e configuração de uma tarefa no Jenkins, isso é um tópico secundário. Partiremos do pressuposto de que a tarefa está pronta. Para implementar uma lista atualizável com versões, precisamos de duas coisas: uma lista de origem já existente com números de versão validados aprioristicamente e uma variável do tipo Choice parameter na tarefa. No nosso exemplo, vamos nomear a variável de BUILD_VERSION, não iremos entrar em detalhes sobre isso. Mas vamos focar mais na lista de origem.
As opções não são tantas assim. Me vêm à mente duas de imediato:
- Usar a API de Acesso Remoto, que o Jenkins oferece a seus usuários;
- Solicitar o conteúdo da pasta remota do repositório (neste caso, isso é o JFrog Artifactory, que não é relevante).
API de Acesso Remoto do Jenkins
Seguindo a bela tradição, prefiro evitar explicações extensas.
Permitirei-me apenas uma tradução livre de um trecho do primeiro parágrafo :
O Jenkins fornece uma API para acesso remoto e compreensível por máquinas ao seu funcionalidade. O acesso remoto é oferecido em um estilo semelhante ao REST. Isso significa que não há um único ponto de entrada para todas as funcionalidades, mas, em vez disso, utiliza-se uma URL do tipo "…/api/", onde "…" representa o objeto ao qual as funcionalidades da API se aplicam.
Em outras palavras, se a tarefa de deployment, da qual estamos falando, estiver disponível no endereço http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build, as as API callbacks for this task are available at http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/
Next, we have the option of how to receive the output. Let's focus on XML, as the API only allows filtering in this case.
Let's try to get a list of all task runs. We are only interested in the build name (displayName) and its result (result):
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]Did it work?
Now we'll filter only those runs that ultimately result in SUCCESS. We use the argument &exclude and as a parameter, we will pass the path to the value not equal to SUCCESS. Yes, yes. Double negation is an assertion. We exclude everything that does not interest us:
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result!='SUCCESS'] Screenshot of the list of successful

And just for fun, let's make sure the filter hasn't misled us (filters never lie!) and display the list of 'non-successful':
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]&exclude=freeStyleProject/allBuild[result='SUCCESS'] Screenshot of the list of non-successful

List of versions from the folder on the remote server
There is also a second way to get the list of versions. I actually prefer it over querying the Jenkins API. Because if the application was successfully built, it means it was packaged and placed in the repository in the corresponding folder. Like, the repository is, by default, a storage of working application versions. So, let's ask it what versions are stored. We will curl the remote folder, grep, and awk. If anyone is interested in a one-liner, it's under the spoiler.
Command in one line
Note two things: I'm passing the credentials for connection in the header and I don't need all versions from the folder, just those created within the last month. Edit the command according to your realities and needs:
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[^/]+ )Task configuration and job configuration file in Jenkins
We figured out the source of the version list. Now let's incorporate the obtained list into the task. The obvious solution for me was to add a step in the job for building the application. A step that would be executed in case of a 'success' result.
Open the job build settings and scroll to the bottom. Click on the buttons: Add build step -> Conditional step (single). In the step settings, we select the condition Current build status, set the value SUCCESS, the action to be performed in case of success Run shell command.
E agora a parte mais interessante. As configurações dos trabalhos do Jenkins são armazenadas em arquivos. No formato XML. No caminho http://caminho-para-o-trabalho/config.xml Portanto, você pode baixar o arquivo de configuração, editá-lo da maneira necessária e colocá-lo de volta no lugar de onde o pegou.
Lembre-se, acima concordamos que criariamos um parâmetro para a lista de versões BUILD_VERSION?
Vamos baixar o arquivo de configuração e dar uma olhada dentro dele. Apenas para garantir que o parâmetro está no lugar e realmente na forma correta.
Captura de tela sob o spoiler.
O fragmento config.xml que você possui deve parecer assim. Exceto pelo fato de que o conteúdo do elemento choices ainda está ausente

Conferiu? Então vamos escrever um script que será executado em caso de construção bem-sucedida.
O script irá obter a lista de versões, baixar o arquivo de configuração, escrever a lista de versões no lugar necessário e, em seguida, colocá-lo de volta. Sim. Tudo certo. Escrever a lista de versões no XML no lugar onde já existe uma lista de versões (que existirá no futuro, após a primeira execução do script). Eu sei, existem ainda alguns fervorosos amantes de expressões regulares. Eu não me incluo entre eles. Por favor, instale na máquina onde a configuração será editada. Parece-me que esse não é um grande ônus para evitar a edição do XML com sed.
Sob o spoiler, trago o código que executa toda a sequência descrita acima.
Escrevendo a lista de versões na configuração a partir da pasta em um servidor remoto
#!/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.xmlSe você prefere a opção de obter versões do Jenkins e é tão preguiçoso quanto eu, então sob o spoiler está o mesmo código, mas a lista é do Jenkins:
Escrevendo a lista de versões na configuração a partir do Jenkins
Apenas tenha em mente o seguinte: meu nome de construção consiste em um número sequencial e um número de versão, separados por dois pontos. Portanto, o awk remove a parte desnecessária. Ajuste essa linha conforme 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.xmlTeoricamente, se você testou o código escrito com base nos exemplos acima, então na tarefa de implantação já deve aparecer uma lista suspensa com versões. Assim como na captura de tela sob o spoiler.
Lista de versões preenchida corretamente

Se tudo funcionou, copie e cole o script em Run shell command e salve as alterações.
Conexão ao Cloud shell
Os construtores estão em nossos contêineres. Para entrega de aplicativos e gerenciamento de configuração, usamos Ansible. Portanto, ao falarmos sobre a construção de contêineres, três opções vêm à mente: instalar o Docker dentro do Docker, instalar o Docker em uma máquina com Ansible, ou construir contêineres na console em nuvem. Sobre os plugins para Jenkins, concordamos em não mencionar nesta nota. Lembra-se?
Decidi: bem, já que os contêineres podem ser construídos na console em nuvem "prontos para uso", por que complicar? Mantenha as coisas limpas, certo? Quero construir contêineres com Jenkins na console em nuvem e depois enviá-los para o Kubernetes a partir de lá. Além disso, as conexões internas do Google têm canais bem robustos, o que favorece a velocidade de implantação.
Para se conectar à console em nuvem, são necessárias duas coisas: gcloud e permissões de acesso ao Google Cloud API para a instância da VM a partir da qual essa conexão será estabelecida.
Para aqueles que planejam se conectar de fora do Google Cloud
O Google permite a desativação da autorização interativa em seus serviços. Isso permite se conectar à console mesmo de uma máquina de café, desde que esteja em *nix e tenha sua própria console.
Se houver interesse em que eu explique esse assunto mais detalhadamente nesta nota — escreva nos comentários. Se reunir um número suficiente de votos — farei uma atualização sobre esse tema.
A maneira mais simples de conceder permissões é através da interface da web.
- Pare a instância da VM, da qual a conexão à console em nuvem será realizada posteriormente.
- Abra os Detalhes da instância e clique em Editar.
- Na parte mais baixa da página, selecione o escopo de acesso da instância Acesso total a todas as APIs do Cloud.
Captura de tela

- Salve as alterações e inicie a instância.
Após a inicialização da VM, conecte-se a ela via SSH e verifique se a conexão ocorre sem erro. Use o comando:
gcloud alpha cloud-shell ssh Uma conexão bem-sucedida se parece aproximadamente assim

Implantação no GKE
Como estamos totalmente empenhados em migrar para IaC (Infrastructure as Code), nossos Dockerfiles estão armazenados no Git. Essa é uma parte. A implantação no Kubernetes é descrita por um arquivo yaml, que é usado apenas por esta tarefa, e que é, por si só, também uma espécie de código. Essa é a outra parte. Em resumo, meu plano é:
- Vamos pegar os valores das variáveis BUILD_VERSION E, opcionalmente, valores de variáveis que serão passados através de ENV.
- Baixamos o Dockerfile do Git.
- Geramos o YAML para o deploy.
- Enviamos ambos os arquivos via SCP para o console em nuvem.
- Construímos o container lá e o enviamos para o Container registry.
- Aplicamos o arquivo de deploy da carga no Kubernetes.
Vamos ser mais específicos. Como falamos sobre ENV, suponha que precisaremos passar os valores de dois parâmetros: PARAM1 e PARAM2. Adicionamos a definição deles no deploy, tipo — Parâmetro String.
Captura de tela

Vamos gerar o YAML simplesmente redirecionando echo para um arquivo. Supõe-se, é claro, que no seu Dockerfile estejam presentes PARAM1 e PARAM2, que o nome da carga será awesomeapp, e que o container construído com a versão especificada está localizado em Container registry no caminho gcr.io/awesomeapp/awesomeapp-$BUILD_VERSION, onde $BUILD_VERSION foi exatamente escolhido na lista suspensa.
Listagem dos comandos
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.yamlApós a conexão com o agente do Jenkins através de gcloud alpha cloud-shell ssh o modo interativo não está disponível, portanto, enviamos comandos para o console em nuvem usando o parâmetro —command.
Limpamos a pasta inicial no console em nuvem do antigo Dockerfile:
gcloud alpha cloud-shell ssh --command="rm -f Dockerfile"Colocamos o Dockerfile recém-baixado na pasta inicial do console em nuvem usando SCP:
gcloud alpha cloud-shell scp localhost:./Dockerfile cloudshell:~Construímos, tagueamos e enviamos o container 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"
Procedemos de maneira semelhante com o arquivo de deploy. Note que nos comandos abaixo são utilizados nomes fictícios de cluster para onde está ocorrendo o deploy (awsm-cluster) e o nome do projeto (awesome-project), onde se encontra o cluster.
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"Estamos iniciando a tarefa, abrindo a saída do console e esperamos ver uma compilação bem-sucedida do contêiner.
Captura de tela

E então, um implantação bem-sucedida do contêiner compilado
Captura de tela

Eu de propósito ignorei a configuração Ingress. Por um simples motivo: uma vez configurada com um workload nome específico, ela permanecerá funcional, não importa quantas implantações com esse nome sejam feitas. E, na verdade, isso está um pouco além da história.
Em vez das saídas
Todas as etapas acima, provavelmente, poderiam ter sido ignoradas e simplesmente instalar algum plugin para o Jenkins, existem milhões. Mas por algum motivo eu não gosto de plugins. Bem, mais precisamente, recorro a eles apenas em situações desesperadas.
E eu simplesmente gosto de explorar um novo tema para mim. O texto acima é também uma maneira de compartilhar as descobertas que fiz ao resolver a tarefa descrita no início. Compartilhar com aqueles que, assim como eu, não são verdadeiros monstros em DevOps. Se pelo menos alguém se beneficiar do que encontrei, eu ficarei feliz.
Fonte: habr.com

