본문 바로가기

DevOps

[DevOps] EKS 환경에서 Helm으로 Jenkins 안전하게 구축하기

반응형

1. 개요

AWS 환경에서 Jenkins를 안정적이고 효율적으로 운영하고 싶다면 Helm으로 Jenkins를 설치하는 것이 정신 건강상 좋다고 본다.

 

왜냐하면 EC2 환경에서의 Agent 연결하는 방식과 다르게 Controller와 Agent를 단순 values.yaml 파일만 가지고 분리하는 것도 굉장히 쉽고 Controller를 재시작해도 EBS Volume을 붙여둔다면 안전하게 재시작할 수 있기 때문이다.

 

다음의 내용들을 고려하여 작업한다고 가정한다.

 

- Agent, Controller의 분리

- Controller를 자유롭게 재시작할 수 있도록 EBS Volume 붙이기

- docker 명령어를 agent pod에서 사용할 수 있도록 하기

- 모니터링을 수월하게 할 수 있도록 하기

...

 

 

참괴 : 이 글에서는 helm install, ebs csi driver, 외부 domain으로 노출하기 등의 내용은 다루지 않는다.

 

2. 구축하기

Jenkins 구축할 때 아래의 Helm Chart를 사용할 것이다.

 

https://github.com/jenkinsci/helm-charts/tree/main/charts/jenkins

 

helm-charts/charts/jenkins at main · jenkinsci/helm-charts

Jenkins helm charts. Contribute to jenkinsci/helm-charts development by creating an account on GitHub.

github.com

 

이제 Jenkins를 Helm Chart로 설치하기 위한 순서들에 대해서 알아보자.

 

1. 1. Chart를 Local로 가져오기

: Local로 가져오지 않을 경우 잘못하면 추후 배포 시 다른 버전의 helm chart와 value를 조합할 가능성이 있기 때문에 그 이슈를 최소화한다.

 

helm pull jenkins/jenkins --untar

 

 

1.2. values.yaml 파일 설정

 

1. Custom Dockerfile 만들기

: 미리 Plugin이 설치된 Jenkins Image를 빌드하여 해당 Image를 Controller 이미지로 활용하는게 좋다고 한다. 그 이유는 Pod가 부팅될 때 Plugin을 설치하는 것이 아니라 미리 이미지를 빌드할 때 설치해두면 좀 더 안전하다는 이유인 것 같다.

 

## 예시

FROM jenkins/jenkins:2.442-jdk11

RUN jenkins-plugin-cli --plugins antisamy-markup-formatter:162.v0e6ec0fcfcf6 apache-httpcomponents-client-4-api:4.5.14-208.v438351942757 \
...
...

 

 

2. Executors를 0으로 설정

: Jenkins Controller는 Job을 명시적으로 실행시키지 않도록 하여 좀 더 안전하게 운영할 수 있도록 한다. 만약 Master node에서 Groovy Script 등으로 모니터링을 해야 하는 일이 있다면 numExecutors: 1로 설정해도 될 것 같다.

numExecutors: 0

executorMode: "NORMAL"

 

 

3. Environment variable

: 만약 System timezone이 UTC라면 Jenkins에서 변경해줘도 된다.

containerEnv:
  - name: TZ
    value: "Asia/Seoul"

javaOpts: >
  -Duser.timezone=Asia/Seoul
  -Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Seoul

 

 

4. 적절한 nodeSelector, CPU & Memory 설정 (설명 생략)

: 이런 부분들은 생략

 

 

5. Prometheus 모니터링

: Jenkins Heap memory 라던가 Job 상태 등을 모니터링할 수 있도록 해주는 설정이다.

serviceAnnotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "8080"
  prometheus.io/path: "/prometheus"

...

  prometheus:
    enabled: true

 

 

 

6. SSO 설정

: 만약 회사에 SAML 로그인할 수 있는 IdP가 있다면 Jenkins에서 SAML로 인증할 수 있다.

