fbpx
Publicado em: segunda-feira, 18 de nov de 2024
A anatomia de um JenkinsFile para projeto .NET

Segundo a JetBrains, o Jenkins continua relevante, ocupando a primeira posição na categoria Team Tools > Continuous integration (CI) system, portanto, mesmo não envelhecendo muito bem, por mais caquético que pareça, continua ocupando 50% do mercado corporativo.

https://www.jetbrains.com/lp/devecosystem-2023/team-tools/#ci_tools

Vale lembrar que a JetBrains possui o IntelliJ IDEA, uma IDE para Java, assim como o RIDER, o que coloca em sua base de clientes fiéis muita gente do mercado java, e, portanto, interferindo significativamente produzindo tendências em gráficos desse tipo de ferramenta. Na ausência de dados mais elaborados, dou-me por convencido pelo meu próprio viés, pela minha própria experiência com Jenkins. Assim, parto desse princípio.

Jenkins

Jenkins é uma ferramenta amplamente reconhecida no mercado para automação de tarefas relacionadas à integração e entrega contínua (CI/CD). Inicialmente um fork do Hudson, sua história remonta ao início da década de 2010, quando a aquisição da SUN pela Oracle desencadeou a sua separação. Desde então, o Jenkins se consolidou como uma das soluções mais utilizadas para gerenciar pipelines de automação em equipes de desenvolvimento.

Com o Jenkins, é possível configurar tarefas de forma agendada ou baseadas em eventos, sendo os mais comuns os gatilhos provenientes de repositórios Git, como push, pull request (PR) ou a criação de novas branches. Embora seja tecnicamente possível executar pipelines sem uma relação direta com repositórios Git, essa é uma abordagem menos comum e muitas vezes específica a cenários de nicho.

Jenkins Pipelines

A introdução dos Jenkins Pipelines foi uma das inovações mais marcantes da ferramenta, trazendo uma abordagem mais robusta e flexível para gerenciar tarefas automatizadas. Com o conceito de pipelines como código, o Jenkins possibilita definir toda a lógica de execução de tarefas em arquivos chamados de Jenkinsfile, escritos em uma DSL (Domain-Specific Language) baseada em Groovy.

Os Jenkins Pipelines possuem dois tipos principais:

  1. Pipeline Declarativo
    Este é o modelo mais simples e intuitivo, com uma estrutura mais rígida e clara. Ele é ideal para equipes que buscam padronização e menor curva de aprendizado, permitindo configurações organizadas e legíveis.
  2. Pipeline Scriptado
    Oferece maior flexibilidade e controle, mas com uma curva de aprendizado mais acentuada. Nesse modelo, é possível criar pipelines altamente customizados, porém à custa de maior complexidade.

Exemplo básico de um pipeline declarativo:

pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                echo 'Building...'
            }
        }
        stage('Test') {
            steps {
                echo 'Testing...'
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying...'
            }
        }
    }
}

Esse pipeline básico representa uma sequência clássica de build, test e deploy, que pode ser estendida para atender cenários mais complexos.

Jenkins Pipelines com Docker

A combinação do Jenkins com o Docker eleva as capacidades de automação a outro nível. O Docker permite que os pipelines sejam executados em ambientes isolados, garantindo consistência e previsibilidade em cada execução. Além disso, ao integrar Docker diretamente em um Jenkinsfile, é possível definir imagens específicas para cada estágio do pipeline, garantindo que todas as dependências estejam prontas sem necessidade de configuração manual no ambiente do Jenkins.

Até aqui você deve estar pensando: Mas o GitHub Actions já faz isso, não?

Não com cache de imagens docker e não com cache de pacotes nuget.

É aqui que ganhamos muitos minutos a cada build.

Exemplo de Jenkins Pipeline com Docker:

pipeline {
    agent {
        docker {
            image 'mcr.microsoft.com/dotnet/sdk:8.0'
        }
    }
    stages {
        stage('Build') {
            steps {
                sh 'dotnet build'
            }
        }
        stage('Test') {
            steps {
                sh 'dotnet test'
            }
        }
        stage('Docker Build') {
            steps {
                script {
                    docker.build('my-application-image')
                }
            }
        }
    }
}

