já adivinha o que começou a acontecer a seguir ... Devo admitir, estou um pouco preguiçoso (já admiti isso antes? Não?), E, dado o fato de que os líderes de equipe têm acesso ao Jenkins, no qual Temos todo o CI / CD, pensei: deixa ele se implantar o quanto quiser! Lembrei-me de uma piada: dê um peixe a um homem e ele ficará satisfeito durante o dia; chame uma pessoa Sated e ela será Sated por toda a sua vida. E ele foi fazer o trabalho, que seria capaz de implantar um contêiner com a aplicação de qualquer versão montada com sucesso nele e transferir quaisquer valores ENV para ele (meu avô, um filólogo, um professor de inglês no passado, agora torcia o dedo em sua têmpora e me olhava de maneira muito expressiva depois de ler isto sentença).
Então, em um post vou falar sobre como aprendi:
- Atualize tarefas dinamicamente no Jenkins a partir da própria tarefa ou de outras tarefas;
- Conecte-se ao console da nuvem (Cloud shell) a partir de um nó com o agente Jenkins instalado;
- Implante uma carga de trabalho no Google Kubernetes Engine.
Na verdade, sou, é claro, um pouco astuto. Presume-se que pelo menos parte de sua infraestrutura esteja na nuvem do Google e, portanto, você é o usuário e, claro, tem uma conta do GCP. Mas a nota não é sobre isso.
Esta é minha próxima folha de cola. Só quero escrever essas notas em um caso: eu tinha um problema diante de mim, inicialmente não sabia como resolvê-lo, a solução não foi pesquisada no Google em sua forma final, então pesquisei em partes e eventualmente resolvi o problema. E para que, no futuro, quando eu esquecer como fiz isso, não tenha que pesquisar tudo de novo peça por peça e compilar junto, escrevo essas folhas de cola para mim mesmo.
Disclaimer: 1. « », best practice . « » .
2. , , , — .
Jenkins
Prevejo sua pergunta: o que a atualização dinâmica do trabalho tem a ver com isso? Insirai o valor do parâmetro string com as alças e vá em frente!
A resposta é: eu sou muito preguiçoso, não gosto quando as pessoas reclamam: Misha, a implantação está travando, tudo se foi! 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 completa possível. Se for possível evitar que o usuário insira dados diretamente fornecendo uma lista de valores para selecionar, então organizo a seleção.
O plano é o seguinte: criar um job no Jenkins, no qual, antes do lançamento, seria possível selecionar uma versão da lista, especificar valores para os parâmetros passados ao container via ENV , em seguida coletar o container e enviá-lo ao Container Registry. Mais adiante, o contêiner é lançado no kubera comocarga de trabalho com parâmetros especificados no trabalho.
Não consideraremos o processo de criação e configuração de um trabalho no Jenkins, isso é off-topic. Partiremos do pressuposto de que a tarefa está pronta. Para implementar uma lista de versão atualizável, precisamos de duas coisas: uma lista de origem existente com números de versão válidos a priori e uma variável do tipo de parâmetro Choice na tarefa. Em nosso exemplo, deixe a variável ser nomeada BUILD_VERSION , não vamos nos alongar sobre ela em detalhes. Mas vamos dar uma olhada mais de perto na lista de fontes.
Não existem tantas opções. Dois imediatamente me ocorreram:
- Use a API de acesso remoto que o Jenkins oferece a seus usuários;
- Consulte o conteúdo da pasta do repositório remoto (em nosso caso, é JFrog Artifactory, o que não é importante).
API de acesso remoto Jenkins
De acordo com a boa tradição estabelecida, prefiro evitar longas explicações.
Só me permitirei traduzir livremente um trecho do primeiro parágrafo da primeira página da documentação da API :
O Jenkins fornece uma API para acesso remoto legível por máquina à sua funcionalidade. <...> O acesso remoto é oferecido em um estilo semelhante ao REST. Isso significa que não há um único ponto de entrada para todos os recursos e, em vez disso, um URL como " ... / api / " é usado, onde " ... " é o objeto ao qual os recursos da API são aplicados.Em outras palavras, se a tarefa de implantação, da qual estamos falando no momento, está disponível no endereço
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build
, então os apitos da API para essa tarefa estão disponíveis em
Avançar, temos a opção de como receber a saída. Vamos nos deter no XML, já que a API só permite a filtragem neste caso.
Vamos apenas tentar obter uma lista de todas as execuções de trabalho. Estamos interessados apenas no nome do assembly ( displayName ) e seu resultado ( resultado ):http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/
http://jenkins.mybuild.er/view/AweSomeApp/job/AweSomeApp_build/api/xml?tree=allBuilds[displayName,result]
Aconteceu?
Agora, vamos filtrar apenas aqueles lançamentos que terminam com um resultado de SUCESSO . Usamos o argumento & exclude e passamos a ele o caminho para um valor diferente de SUCCESS como parâmetro . Sim Sim. A dupla negação é 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 sucesso

