안녕하세요, devyu입니다.
오늘은 DevOps의 핵심 요소인 지속적 통합(CI)에 대해 이야기해보려고 합니다. 특히 KT Cloud의 K2P Standard Container Devops의 Kubernetes 환경에서 Jenkins 파이프라인을 구축하고, GitLab의 소스 코드를 사용해 이미지를 빌드하고 푸시하는 과정을 소개하겠습니다.
먼저, 전체적인 구조를 팀 내에서 조금 더 잘 이해하기 위해 아래 예시 그림을 참조하였습니다.(출처: https://jh-labs.tistory.com/673)

이 구조에서 Jenkins는 GitLab의 웹훅(webhook)을 받아 파이프라인을 트리거하게 됩니다. 이제 각 구성 요소에 대해 자세히 알아보겠습니다.
Jenkins 파이프라인은 Kubernetes 클러스터 내에서 실행됩니다. 여기서 중요한 점은 별도의 Jenkins 에이전트와 Docker-in-Docker(DinD) 환경을 구성한다는 것입니다. 이를 통해 컨테이너 내에서 다른 컨테이너를 빌드하고 관리할 수 있게 됩니다.
파이프라인 전반에서 사용할 변수들을 초기화합니다. 주요 변수로는 REPO, DOCKER_IMAGE_NAME, DOCKER_CREDENTIALS_ID 등이 있습니다.
DinD는 CI/CD 파이프라인에서 매우 중요한 역할을 합니다. 컨테이너 내부에서 다른 Docker 컨테이너를 실행할 수 있게 해주는 이 기술은 Jenkins와 같은 CI 도구가 컨테이너화된 환경에서 도커 빌드 및 테스트 작업을 수행할 수 있도록 지원합니다.
통합 테스트: 서로 다른 서비스를 도커 컨테이너로 빠르게 배포하고 테스트할 수 있습니다.
빌드 환경 격리: 각 빌드를 위한 독립적인 환경을 제공하여 충돌을 방지합니다.
배포 자동화: 컨테이너를 이용한 배포 프로세스를 자동화하고 다양한 환경에 쉽게 배포할 수 있습니다.
Jenkins 파이프라인에서 DinD를 구현하기 위해서는 Kubernetes 클러스터 내의 특정 Pod에 Docker 데몬을 실행하도록 설정해야 합니다. 주요 구성 요소는 다음과 같습니다:
Privileged Mode: 컨테이너가 호스트 시스템의 커널 기능에 접근할 수 있도록 합니다.
Volume Mounts: /var/lib/docker와 /sys/fs/cgroup을 마운트하여 데이터와 자원 관리를 수행합니다.
Network Configuration: Docker 데몬이 TCP 소켓을 통해 외부와 통신할 수 있도록 설정합니다.
보안: Privileged 모드는 보안상의 위험이 있으므로 신뢰할 수 있는 애플리케이션에서만 사용해야 합니다.
자원 사용: DinD 사용 시 추가적인 오버헤드가 발생할 수 있습니다.
이제 실제 사용한 파이프라인 코드를 살펴보겠습니다.
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: jenkins-agent
image: jenkins/inbound-agent:4.10-3
command:
- cat
tty: true
- name: dind-daemon
image: docker:19.03.12-dind
securityContext:
privileged: true # DinD가 동작하기 위한 권한 설정
env:
- name: DOCKER_TLS_CERTDIR
value: "" # Docker TLS 비활성화 (필요시)
command:
- dockerd-entrypoint.sh
args:
- --host=tcp://0.0.0.0:2375
- --host=unix:///var/run/docker.sock
volumeMounts:
- name: dind-storage
mountPath: /var/lib/docker
- name: cgroup
mountPath: /sys/fs/cgroup
volumes:
- name: dind-storage
emptyDir: {}
- name: cgroup
hostPath:
path: /sys/fs/cgroup
'''
}
}
environment {
REPO = '{gitlab 레포 이름}'
DOCKER_IMAGE_NAME = '{이미지 이름}'
DOCKER_CREDENTIALS_ID = 'docker-auth'
}
stages {
stage('Prepare cgroup for DinD') {
steps {
container('dind-daemon') {
script {
sh '''
mkdir -p /sys/fs/cgroup/systemd
mount -t cgroup -o none,name=systemd cgroup /sys/fs/cgroup/systemd
'''
}
}
}
}
stage('Checkout') {
steps {
script {
git branch: 'main',
url: '{gitlab주소}',
credentialsId: 'gitlab-account'
}
}
}
stage('Build and Push Image') {
steps {
container('dind-daemon') {
script {
withCredentials([usernamePassword(credentialsId: "${DOCKER_CREDENTIALS_ID}",
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASSWORD')]) {
sh '''
echo $DOCKER_PASSWORD | docker login -u $DOCKER_USER --password-stdin
docker build -t ${DOCKER_IMAGE_NAME}:${BUILD_NUMBER} -t ${DOCKER_IMAGE_NAME}:latest .
docker push ${DOCKER_IMAGE_NAME}:${BUILD_NUMBER}
docker push ${DOCKER_IMAGE_NAME}:latest
'''
}
}
}
}
}
stage('Deploy to Kubernetes via ArgoCD') {
steps {
script {
// ArgoCD 관련 로직
}
}
}
}
}
이 파이프라인 코드는 다음과 같은 주요 단계로 구성되어 있습니다:
각 단계는 Jenkins 파이프라인의 특정 목적을 달성하기 위해 설계되었으며, 전체적으로 자동화된 CI/CD 프로세스를 구현합니다.
지금까지 Jenkins와 Kubernetes를 활용한 CI/CD 파이프라인 구축에 대해 설명드렸습니다. 이제부터 파이프라인을 구축하고 운영하면서 발견한 이슈들과 앞으로의 개선 계획에 대해 말씀드리도록 하겠습니다.
현재 구성에서는 Docker-in-Docker (DinD)를 사용하기 위해 privileged 모드를 사용하고 있어 보안 위험이 있습니다. 이를 개선하기 위해 Kaniko와 같은 도구를 도입하여 rootless 컨테이너 빌드를 구현하고, Jenkins 에이전트의 권한을 최소화하는 방향으로 변경할 예정입니다.
파이프라인 실행 시간이 길어지는 문제가 있어 이를 개선하기 위해 병렬 실행이 가능한 단계를 식별하고 동시에 실행하도록 파이프라인을 재구성할 예정입니다. 또한, 의존성 다운로드 시간을 줄이기 위해 캐싱 메커니즘을 도입할 계획입니다.
파이프라인 실행에 대한 상세한 모니터링과 로깅이 부족한 문제를 해결하기 위해 Prometheus와 Grafana를 통합하여 실시간 메트릭스를 수집하고 시각화할 예정입니다. 또한, ELK 스택을 도입하여 중앙 집중식 로깅 시스템을 구축할 계획입니다.
이를 통해 파이프라인의 성능 병목 현상을 쉽게 파악하고, 문제 발생 시 빠르게 대응할 수 있게 될 것으로 예상합니다.
Jenkins와 Kubernetes를 통합한 CI/CD 파이프라인 구축은 개발 효율성을 크게 향상시킬 수 있습니다. 이러한 구성을 통해 개발자들은 코드 변경사항을 빠르게 반영하고, 자동화된 테스트 및 배포 과정을 통해 소프트웨어의 품질을 지속적으로 관리할 수 있습니다.
GitLab과 ArgoCD의 연동은 이 프로세스를 더욱 강력하고 효율적으로 만들어 줍니다. 각 단계별로 필요한 설정과 구성 방법을 세심하게 조정하여, 조직의 요구사항에 맞게 파이프라인을 최적화할 수 있습니다.