Nesse exemplo:

  • O pipeline utiliza uma imagem Docker do SDK do .NET 8 para o estágio de build e testes, garantindo que a ferramenta e o ambiente estejam corretamente configurados.
  • A imagem Docker da aplicação é construída no estágio “Docker Build”, permitindo que seja reutilizada para testes locais ou envio a um repositório de imagens, como o Docker Hub.

Benefícios dessa abordagem:

  1. Consistência: Cada estágio usa imagens Docker específicas, reduzindo problemas de incompatibilidade.
  2. Escalabilidade: Jenkins pode distribuir tarefas entre diferentes agentes Docker.
  3. Isolamento: Ambientes independentes para cada execução garantem que problemas em uma tarefa não afetem outras.

Jenkins Pipelines e GitHub Organization Plugin: Integração e Praticidade

A integração entre Jenkins Pipelines e o GitHub Organization Plugin é uma combinação poderosa para equipes que utilizam GitHub como principal plataforma de gerenciamento de código-fonte. Esse plugin automatiza a descoberta e a configuração de repositórios GitHub, tornando o processo de criação e manutenção de pipelines mais ágil e eficiente.

Com o GitHub Organization Plugin, o Jenkins pode conectar-se diretamente a uma organização no GitHub, detectando automaticamente todos os repositórios que possuem um arquivo Jenkinsfile. Isso elimina a necessidade de configurar manualmente pipelines individuais para cada projeto. Quando um novo repositório é adicionado à organização e contém um Jenkinsfile, o plugin detecta automaticamente e cria o pipeline correspondente no Jenkins. Além disso, quaisquer alterações no pipeline (como novos estágios ou configurações) são automaticamente aplicadas nas próximas execuções.

Benefícios dessa integração:

  1. Automação Completa na Detecção de Repositórios
    Equipes não precisam gastar tempo adicionando manualmente cada novo repositório ao Jenkins. O plugin faz isso automaticamente, promovendo maior eficiência.
  2. Padronização de Pipelines
    Como o Jenkinsfile reside no repositório de código, cada projeto pode ter seu pipeline gerenciado de forma independente, enquanto segue os padrões da equipe.
  3. Redução de Sobrecarga Operacional
    A integração remove o esforço de manutenção manual, especialmente em organizações com dezenas ou centenas de repositórios. A lógica do pipeline é definida diretamente no código e ajustada conforme necessário.
  4. Suporte Nativo ao GitHub
    O plugin utiliza APIs do GitHub para garantir integração contínua, incluindo autenticação via tokens pessoais ou OAuth, simplificando a configuração de permissões.
  5. Detecção de Mudanças em Repositórios
    Modificações em branches ou novos pull requests disparam automaticamente os pipelines relevantes, garantindo que o código seja sempre validado de acordo com as regras definidas pela equipe.
  6. Facilidade na Colaboração entre Times
    Ao centralizar a lógica dos pipelines no código dos repositórios, fica mais fácil para os times de desenvolvimento e DevOps colaborarem e ajustarem processos de CI/CD.

Um exemplo de caso prático

Imagine uma organização com múltiplos microservices armazenados em diferentes repositórios GitHub. Antes do GitHub Organization Plugin, a configuração de pipelines para cada repositório seria um processo manual e propenso a erros. Com a integração, basta configurar o plugin para a organização no GitHub, e ele automaticamente gera pipelines para cada repositório com um Jenkinsfile. Novos microservices podem ser adicionados sem qualquer intervenção no Jenkins, reduzindo significativamente o tempo gasto em configuração.

Esse nível de automação, aliado à flexibilidade dos Jenkins Pipelines, torna essa integração uma escolha óbvia para equipes que priorizam produtividade e escalabilidade em seus fluxos de trabalho de CI/CD.

Definindo Dockerfiles customizados pora o Jenkins Pipelines

Uma das funcionalidades mais poderosas ao combinar Jenkins Pipelines e Docker é a possibilidade de definir e utilizar Dockerfiles customizados para os agentes que executarão as etapas do pipeline. Essa abordagem permite criar ambientes totalmente personalizados, ajustados às necessidades específicas de cada projeto ou etapa do processo de CI/CD.

Em vez de depender apenas de imagens Docker pré-construídas, o Jenkins pode utilizar um Dockerfile customizado, armazenado no repositório do projeto, para criar imagens sob demanda. Isso garante que todas as dependências, ferramentas, configurações e versões necessárias estejam disponíveis, reduzindo a possibilidade de erros relacionados a inconsistências de ambiente.

Como Funciona na Prática

No Jenkinsfile, podemos definir um agente Docker que constrói a imagem a partir de um Dockerfile customizado. Por exemplo:

pipeline {
    agent {
        dockerfile {
            filename 'Dockerfile'
            dir 'docker'
        }
    }
    stages {
        stage('Build') {
            steps {
                sh 'mvn clean package'
            }
        }
        stage('Test') {
            steps {
                sh 'mvn test'
            }
        }
    }
}

Nesse exemplo:

  • O filename especifica o nome do Dockerfile (caso não seja o padrão Dockerfile).
  • A opção dir indica o diretório onde o Dockerfile está localizado, permitindo organizar os arquivos em uma estrutura de pastas clara.

Benefícios de Utilizar Dockerfiles Customizados

  1. Ambientes sob medida
    Você pode configurar o ambiente exato necessário para seu projeto, incluindo ferramentas específicas, bibliotecas ou configurações personalizadas que não estão disponíveis em imagens Docker padrão.
  2. Controle total de versões
    Como o Dockerfile reside no repositório, qualquer alteração ou ajuste no ambiente está versionado junto com o código, garantindo rastreabilidade e controle.
  3. Redução de problemas de compatibilidade
    Personalizar o ambiente elimina discrepâncias entre máquinas de desenvolvedores e servidores de CI, garantindo que o mesmo ambiente seja usado em todas as execuções.
  4. Facilidade de compartilhamento entre equipes
    Outros times podem reutilizar o Dockerfile customizado, garantindo consistência nos ambientes de diferentes projetos.
  5. Suporte a dependências complexas
    Projetos com requisitos específicos, como bibliotecas nativas, versões específicas de ferramentas ou configurações de runtime otimizadas, podem ser facilmente suportados.

Um Caso de Uso Típico

Imagine um projeto que utiliza uma versão específica do .NET e queira usar SonarQube para analisar seu código. Em vez de usar uma imagem padrão do você pode criar um Dockerfile como este:

FROM mcr.microsoft.com/dotnet/sdk:8.0
RUN export PATH="$PATH:/root/.dotnet/tools"
RUN dotnet tool install --global dotnet-sonarscanner
RUN dotnet tool install --global dotnet-coverage

# Default to UTF-8 file.encoding
ENV LANG C.UTF-8

# Install docker (cli and engine)
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh

#ENV JAVA_HOME /usr/local/openjdk-17
#ENV PATH $JAVA_HOME/bin:$PATH

# Install OpenJDK to run sonar scanner
RUN apt-get update && \
apt-get install -y --no-install-recommends openjdk-17-jdk

Esse Dockerfile garante que o ambiente de build inclua todas as dependências necessárias para rodar o scan do sonar. No Jenkins Pipeline, basta apontar para esse Dockerfile, e a imagem será criada automaticamente antes da execução do pipeline.


Flexibilidade e Escalabilidade com Dockerfiles Customizados

A possibilidade de definir Dockerfiles customizados nos Jenkins Pipelines combina o melhor do mundo da automação com a flexibilidade dos containers. Essa abordagem é especialmente valiosa em ambientes corporativos, onde a consistência e a previsibilidade são pilares para entregar software com qualidade e rapidez. Com ela, o Jenkins se torna uma ferramenta ainda mais poderosa para gerenciar pipelines complexos e dinâmicos, sem sacrificar a eficiência.

Jenkins: Relevância e Modernização

Embora o Jenkins possa parecer uma ferramenta “legada” para alguns, sua flexibilidade e ampla adoção ainda fazem dele uma escolha sólida em muitos cenários corporativos. A combinação de pipelines como código com containers Docker é uma das estratégias que garantem sua relevância contínua, mesmo em um mercado competitivo com ferramentas mais modernas como GitHub Actions e GitLab CI.

Se você deseja modernizar seus pipelines ou integrar Jenkins a um stack de DevOps moderno com Kubernetes e Docker, o Jenkins continua sendo uma escolha poderosa, especialmente em projetos de larga escala.

Mais exemplos

def projetcs