JCasC:
  defaultConfig: true
  configUrls: []
  configScripts:
    welcome-message: |
      jenkins:
		...

security:
    apiToken:
      creationOfLegacyTokenEnabled: false
      tokenGenerationOnCreationEnabled: false
      usageStatisticsEnabled: true
...
  securityRealm: |-
    saml:
      binding: "XXXXXXX"
      displayNameAttributeName: "displayname"
      emailAttributeName: "email"
      groupsAttributeName: "group"
      idpMetadataConfiguration:
        period: 0
        url: "XXXX.YYYYY.ZZZZZ"

      maximumAuthenticationLifetime: 7200
      usernameAttributeName: "username"
      usernameCaseConversion: "none"
  authorizationStrategy: |-
    globalMatrix:
      permissions:
        - "USER:Overall/Read:Anonymous"
        ...
        ...
        ...
        ...

 

 

 

7. agent 설정

이 부분이 꽤나 중요할 수 있는데 agent 설정으로부터 agent가 어떤 podTemplate을 가지는지, 어떤 sidecar 컨테이너를 가지는지, controller와 어떤 url로 통신하는지 등을 설정할 수 있다.

 

agent:
  enabled: true
  defaultsProviderTemplate: ""
  # URL for connecting to the Jenkins controller
  jenkinsUrl: "<MY_JENKINS_URL>"
  ...
  ...
  image: "<MY_JENKINS_AGENT_IMAGE_URL>"
  tag: "<MY_TAG>"
  ..
  podRetention: "Never" # default : Never
  ..
  envVars:
    - name: TZ
      value: "Asia/Seoul"
  ..
  sideContainerName: "jnlp"
  ..
  idleMinutes: 1
  ..
  podName: "jenkins-agent"
  ..
additionalAgents:
  a-agent:
  ..
  ..
  b-agent:
  ..
  ..

 

agent 섹션은 모든 agent의 공통 설정(부모라고 생각해도 된다.)이며 additionalAgents 섹션은 하위 세부 설정들이다.

 

예를 들어 docker build하는 agent와 spring batch를 수행하는 agent는 각각 설치되어야 하는 소프트웨어들이 다를 것이다.

 

docker build하는 agent는 docker 명령어가 설치되어 있어야 할 것이고 그 외에 npm 이라던가 gradle 등 빌드 도구도 깔려 있어야 할 것이다.

 

하지만 spring batch를 수행하는 agent는 적절한 jdk만 깔려 있으면 된다.

 

따라서 이런 목적에 따라서 각각 맞는 additionalAgents 설정을 하면 된다.

 

물론 nodeSelector도 각각 다르게 하여 어떤 agent는 CPU intensive한 워크로드에 배포하던다 어떤 agent는 Memory intensive한 워크로드에 배포하던가 아니면 spot instance를 활용하거나 등 또한 가능하다.

 

그렇다면 왜 <JENKINS_IMAGE_URL> 부분을 남겨두었는지 알게 될 것이다.

 

예를 들어 Docker 커맨드를 사용해야 하는 agent의 custom image는 다음처럼 Dockerfile을 작성할 수 있다.

 

FROM jenkins/inbound-agent:jdk17

USER root
RUN apt update && apt install apt-transport-https ca-certificates curl gnupg lsb-release unzip wget -y
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
RUN echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \
    $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
RUN apt-get update && apt -y install docker-ce-cli

# awscli 설치
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
    unzip awscliv2.zip && \
    ./aws/install

# argocd cli 설치
RUN wget https://github.com/argoproj/argo-cd/releases/download/v2.12.2/argocd-linux-amd64 -O argocd && \
    chmod +x argocd && \
    mv argocd /usr/local/bin

ENV DOCKER_HOST=tcp://localhost:2375

 

 

8. persistence

당연히 jenkins controller는 데이터 영속성이 굉장히 중요하다.

 

따라서 ebs volume을 붙여줘야 하는데, 아래의 설정처럼 적용할 수 있다.

 

참고할 내용으로 EBS Volume은 AZ가 고정되어 있다. 근데 만약 Pod가 다른 AZ에 뜨려고 하면 VolumeAttach가 안되서 Controller가 뜨지 않을 수 있으니 AZ를 고정해서 배포해주는 것도 좋을 수 있다.

 

persistence:
  # -- Enable the use of a Jenkins PVC
  enabled: true

  # A manually managed Persistent Volume and Claim
  # Requires persistence.enabled: true
  # If defined, PVC must be created manually before volume will be bound
  # -- Provide the name of a PVC
  existingClaim:

  # jenkins data Persistent Volume Storage Class
  # If defined, storageClassName: <storageClass>
  # If set to "-", storageClassName: "", which disables dynamic provisioning
  # If undefined (the default) or set to null, no storageClassName spec is
  #   set, choosing the default provisioner (gp2 on AWS, standard on GKE, AWS & OpenStack)
  # -- Storage class for the PVC
  storageClass: ebs-sc

 

 

9. docker 명령어를 사용할 수 있도록 agent 설정

당연히 jenkins agent pod는 docker command를 기본적으로 사용할 수 없다.

 

아마 옛날 Blog 글들을 보면 docker가 container runtime 이던 시절의 글들이 꽤나 많아서 그대로 따라하면 정상적으로 command를 사용할 수 없을 것이다.

 

당연한 것이 현재는 containerd가 기본 eks runtime이고 docker runtime은 node에서 사용할 수 없는데 그 당시에는 ec2 host에 docker를 binding 하여 jenkins agent에서 docker command를 사용하는 것들이 꽤나 많았기 때문이다.

 

이런 이유로 인해 필자는 sidecar Container와 TCP 통신을 통해 docker command를 사용할 수 있게 된다.

 

