O Kubernetes (K8s) tornou-se uma das plataformas mais usadas para hospedar contêineres Docker. O Kubernetes oferece recursos avançados de orquestração, recursos de rede, segurança integrada, gerenciamento de usuários, alta disponibilidade, gerenciamento de volume, um amplo ecossistema de ferramentas de suporte e muito mais.
Uma ferramenta de suporte é o Helm , que fornece funcionalidade de gerenciamento de pacotes para o Kubernetes. Os aplicativos implantados pelo Helm são definidos em gráficos e o Jenkins fornece um gráfico do Helm para implantar uma instância do Jenkins no Kubernetes.
Nesta postagem, você aprenderá como instalar uma instância Jenkins com Helm e conectar agentes para executar tarefas de construção.
Para acompanhar este post, você precisa de um cluster Kubernetes e do cliente Helm. Todos os principais provedores de nuvem oferecem clusters Kubernetes hospedados:
- AWS tem EKS
- O Azure tem AKS
- O Google Cloud tem GKE
Se você deseja executar um cluster Kubernetes de desenvolvimento em seu PC local, kind
permite criar e destruir clusters para teste. A postagem Criando clusters Kubernetes de teste com Kind
fornece instruções sobre como executar o Kubernetes localmente.
Você também deve ter o cliente Helm instalado. A documentação do Helm
fornece instruções de instalação.
Os gráficos Jenkins Helm são fornecidos em https://charts.jenkins.io. To make this chart repository available, run the following commands:
helm repo add jenkins https://charts.jenkins.io
helm repo update
Para implantar uma instância do Jenkins com as configurações padrão, execute o comando:
helm upgrade --install myjenkins jenkins/jenkins
O comando helm upgrade
é normalmente usado para atualizar uma versão existente. No entanto, o argumento --install
garante que a versão seja criada se ela não existir. Isso significa que helm upgrade --install
cria e atualiza uma versão, eliminando a necessidade de manipular comandos de instalação e atualização, dependendo se a versão existe ou não.
O nome da versão é myjenkins
, e o argumento final jenkins/jenkins
define o gráfico a ser instalado.
A saída é mais ou menos assim:
$ upgrade do helm --install myjenkins jenkins/jenkins
Release "myjenkins" does not exist. Installing it now. NAME: myjenkins LAST DEPLOYED: Thu jan 19 08:13:11 2023 NAMESPACE: default STATUS: deployed REVISION: 1 NOTES:
Get your 'admin' user password by running: kubectl exec --namespace default -it svc/myjenkins -c jenkins -- /bin/cat /run/secrets/chart-admin-password && echo
Get the Jenkins URL to visit by running these commands in the same shell: echo http://127.0.0.1:8080 kubectl --namespace default port-forward svc/myjenkins 8080:8080
Login with the password from step 1 and the username: admin
Configure security realm and authorization strategy
Use Jenkins Configuration as Code by specifying configScripts in your values.yaml file, see documentation: http:///configuration-as-code and examples: https://github.com/jenkinsci/configuration-as-code-plugin/tree/master/demos
For more information on running Jenkins on Kubernetes, visit: https://cloud.google.com/solutions/jenkins-on-container-engine
For more information about Jenkins Configuration as Code, visit: https://jenkins.io/projects/jcasc/
NOTE: Consider using a custom image with pre-installed plugins
O primeiro comando listado nas notas retorna a senha para o usuário admin
:
$ kubectl exec --namespace default -it svc/myjenkins -c jenkins -- /bin/cat /run/secrets/chart-admin-password && eco
O segundo comando listado nas notas estabelece um túnel para o serviço no cluster Kubernetes.
No Kubernetes, um serviço é um recurso que configura a rede do cluster para expor um ou mais pods. O tipo de serviço padrão é ClusterIP
, que expõe os pods por meio de um endereço IP privado. É esse endereço de IP privado que encapsulamos para obter acesso à IU da Web do Jenkins.
Um pod do Kubernetes é um recurso que hospeda um ou mais contêineres. Isso significa que a instância do Jenkins está sendo executada como um contêiner dentro de um pod:
$ kubectl --namespace default port-forward svc/myjenkins 8080:8080
Encaminhamento de 127.0.0.1:8080 -> 8080
Encaminhando de [::1]:8080 -> 8080
Após o túnel ser estabelecido, abra http://localhost:8080 em seu PC local e você será direcionado para a instância Jenkins no cluster Kubernetes. Faça login com o nome de usuário admin
e a senha retornada pelo primeiro comando.
Agora você tem uma instância funcional, embora básica, do Jenkins em execução no Kubernetes.
Acessar o Jenkins por meio de um túnel é útil para depuração, mas não é uma ótima experiência para um servidor de produção. Para acessar o Jenkins por meio de um endereço IP disponível publicamente, você deve substituir a configuração padrão definida no gráfico. Existem centenas de valores que podem ser definidos e a lista completa está disponível executando o comando:
helm show values jenkins/jenkins
Configurar o serviço que expõe o pod Jenkins como um LoadBalancer
é a maneira mais fácil de acessar o Jenkins publicamente.
Um serviço do tipo LoadBalancer
expõe pods por meio de um endereço IP público. Exatamente como esse endereço IP público é criado é deixado para o cluster. Por exemplo, plataformas Kubernetes hospedadas como EKS, AKS e GKE criam um balanceador de carga de rede para direcionar o tráfego para o cluster K8s.
Observe que os serviços LoadBalancer
requerem configuração adicional ao usar um cluster Kubernetes de teste local, como os clusters criados por tipo. Consulte a documentação do tipo para obter mais informações.
Para configurar o serviço como um LoadBalancer
, crie um arquivo chamado values.yaml
com o seguinte conteúdo:
controlller:
serviceType: LoadBalancer
Em seguida, atualize a versão do Helm usando os valores definidos em values.yaml
com o comando:
helm upgrade --install -f values.yaml myjenkins jenkins/jenkins
A saída é alterada sutilmente com a adição de novas instruções para retornar o IP público do serviço:
$ helm upgrade --install -f values.yaml myjenkins jenkins/jenkins Release "myjenkins" has been upgraded. Happy Helming! NAME: myjenkins LAST DEPLOYED: Thu jan 19 08:45:23 2023 NAMESPACE: default STATUS: deployed REVISION: 4 NOTES:
Get your 'admin' user password by running: kubectl exec --namespace default -it svc/myjenkins -c jenkins -- /bin/cat /run/secrets/chart-admin-password && echo
Get the Jenkins URL to visit by running these commands in the same shell: NOTE: It may take a few minutes for the LoadBalancer IP to be available. You can watch the status of by running 'kubectl get svc --namespace default -w myjenkins' export SERVICE_IP=$(kubectl get svc --namespace default myjenkins --template "{{ range (index .status.loadBalancer.ingress 0) }}{{ . }}{{ end }}") echo http://$SERVICE_IP:8080/login
Login with the password from step 1 and the username: admin
Configure security realm and authorization strategy
Use Jenkins Configuration as Code by specifying configScripts in your values.yaml file, see documentation: http:///configuration-as-code and examples: https://github.com/jenkinsci/configuration-as-code-plugin/tree/master/demos
For more information on running Jenkins on Kubernetes, visit: https://cloud.google.com/solutions/jenkins-on-container-engine
For more information about Jenkins Configuration as Code, visit: https://jenkins.io/projects/jcasc/
NOTE: Consider using a custom image with pre-installed plugins
Usando as novas instruções na etapa 2, execute o seguinte comando para obter o endereço IP público ou o nome do host do serviço:
kubectl get svc --namespace default myjenkins --template "{{ range (index .status.loadBalancer.ingress 0) }}{{ . }}{{ end }}"
Implantei o Jenkins em um cluster EKS e este é o resultado do comando para minha infraestrutura:
$ kubectl get svc --namespace default myjenkins --template "{{ range (index .status.loadBalancer.ingress 0) }}{{ . }}{{ end }}" a84aa6226d6e5496882cfafdd6564a35-901117307.us-west-1.elb.amazonaws.com
Para acessar o Jenkins, abra http://service_ip_or_hostname:8080 .
Você pode perceber que o Jenkins relata o seguinte erro ao acessá-lo por meio de seu endereço IP público:
It appears that your reverse proxy set up is broken.
Isso pode ser resolvido definindo a URL pública na controller.jenkinsUrlpropriedade, substituindo a84aa6226d6e5496882cfafdd6564a35-901117307.us-west-1.elb.amazonaws.compelo endereço IP ou nome do host da sua instância do Jenkins:
controller:
jenkinsUrl: http://a84aa6226d6e5496882cfafdd6564a35-901117307.us-west-1.elb.amazonaws.com:8080/
Liste todos os plug-ins adicionais a serem instalados no controller.additionalPlugins
array:
controller:
additionalPlugins:
- octopusdeploy:3.1.6
O ID e a versão do plug-in são encontrados no site do plug-in do Jenkins
:
Essa abordagem é conveniente, mas a desvantagem é que a instância do Jenkins precisa entrar em contato com o site de atualização do Jenkins para recuperá-los como parte da primeira inicialização.
Uma abordagem mais robusta é baixar os plug-ins como parte de uma imagem personalizada, o que garante que os plug-ins sejam inseridos na imagem do Docker. Ele também permite que ferramentas adicionais sejam instaladas no controlador Jenkins. A postagem anterior
contém detalhes sobre como criar e publicar imagens personalizadas do Docker.
Observe que a imagem personalizada do Docker deve ter os seguintes plug-ins instalados, além dos plug-ins personalizados. Esses plug-ins são necessários para o gráfico do Helm funcionar corretamente:
- kubernetes
- workflow-aggregator
- git
- configuration-as-code
FROM jenkins/jenkins:lts-jdk11
USER root
RUN apt update && \
apt install -y --no-install-recommends gnupg curl ca-certificates apt-transport-https && \
curl -sSfL https://apt.octopus.com/public.key | apt-key add - && \
sh -c "echo deb https://apt.octopus.com/ stable main > /etc/apt/sources.list.d/octopus.com.list" && \
apt update && apt install -y octopuscli
RUN jenkins-plugin-cli --plugins octopusdeploy:3.1.6 kubernetes:1.29.2 workflow-aggregator:2.6 git:4.7.1 configuration-as-code:1.52
USER jenkins
To use the custom image, you define it in the values.yml
with the following properties. This example uses the custom Jenkins image pushed to my DockerHub account:
controller:
image: "docker.io/mcasperson/myjenkins"
tag: "latest"
installPlugins: false
Para usar a imagem personalizada, defina-a no values.yml
com as seguintes propriedades. Este exemplo usa a imagem personalizada do Jenkins enviada para minha conta do DockerHub
:
controller:
JCasC:
configScripts:
this-is-where-i-configure-the-executors: |
jenkins:
numExecutors: 5
Você pode encontrar um exemplo Dockerfilede instalação de ferramentas para Java, DotNET Core, PHP, Python, Ruby e Go no repositório jenkins-complete-image
.
Jenkins Configuration as Code (JCasC) é um plug
-in que fornece um método opinativo para configurar o Jenkins por meio de arquivos YAML. Isso fornece uma alternativa para escreverscripts Groovy
referenciando diretamente a API Jenkins
, que é poderosa, mas requer que os administradores se sintam à vontade para escrever código.
JCasC é definido sob a controller.JCasC.configScript
propriedade. As chaves filhas abaixo configScript
têm nomes de sua escolha que consistem em letras minúsculas, números e hífens e servem como uma forma de resumir o bloco de texto que definem.
O valor atribuído a essas chaves são strings de várias linhas, que por sua vez definem um arquivo JCasC YAML. O caractere pipe ( |) fornece um método conveniente para definir uma string de várias linhas, mas não é significativo.
O resultado final dá a aparência de um documento YAML contínuo. Lembre-se de que o conteúdo que aparece após o caractere de barra vertical é simplesmente um valor de texto de várias linhas que também é YAML.
O exemplo abaixo configura o número de executores disponíveis para o controlador Jenkins, com o JCasC YAML definido sob uma chave exagerada chamada this-is-where-i-configure-the-executors
para reforçar o fato de que essas chaves podem ter qualquer nome:
controller:
JCasC:
configScripts:
this-is-where-i-configure-the-executors: |
jenkins:
numExecutors: 5
Para comparação, a mesma configuração também pode ser obtida com o seguinte script Groovy salvo como /usr/share/jenkins/ref/init.groovy.d/executors.groovy
uma imagem personalizada do Docker:
import jenkins.model.*
Jenkins.instance.setNumExecutors(5)
Mesmo este exemplo simples destaca os benefícios do JCasC:
- Cada propriedade JCasC é documentada em http://jenkinshost/configuration-as-code/reference (substitua jenkinshostpelo nome do host de sua própria instância Jenkins), enquanto escrever um script Groovy requer conhecimento da API Jenkins .
- A configuração JCasC é YAML vanilla, que é muito mais acessível do que scripts escritos em Groovy.
- JCasC é opinativo, fornecendo uma abordagem consistente para configuração comum. Os scripts Groovy podem resolver o mesmo problema de várias maneiras, o que significa que scripts com mais do que algumas linhas de código exigem a experiência de um engenheiro de software para serem compreendidos.
Apesar de todos os benefícios, o JCasC não é um substituto completo para definir as propriedades do sistema ou executar scripts Groovy. Por exemplo, o JCasC não suporta a capacidade de desabilitar o CSRF , o que significa que essa opção é exposta apenas por meio das propriedades do sistema.
Os volumes no Kubernetes são um pouco mais complicados do que os encontrados no Docker normal porque os volumes do K8s tendem a ser hospedados fora do nó que executa o pod. Isso ocorre porque os pods podem ser realocados entre nós e, portanto, precisam acessar volumes de qualquer nó.
Para complicar, ao contrário dos volumes do Docker, apenas os volumes especializados do Kubernetes podem ser compartilhados entre os pods. Esses volumes compartilhados são chamados de ReadWriteManyvolumes. Normalmente, porém, um volume do Kubernetes é usado apenas por um único pod e é chamado de ReadWriteOnce
volumes.
O gráfico Jenkins Helm configura um ReadWriteOnce
volume para hospedar o diretório inicial do Jenkins. Como esse volume só pode ser acessado pelo pod no qual está montado, todas as operações de backup devem ser executadas por esse pod.
Felizmente, o gráfico Helm oferece opções abrangentes de backup
, com a capacidade de realizar backups e salvá-los em provedores de armazenamento em nuvem`
No entanto, você pode orquestrar backups simples e independentes da nuvem com dois comandos.
O primeiro comando é executado tardentro do pod para fazer backup do /var/jenkins_hom
e diretório no /tmp/backup.tar.gz
arquivo. Observe que o nome do pod myjenkins-0
é derivado do nome da versão do Helm myjenkins:
kubectl
exec
-c jenkins myjenkins-0 -- tar czf /tmp/backup.tar.gz /var/jenkins_home
O segundo comando copia o arquivo de backup do pod para sua máquina local:
kubectl cp -c jenkins myjenkins-0:/tmp/backup.tar.gz ./backup.tar.gz
Neste ponto, backup.tar.gzpode ser copiado para um local mais permanente.
Além de instalar o Jenkins em um cluster Kubernetes, você também pode criar agentes Jenkins dinamicamente no cluster. Esses agentes são criados quando novas tarefas são agendadas no Jenkins e são automaticamente limpos após a conclusão das tarefas.
As configurações padrão para agentes são definidas na agent
propriedade no values.yaml
arquivo. O exemplo abaixo define um agente com o rótulo Jenkins default
, criado em pods prefixados com o nome default
, e com limites de CPU e memória:
agent:
podName: default
customJenkinsLabels: default
resources:
limits:
cpu: "1"
memory: "2048Mi"
Agentes mais especializados são definidos sob a additionalAgents
propriedade. Esses modelos de pod herdam os valores daqueles definidos na agent
propriedade.
O exemplo abaixo define um segundo modelo de pod alterando o nome do pod e os rótulos Jenkins para maven
e especificando uma nova imagem do Docker jenkins/jnlp-agent-maven:latest
:
agent:
podName: default
customJenkinsLabels: default
resources:
limits:
cpu: "1"
memory: "2048Mi"
additionalAgents:
maven:
podName: maven
customJenkinsLabels: maven
image: jenkins/jnlp-agent-maven
tag: latest
Para encontrar as definições do agente, navegue até Manage Jenkins , Manage Nodes and Clouds e, finalmente, Configure Clouds .
Para usar os agentes para executar um pipeline, defina o, agent
block like this:
pipeline {
agent {
kubernetes {
inheritFrom 'maven'
}
}
// ...
}
Por exemplo, este pipeline para um aplicativo Java usa o maven
agent template:
pipeline {
// This pipeline requires the following plugins:
// * Pipeline Utility Steps Plugin: https://wiki.jenkins.io/display/JENKINS/Pipeline+Utility+Steps+Plugin
// * Git: https://plugins.jenkins.io/git/
// * Workflow Aggregator: https://plugins.jenkins.io/workflow-aggregator/
// * Octopus Deploy: https://plugins.jenkins.io/octopusdeploy/
// * JUnit: https://plugins.jenkins.io/junit/
// * Maven Integration: https://plugins.jenkins.io/maven-plugin/
parameters {
string(defaultValue: 'Spaces-1', description: '', name: 'SpaceId', trim: true)
string(defaultValue: 'SampleMavenProject-SpringBoot', description: '', name: 'ProjectName', trim: true)
string(defaultValue: 'Dev', description: '', name: 'EnvironmentName', trim: true)
string(defaultValue: 'Octopus', description: '', name: 'ServerId', trim: true)
}
tools {
jdk 'Java'
}
agent {
kubernetes {
inheritFrom 'maven'
}
}
stages {
stage('Environment') {
steps {
echo "PATH = ${PATH}"
}
}
stage('Checkout') {
steps {
// If this pipeline is saved as a Jenkinsfile in a git repo, the checkout stage can be deleted as
// Jenkins will check out the code for you.
script {
/*
This is from the Jenkins "Global Variable Reference" documentation:
SCM-specific variables such as GIT_COMMIT are not automatically defined as environment variables; rather you can use the return value of the checkout step.
*/
def checkoutVars = checkout([$class: 'GitSCM', branches: [[name: '*/master']], userRemoteConfigs: [[url: 'https://github.com/mcasperson/SampleMavenProject-SpringBoot.git']]])
env.GIT_URL = checkoutVars.GIT_URL
env.GIT_COMMIT = checkoutVars.GIT_COMMIT
env.GIT_BRANCH = checkoutVars.GIT_BRANCH
}
}
}
stage('Dependencies') {
steps {
// Download the dependencies and plugins before we attempt to do any further actions
sh(script: './mvnw --batch-mode dependency:resolve-plugins dependency:go-offline')
// Save the dependencies that went into this build into an artifact. This allows you to review any builds for vulnerabilities later on.
sh(script: './mvnw --batch-mode dependency:tree > dependencies.txt')
archiveArtifacts(artifacts: 'dependencies.txt', fingerprint: true)
// List any dependency updates.
sh(script: './mvnw --batch-mode versions:display-dependency-updates > dependencieupdates.txt')
archiveArtifacts(artifacts: 'dependencieupdates.txt', fingerprint: true)
}
}
stage('Build') {
steps {
// Set the build number on the generated artifact.
sh '''
./mvnw --batch-mode build-helper:parse-version versions:set \
-DnewVersion=\\${parsedVersion.majorVersion}.\\${parsedVersion.minorVersion}.\\${parsedVersion.incrementalVersion}.${BUILD_NUMBER}
'''
sh(script: './mvnw --batch-mode clean compile', returnStdout: true)
script {
env.VERSION_SEMVER = sh (script: './mvnw -q -Dexec.executable=echo -Dexec.args=\'${project.version}\' --non-recursive exec:exec', returnStdout: true)
env.VERSION_SEMVER = env.VERSION_SEMVER.trim()
}
}
}
stage('Test') {
steps {
sh(script: './mvnw --batch-mode -Dmaven.test.failure.ignore=true test')
junit(testResults: 'target/surefire-reports/*.xml', allowEmptyResults : true)
}
}
stage('Package') {
steps {
sh(script: './mvnw --batch-mode package -DskipTests')
}
}
stage('Repackage') {
steps {
// This scans through the build tool output directory and find the largest file, which we assume is the artifact that was intended to be deployed.
// The path to this file is saved in and environment variable called JAVA_ARTIFACT, which can be consumed by subsequent custom deployment steps.
script {
// Find the matching artifacts
def extensions = ['jar', 'war']
def files = []
for(extension in extensions){
findFiles(glob: 'target/**.' + extension).each{files << it}
}
echo 'Found ' + files.size() + ' potential artifacts'
// Assume the largest file is the artifact we intend to deploy
def largestFile = null
for (i = 0; i < files.size(); ++i) {
if (largestFile == null || files[i].length > largestFile.length) {
largestFile = files[i]
}
}
if (largestFile != null) {
env.ORIGINAL_ARTIFACT = largestFile.path
// Create a filename based on the repository name, the new version, and the original file extension.
env.ARTIFACTS = "SampleMavenProject-SpringBoot." + env.VERSION_SEMVER + largestFile.path.substring(largestFile.path.lastIndexOf("."), largestFile.path.length())
echo 'Found artifact at ' + largestFile.path
echo 'This path is available from the ARTIFACTS environment variable.'
}
}
// Octopus requires files to have a specific naming format. So copy the original artifact into a file with the correct name.
sh(script: 'cp ${ORIGINAL_ARTIFACT} ${ARTIFACTS}')
}
}
stage('Deployment') {
steps {
octopusPushPackage(additionalArgs: '', packagePaths: env.ARTIFACTS.split(":").join("\n"), overwriteMode: 'OverwriteExisting', serverId: params.ServerId, spaceId: params.SpaceId, toolId: 'Default')
octopusPushBuildInformation(additionalArgs: '', commentParser: 'GitHub', overwriteMode: 'OverwriteExisting', packageId: env.ARTIFACTS.split(":")[0].substring(env.ARTIFACTS.split(":")[0].lastIndexOf("/") + 1, env.ARTIFACTS.split(":")[0].length()).replace("." + env.VERSION_SEMVER + ".zip", ""), packageVersion: env.VERSION_SEMVER, serverId: params.ServerId, spaceId: params.SpaceId, toolId: 'Default', verboseLogging: false, gitUrl: env.GIT_URL, gitCommit: env.GIT_COMMIT, gitBranch: env.GIT_BRANCH)
octopusCreateRelease(additionalArgs: '', cancelOnTimeout: false, channel: '', defaultPackageVersion: '', deployThisRelease: false, deploymentTimeout: '', environment: params.EnvironmentName, jenkinsUrlLinkback: false, project: params.ProjectName, releaseNotes: false, releaseNotesFile: '', releaseVersion: env.VERSION_SEMVER, serverId: params.ServerId, spaceId: params.SpaceId, tenant: '', tenantTag: '', toolId: 'Default', verboseLogging: false, waitForDeployment: false)
octopusDeployRelease(cancelOnTimeout: false, deploymentTimeout: '', environment: params.EnvironmentName, project: params.ProjectName, releaseVersion: env.VERSION_SEMVER, serverId: params.ServerId, spaceId: params.SpaceId, tenant: '', tenantTag: '', toolId: 'Default', variables: '', verboseLogging: false, waitForDeployment: true)
}
}
}
}
Você pode confirmar que o agente foi criado no cluster durante a execução da tarefa executando:
kubectl get pods
No exemplo abaixo, o pod java-9-k0hmj-vcvdz-wknh4
está em processo de criação para executar o exemplo de pipeline acima:
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
java-9-k0hmj-vcvdz-wknh4 0/1 ContainerCreating 0 1s
myjenkins-0 2/2 Running 0 49m
Hospedar Jenkins e seus agentes em um cluster Kubernetes permite que você crie uma plataforma de construção escalável e responsiva, criando e destruindo agentes em tempo real para lidar com cargas de trabalho elásticas. E graças ao gráfico Jenkins Helm, instalar Jenkins e configurar os nós requer apenas algumas linhas de YAML.
Neste post você aprendeu como:
Nesta postagem você aprendeu como:
- Implantar Jenkins no Kubernetes
- Expor Jenkins em um endereço IP público
- Instalar plug-ins adicionais como parte do processo de instalação
- Configurar o Jenkins por meio do JCasC
- Faça backup do diretório inicial do Jenkins
- Crie agentes do Kubernetes que são criados e destruídos conforme necessário
Confira nossos outros posts sobre a instalação do Jenkins: