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
이제 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
좀 더 상세한 모니터링을 하고 싶다면 Opentelemetry plugin을 사용할 수도 있다.
https://plugins.jenkins.io/opentelemetry/
Jenkins에서 직접 Groovy Script를 날려서 모니터링 할 수도 있다.
- Admin 수준의 권한이 있다면 Batch Job을 돌면서 Groovy Script로 Job monitoring을 할 수도 있고 특이 사항이 있다면 Slack으로 알람도 발송할 수 있다.
참고
https://wiki.jenkins.io/display/JENKINS/Monitoring+Scripts
(추가 : 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로 배포하는 것과 연관이 크지 않기 때문에 생략했다.
'DevOps' 카테고리의 다른 글
[EKS] Spot Instance를 안전하게 처리하는 방안에 대해서 (9) | 2024.11.08 |
---|---|
[Harbor] EKS 환경에서 Harbor 고가용성(HA)으로 구성하기 (2) | 2024.06.08 |
[Grafana Loki] 청크 중복성 제거하기 (ingester에만 chunk cache 적용) (0) | 2024.05.10 |
[Strimzi Kafka Connect] 손쉽게 커넥터 Config에 시크릿 적용하기 (0) | 2024.05.01 |
[Ansible] 사용자 계정 생성 및 apache http 설치 실습 (0) | 2024.02.04 |