Jenkins Agent Container --> DOCKER_HOST (tcp://localhost:2375) --> DinD Sidecar Container

 

이 구성으로 Jenkins agent는 Docker CLI로 명령을 보내고, DinD 사이드카의 Docker 데몬이 실제 컨테이너 작업을 수행할 수 있게 된다.

 

따라서 docker command를 사용할 수 있도록 설정해줘야 하는데 다음의 과정이 필요하다.

(누락된 부분도 있을 수 있으니 이 내용은 참고로만 할 것)

 

1. custom image에서 docker-ce-cli 패키지 설치, DOCKER_HOST를 tcp://localhost:2375로 설정

2. DOCKER_TLS_CERTDIR을 공백으로 설정

3. sidecarContainer 설정

 

FROM jenkins/inbound-agent:jdk17

USER root
RUN apt update && apt install apt-transport-https ca-certificates curl gnupg lsb-release unzip wget -y
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
RUN echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian \
    $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
RUN apt-get update && apt -y install docker-ce-cli

# awscli 설치
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
    unzip awscliv2.zip && \
    ./aws/install

# argocd cli 설치
RUN wget https://github.com/argoproj/argo-cd/releases/download/v2.12.2/argocd-linux-amd64 -O argocd && \
    chmod +x argocd && \
    mv argocd /usr/local/bin

ENV DOCKER_HOST=tcp://localhost:2375

 

 

envVars:
  - name: DOCKER_TLS_CERTDIR
    value: ""

 

additionalContainers:
  - sideContainerName: dind
    image:
      repository: docker
      tag: dind
    command: dockerd-entrypoint.sh
    args: ""
    privileged: true
    resources:
      requests:
        cpu: 200m
        memory: 500Mi
      limits:
        memory: 1Gi

 

 

 

 

10. Monitoring

안정적으로 Jenkins를 운영하기 위해서는 Monitoring이 필수적이다.

 

우선 Prometheus plugin을 통해 메트릭을 수집하고 이를 Opentelemetry collector나 Prometheus agent가 scrape 할 수 있도록 한다.

 

그 이후에는 시계열 저장소에 저장한 뒤 Grafana에서 Dashboard를 만들던지 알람 규칙에 따라 알람을 슬랙으로 발송하도록 하면 된다.

 

참고

https://kmaster.tistory.com/114

 

Prometheus를 사용하여 Jenkins 모니터링

Jenkins로 CI/CD 를 구축한 경우에는 빌드 상태나 Jenkins 메모리 사용률, Plugin 상태 등으로 조회하고자 하는 경우가 발생한다. 특히 Namespace (Project) 단위로 여러 대의 Jenkins를 운영한다면 운영팀에서

kmaster.tistory.com

 

 

좀 더 상세한 모니터링을 하고 싶다면 Opentelemetry plugin을 사용할 수도 있다.

 

https://plugins.jenkins.io/opentelemetry/

 

OpenTelemetry

Publish Jenkins metrics to an OpenTelemetry endpoint, including distributed traces of job executions and health metrics of the controller.

plugins.jenkins.io

 

 

Jenkins에서 직접 Groovy Script를 날려서 모니터링 할 수도 있다.

 

- Admin 수준의 권한이 있다면 Batch Job을 돌면서 Groovy Script로 Job monitoring을 할 수도 있고 특이 사항이 있다면 Slack으로 알람도 발송할 수 있다.

 

참고

https://wiki.jenkins.io/display/JENKINS/Monitoring+Scripts

 

Jenkins : Monitoring Scripts

Created by Unknown User (evernat), last modified on Apr 18, 2019 Several scripts to display data about http sessions, threads, memory, JVM or MBeans, when using the Monitoring plugin. Jenkins Script Console Jenkins features a nice Groovy script console whi

wiki.jenkins.io

 

 

 

(추가 : jenkins job log 수집)

efs로 jenkins job log가 떨어지는 path를 volume으로 쉐어링하고 vector sidecar container를 jenkins agent pod에 붙인 다음에 수집했다.

 

 

Vector 설정 : file을 읽은 뒤 filename을 기반으로 메타데이터를 추출한 뒤 loki 혹은 es로 보내도록 한다.

 

sources:
  job-logs:
    type: file
    fingerprint:
      strategy: device_and_inode
    include:
      - /var/jenkins_home/jobs/*/builds/*/log

    exclude:
      - /jenkins-data/jobs/*/builds/legacyIds
      - /jenkins-data/jobs/*/builds/permalinks

transforms:
  job-info:
    type: remap
    inputs:
      - job-logs
    source: |-
      # jobName, jobNumber 추출
      # /jenkins-data/jobs/{잡이름}/builds/{잡넘버}/log
      .splitText = split!(.file, "/")
      .jobName = .splitText[-4]
      .jobNumber = .splitText[-2]
      
      # 불필요 필드 제거
      del(.source_type); del(.timestamp); del(.host); del(.file); del(.splitText);

 

 

 

주의 : device_and_inode는 inode를 기준으로 file의 중복 유무를 구분하기 때문에 로그 누락이 발생할 수도 있다.

기본값은 checksum인데 이 방식으로는 새로운 파일과 기존 파일을 구분할 수 있는 방법을 어떻게 해야 효과적으로 할 수 있는지 찾지 못해 finterprint.strategy는 device_and_inode로 했다.

 

 

 

여기까지 Jenkins를 Helm Chart로 배포할 때 도움이 될만한 내용들을 정리해보았다. 이정도만 해도 꽤나 안정적으로 Jenkins를 운영할 수 있을 것이다.

 

그 외 Jenkins 네이밍 컨벤션 등등 다룰 내용이 꽤나 많지만 Helm Chart로 배포하는 것과 연관이 크지 않기 때문에 생략했다.

반응형