pipeline {
    
    agent none

    environment {
        COMPOSE_PROJECT_NAME = "${env.JOB_NAME}-${env.BUILD_ID}"
    }
    stages {
    
        stage('Init') {

            agent any

            steps {
                // Configurações Iniciais
            }
        }
      
        stage('Build') {
            agent any
            steps {
                // Build de imagens docker
            }
        }

        stage('Test') {
            agent {
                dockerfile {
                    filename './sonar/Dockerfile'
                    args '-u root:root -v /var/run/docker.sock:/var/run/docker.sock -v /docker/nuget-cache:/root/.nuget/packages'
                }
            }
            when {
                branch 'main'
            }
            steps {
                // execução de testes e integração com SonarQube
            }
        }

        stage('Pack') {
            agent any
            when { buildingTag() }
            steps {
                // empacota uma imagem docker ou um pacote nugget
            }
        }

        stage('Publish') {
            agent any
            when { buildingTag() }
            steps {
                // envia para o registry ou nuget server
            }
        }
        
        stage('Deploy') {
            agent any
            when { buildingTag() }
            steps {
                 // implantga a aplicação, caso necessário
            }
        }

    }
}
#FROM mcr.microsoft.com/dotnet/sdk:5.0
FROM mcr.microsoft.com/dotnet/sdk:8.0

RUN export PATH="$PATH:/root/.dotnet/tools"
RUN dotnet tool install --global dotnet-sonarscanner
RUN dotnet tool install --global dotnet-coverage


# Default to UTF-8 file.encoding
ENV LANG C.UTF-8

# Install docker (cli and engine)
RUN curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh && rm get-docker.sh


#ENV JAVA_HOME /usr/local/openjdk-17

#ENV PATH $JAVA_HOME/bin:$PATH

RUN apt-get update && \
apt-get install -y --no-install-recommends openjdk-17-jdk

Nesse exemplo temos:

  • Init: onde para otimizar o fluxo, seto em uma variável a lista de projetos de informações importantes para cada projeto:
    • nome do projeto, usado para achar o diretório, ou csproj, ou dockerfile
    • image: nome da imagem que precisa ser gerada.
    • swarm-service: nesse exemplo é o serviço swarm que recebe esse projeto.
  • Build: produção das imagens docker
  • Test: execução dos testes integrados e testes unitários com integração com o sonarqube, e testcontainers.
  • Pack: revisão das tags e atualização das imagens
  • Publish: envio das imagens para o registry
  • Deploy: deploy em dev.
def projetcs

pipeline {
    
    agent none

    environment {
        COMPOSE_PROJECT_NAME = "${env.JOB_NAME}-${env.BUILD_ID}"
    }
    stages {
    
        stage('Init') {

            agent any

            steps {
                script {
                    projetcs = [
                        ["project" :"AcademiaPay.Gateway.WebApi", "image" : "gateway-webapi", "swarm-service": "academia-gateway_gateway-webapi"],
                        ["project" :"AcademiaPay.Gateway.Worker", "image" : "gateway-worker", "swarm-service": "academia-gateway_gateway-worker"],
                    ]
                }
            }
        }
        ...
    }
}
def projetcs

pipeline {
    
    agent none

    environment {
        COMPOSE_PROJECT_NAME = "${env.JOB_NAME}-${env.BUILD_ID}"
    }
    stages {
    
        ...
      
        stage('Build') {
            agent any
            steps {
                script {
                    for (int i = 0; i < projetcs.size(); ++i) {
                        sh """docker build -f ./${projetcs[i]["project"]}/Dockerfile ./"""
                    }
                }
            }
        }

        ...
    }
}
def projetcs

pipeline {
    
    agent none

    environment {
        COMPOSE_PROJECT_NAME = "${env.JOB_NAME}-${env.BUILD_ID}"
    }
    stages {
    
        ...
      
        stage('Test') {
            agent {
                dockerfile {
                    filename './sonar/Dockerfile'
                    args '-u root:root -v /var/run/docker.sock:/var/run/docker.sock -v /docker/nuget-cache:/root/.nuget/packages'
                }
            }
            when {
                branch 'main'
            }
            steps {
                withCredentials([usernamePassword(credentialsId: 'SonarQube', passwordVariable: 'SONARQUBE_KEY', usernameVariable: 'DUMMY' )]) 
                {
                    script 
                    {
                        def sonarParams = [
                            '/k:"academia-dev_academia-pay-gateway"',                            
                            '/d:sonar.token="$SONARQUBE_KEY"',
                            '/d:sonar.host.url="https://sonar.academia-pay.com"',
                            '/d:sonar.cs.vscoveragexml.reportsPaths=/output-coverage/coverage.xml'
                        ]

                        def sonarParamsText = sonarParams.join(" ")

                        // sonarcloud issue | https://community.sonarsource.com/t/could-not-find-ref-refs-heads-master-in-refs-heads-refs-remotes-upstream-or-refs-remotes-origin/37016/5
                        // git fetch origin master:master | git fetch origin develop:develop
                        sh  """                            
                            chmod +x ./dynamic-sln.sh && ./dynamic-sln.sh

                            dotnet workload restore ./AcademiaPay-Jenkins.sln

                            export PATH="\$PATH:/root/.dotnet/tools"

                            dotnet sonarscanner begin ${sonarParamsText}

                            dotnet build --no-incremental --property:AcademiaPayServiceDefaultsAsPackage=1 ./AcademiaPay-Jenkins.sln

                            dotnet-coverage collect "dotnet test --property:AcademiaPayServiceDefaultsAsPackage=1" -f xml -o "/output-coverage/coverage.xml"

                            dotnet sonarscanner end /d:sonar.token="\$SONARQUBE_KEY"

                        """
                    }
                }
            }
        }

        ...
    }
}
def projetcs

pipeline {
    
    agent none

    environment {
        COMPOSE_PROJECT_NAME = "${env.JOB_NAME}-${env.BUILD_ID}"
    }
    stages {
    
        ...
      
        stage('Pack') {

            agent any

            when { buildingTag() }

            steps {
                
                script {
                    
                    for (int i = 0; i < projetcs.size(); ++i) {

                        sh """docker build -f ./${projetcs[i]["project"]}/Dockerfile -t meu-registry.com/academia-pay/${projetcs[i]["image"]}:${BRANCH_NAME}  ./"""
                        
                        sh """docker tag meu-registry.com/academia-pay/${projetcs[i]["image"]}:${BRANCH_NAME}  meu-registry.com/academia-pay/${projetcs[i]["image"]}:latest"""
                    }
                }
            }

        }

        ...
    }
}
def projetcs

pipeline {
    
    agent none

    environment {
        COMPOSE_PROJECT_NAME = "${env.JOB_NAME}-${env.BUILD_ID}"
    }
    stages {
    
        ...
      
        stage('Publish') {

            agent any

            when { buildingTag() }

            steps {
                
                script {

                    withDockerRegistry( credentialsId: 'jenkins-at-meu-registry.com', url: 'https://meu-registry.com')
                    {

                        for (int i = 0; i < projetcs.size(); ++i) {

                            sh """docker push meu-registry.com/academia-pay/${projetcs[i]["image"]}:${BRANCH_NAME}"""

                            sh """docker push meu-registry.com/academia-pay/${projetcs[i]["image"]}:latest"""

                        }
                    }
                }
            }
        }

        ...
    }
}
def projetcs

pipeline {
    
    agent none

    environment {
        COMPOSE_PROJECT_NAME = "${env.JOB_NAME}-${env.BUILD_ID}"
    }
    stages {
    
        ...
      
        stage('Deploy') {

            agent any

            when { buildingTag() }

            steps {
                
                 script {
                    
                    def isDEV = ( env.BRANCH_NAME.endsWith("-dev") );
                    def isPRD = ( env.BRANCH_NAME.endsWith("-prd") );

                    //echo sh(script: 'env|sort', returnStdout: true)
                    
                    if ( isDEV )
                    {
                       
                        for (int i = 0; i < projetcs.size(); ++i) {

                            sh """docker service update --image meu-registry.com/academia-pay/${projetcs[i]["image"]}:${BRANCH_NAME} ${projetcs[i]["swarm-service"]}"""

                        }
                    }
                 }
            }
        }
    }
}

Jenkins continua imponente e entregando valor, mesmo nascendo na comunidade Java e da comunidade Java uso aqui com .NET desde 2010, quando fui apresentado à solução.

Na medida que seu servidor é construído, e os setups iniciais ficam para o passado, é muito fácil executar tarefas de dias, em horas.

Deixei vários stages e pipelines de demonstração porque eles fazem diferença. Guarde todos que fizer.

0 comentários

Enviar um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.

Lives

Fique de olho nas lives

Fique de olho nas lives no meu canal do Youtube, no Canal .NET e nos Grupos do Facebook e Instagram.

Aceleradores

Existem diversas formas de viabilizar o suporte ao teu projeto. Seja com os treinamentos, consultoria, mentorias em grupo.