Bem, apenas por uma questão de autoindulgência, vamos ter certeza de que o filtro não nos enganou (os filtros nunca mentem!) E exiba 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 malsucedidos

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 da chamada da API Jenkins. Bem, porque se o aplicativo foi criado com sucesso, ele foi empacotado e colocado no repositório na pasta apropriada. Da mesma forma, o repositório é o repositório padrão de versões funcionais de aplicativos. Gostar. Bem, vamos perguntar a ele quais versões estão armazenadas. Vamos curl, grep e awk na pasta remota. Se alguém estiver interessado no unliner, ele está sob o spoiler.
Comando de uma linha
: , , . :
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[^/]+' )
Configuração de trabalho e arquivo de configuração de trabalho no Jenkins
Nós lidamos com a fonte da lista de versões. Agora vamos colocar a lista resultante na tarefa. Para mim, a solução óbvia era adicionar uma etapa no trabalho de construção do aplicativo. A etapa que seria executada se o resultado fosse "sucesso".
Abra as configurações de tarefa de montagem e role até o final. Clique nos botões: Adicionar etapa de compilação -> Etapa condicional (única) . Nas configurações da etapa, selecione a condição de status de compilação atual , defina o valor SUCCESS , a ação a ser executada se o comando Executar shell for bem-sucedido .
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 no local de onde foi retirado.
Lembre-se, concordamos acima que iremos criar um parâmetro BUILD_VERSION para a lista de versões ?
Vamos baixar o arquivo de configuração e dar uma olhada dentro dele. Apenas para ter certeza de que o parâmetro está correto e é realmente o tipo certo.
Captura de tela sob o spoiler.
Seu snippet config.xml deve ter a mesma aparência. Exceto que o conteúdo do elemento de escolhas ainda não está presente

Você está convencido? Tudo bem, estamos escrevendo um script que será executado no caso de uma construção bem-sucedida.
O script receberá uma lista de versões, fará download de um arquivo de configuração, escreverá uma lista de versões no local de que precisamos e a colocará de volta. Sim. Está tudo correto. Escreva uma lista de versões em XML para o 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 alguns amantes ferozes de expressões regulares no mundo. Eu não pertenço a eles. Por favor instalar xmlstarler na máquina onde a configuração será editada. Parece-me que este não é um preço alto a pagar por evitar a edição de XML com sed.
No spoiler, cito o código que executa toda a sequência descrita acima.
Escrevemos no config a lista de versões da pasta no 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 '%s\n' "${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ê gostou mais da opção de obter versões do Jenkins e é tão preguiçoso quanto eu, então no spoiler o mesmo código, mas a lista é do Jenkins:
Nós escrevemos uma lista de versões do Jenkins para a configuração
: , . , awk . .
#!/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 '%s\n' "${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á deve ter uma lista suspensa com as versões. Aqui está algo como a captura de tela sob o spoiler.
Lista de versões corretamente preenchida

Se tudo funcionou, copie e cole o script no comando Executar shell e salve as alterações.
Conexão de nuvem
Os coletores estão em nossos contêineres. Usamos Ansible como nosso gerenciador de configuração e entrega de aplicativos. Assim, 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 com Ansible ou construir contêineres no console da nuvem. Concordamos em não falar sobre plug-ins para Jenkins neste artigo. Lembrar?
Eu decidi: bem, já que os recipientes "fora da caixa" podem ser montados no console da nuvem, então por que cercar uma horta? Mantenha tudo limpo, certo? Quero construir contêineres com Jenkins no console da nuvem e, a partir daí, lançá-los no Kuber. Além disso, o Google tem canais muito ricos dentro da infraestrutura, o que terá um efeito benéfico na velocidade de implantação.
Duas coisas são necessárias para se conectar ao console da nuvem: gcloude direitos de acesso à API do Google Cloud para a instância de VM da qual essa conexão será feita.
Para aqueles que não planejam se conectar a partir da nuvem do Google
. , *nix' .
, — . — .
, — . — .
A maneira mais fácil de conceder permissões é por meio da interface da web.
- Pare a instância de VM da qual você se conectará ao console da nuvem no futuro.
- Abra Detalhes da instância e clique em Editar .
- Na parte inferior da página, selecione o escopo de acesso da instância Acesso total a todas as APIs do Cloud .
Captura de tela
- Salve suas 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 foi bem-sucedida. Use o comando:
gcloud alpha cloud-shell ssh
Uma conexão bem-sucedida é algo assim

Implantar no GKE
Como estamos nos empenhando de todas as maneiras possíveis para mudar completamente para IaC (Infrastucture as a Code), armazenamos dockerfiles no gita. Isso é por um lado. Uma implantação em kubernetes é descrita por um arquivo yaml que é usado apenas por esta tarefa, que também é como um código. Isso está do outro lado. Em geral, quero dizer que o plano é o seguinte:
- Pegamos os valores das variáveis BUILD_VERSION e, opcionalmente, os valores das variáveis que serão passadas por ENV .
- Baixando dockerfile do gita.
- Gerando yaml para implantação.
- Faça upload de ambos os arquivos via scp para o console da nuvem.
- Crie um contêiner lá e envie-o para o Registro de contêineres
- Aplicamos o arquivo de implantação de carga ao Kuber.
Vamos ser mais específicos. Já que começamos a falar sobre ENV , suponha que precisamos passar os valores de dois parâmetros: PARAM1 e PARAM2 . Adicione sua tarefa para implantação, digite - String Parameter .
Captura de tela

Iremos gerar o yaml simplesmente redirecionando o echo para um arquivo. É claro que presume-se que você tenha PARAM1 e PARAM2 no dockerfile , que o nome do carregamento será awesomeapp e o contêiner montado com o aplicativo da versão especificada está no registro do contêiner ao longo do caminho gcr.io/awesomeapp/awesomeapp- $ BUILD_VERSION , onde $ BUILD_VERSION é apenas foi selecionado na lista suspensa.
Comandos de listagem
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
Depois de conectar usando gcloud alpha cloud-shell ssh ao agente Jenkins, o modo interativo não está disponível, portanto, enviamos comandos para o console da nuvem usando o parâmetro --command .
Limpamos a pasta inicial no console da nuvem a partir do dockerfile antigo:
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:~
Coletamos, marcamos e enviamos o contêiner para o registro do contêiner:
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 a seguir usam nomes fictícios para o cluster onde a implantação está ocorrendo ( awsm-cluster ) e o nome do projeto ( awesome-project ) 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"
Iniciamos a tarefa, abrimos a saída do console e esperamos ver uma construção de contêiner bem-sucedida.
Captura de tela

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

Ignorei deliberadamente a configuração do Ingress . Por um motivo simples: depois de configurá-lo para uma carga de trabalho com um determinado nome, ele permanecerá operacional, não importa quantas implantações com esse nome sejam realizadas. Bem, em geral, isso está um pouco além do escopo da história.
Em vez de conclusões
Todos os passos acima, provavelmente, não poderiam ter sido feitos, mas simplesmente instalei algum plugin para Jenkins, seu muuulon. Mas de alguma forma eu não gosto de plug-ins. Bem, mais precisamente, só recorro a eles por desespero.
E eu só gosto de pegar um tópico novo para mim. O texto acima também é uma forma de compartilhar as descobertas que fiz, resolvendo o problema descrito logo no início. Compartilhe com aqueles que, tipo, não são de forma alguma um lobo terrível em devops. Se minhas descobertas ajudarem pelo menos alguém, ficarei feliz.