Cloudnet AEWS 8주차 스터디를 진행하며 정리한 글입니다.
이번 포스팅에서는 Jenkins에 대해 알아보고 Jenkins를 통한 CI/CD 구성하는 방법에 대해 소개하겠습니다.
목표 구성

Jenkins

Jenkins는 소프트웨어 개발 과정에서 지속적인 통합(Continuous Integration) 과 지속적인 배포(Continuous Delivery) 를 지원하기 위해 널리 사용되는 오픈소스입니다.
파이프라인을 통한 개발 주기 단축 및 자동화 (빌드, 배포, 테스트 등)를 위해 사용합니다.
Jenkins 특징
- 플러그인 기반 구조
- 여러 플러그인 통해 개발, 테스트, 배포 도구와 통합 가능
- 파이프라인 기능
- Jenkinsfile이라는 선언형 DSL을 이용해 빌드, 테스트, 배포 등 작업 흐름을 코드로 정의 가능
- 웹 기반 UI
- 사용자가 웹 브라우저 통해 손쉽게 Job 생성하고 관리할 수 있는 UI 제공
Jenkins 작업 소개
작업 소개 (프로젝트, Job, Item) : 3가지 유형의 지시 사항 포함
- 작업을 수행하는 시점 Trigger
- 작업 수행 태스크 task가 언제 시작될지를 지시
- 작업을 구성하는 단계별 태스크 Built step
- 특정 목표를 수행하기 위한 태스크를 단계별 step로 구성할 수 있다.
- 이것을 젠킨스에서는 빌드 스텝 build step이라고 부른다.
- 태스크가 완료 후 수행할 명령 Post-build action
- 예를 들어 작업의 결과(성공 or 실패)를 사용자에게 알려주는 후속 동작이나, 자바 코드를 컴파일한 후 생성된 클래스 파일을 특정 위치로 복사 등
- (참고) 젠킨스의 빌드 : 젠킨스 작업의 특정 실행 버전
- 사용자는 젠킨스 작업을 여러번 실행할 수 있는데, 실행될 때마다 고유 빌드 번호가 부여된다.
- 작업 실행 중에 생성된 아티팩트, 콘솔 로드 등 특정 실행 버전과 관련된 모든 세부 정보가 해당 빌드 번호로 저장된다.
Jenkins 실습 환경 구성
# kind 클러스터 설치
❯ brew install kind
# 유용한 도구 사전 설치
❯ brew install kubernetes-cli
❯ brew install helm
❯ brew install krew
❯ brew install kube-ps1
❯ brew install kubectx
❯ cat <<EOT > docker-compose.yaml
services:
jenkins:
container_name: jenkins
image: jenkins/jenkins
restart: unless-stopped
networks:
- cicd-network
ports:
- "8080:8080"
- "50000:50000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- jenkins_home:/var/jenkins_home
gogs:
container_name: gogs
image: gogs/gogs
restart: unless-stopped
networks:
- cicd-network
ports:
- "10022:22"
- "3000:3000"
volumes:
- gogs-data:/data
volumes:
jenkins_home:
gogs-data:
networks:
cicd-network:
driver: bridge
EOT
# 배포
❯ docker compose up -d
❯ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
gogs gogs/gogs "/app/gogs/docker/st…" gogs 2 seconds ago Up Less than a second (health: starting) 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp, 0.0.0.0:10022->22/tcp, :::10022->22/tcp
jenkins jenkins/jenkins "/usr/bin/tini -- /u…" jenkins 2 seconds ago Up Less than a second 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 0.0.0.0:50000->50000/tcp, :::50000->50000/tcp
# 기본 정보 확인
for i in gogs jenkins ; do echo ">> container : $i <<"; docker compose exec $i sh -c "whoami && pwd"; echo; done
>> container : gogs <<
root
/app/gogs
>> container : jenkins <<
jenkins
/
# 초기 계정 정보 확인
❯ docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
47d5dbcf2de64db3a3db2be247d26fb9
admin, 위에서 확인한 패스워드로 로컬에서 접근한 뒤 권장 플러그인을 설치한 뒤, admin 유저를 생성합니다.
admin/qwe123로 유저를 생성한 뒤, 내 로컬 주소 192.168.219.103로 아이피를 등록하였습니다.


Gogs 설치

토큰 생성 (fd816432cf3fb1e9f6ecb60c70162c5d55ac047c)



❯ TOKEN=fd816432cf3fb1e9f6ecb60c70162c5d55ac047c
❯ MyIP=192.168.219.103
❯ git clone http://devops:$TOKEN@$MyIP:3000/yoo/dev-app.git
Cloning into 'dev-app'...
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (4/4), 691 bytes | 230.00 KiB/s, done.
❯ git config --local user.name "yoo"
❯ git config --local user.email "yoo@test.com"
❯ git config --local init.defaultBranch main
❯ git config --local credential.helper store
❯ git --no-pager config --local --list
❯ cat .git/config
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true
core.ignorecase=true
core.precomposeunicode=true
remote.origin.url=http://devops:fd816432cf3fb1e9f6ecb60c70162c5d55ac047c@192.168.219.103:3000/yoo/dev-app.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.main.remote=origin
branch.main.merge=refs/heads/main
branch.main.vscode-merge-base=origin/main
user.name=yoo
user.email=yoo@test.com
init.defaultbranch=main
credential.helper=store
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = http://devops:fd816432cf3fb1e9f6ecb60c70162c5d55ac047c@192.168.219.103:3000/yoo/dev-app.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
vscode-merge-base = origin/main
[user]
name = yoo
email = yoo@test.com
[init]
defaultBranch = main
[credential]
helper = store
❯ git --no-pager branch
❯ git remote -v
* main
origin http://devops:fd816432cf3fb1e9f6ecb60c70162c5d55ac047c@192.168.219.103:3000/yoo/dev-app.git (fetch)
origin http://devops:fd816432cf3fb1e9f6ecb60c70162c5d55ac047c@192.168.219.103:3000/yoo/dev-app.git (push)
# server.py 파일 작성
cat > server.py <<EOF
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
from datetime import datetime
import socket
class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
match self.path:
case '/':
now = datetime.now()
hostname = socket.gethostname()
response_string = now.strftime("The time is %-I:%M:%S %p, VERSION 0.0.1\n")
response_string += f"Server hostname: {hostname}\n"
self.respond_with(200, response_string)
case '/healthz':
self.respond_with(200, "Healthy")
case _:
self.respond_with(404, "Not Found")
def respond_with(self, status_code: int, content: str) -> None:
self.send_response(status_code)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(bytes(content, "utf-8"))
def startServer():
try:
server = ThreadingHTTPServer(('', 80), RequestHandler)
print("Listening on " + ":".join(map(str, server.server_address)))
server.serve_forever()
except KeyboardInterrupt:
server.shutdown()
if __name__== "__main__":
startServer()
EOF
❯ python3 server.py
❯ curl localhost
The time is 2:09:33 AM, VERSION 0.0.1
Server hostname: yujiyeon-ui-MacBookPro.local
# Dockerfile 생성
cat > Dockerfile <<EOF
FROM python:3.12
ENV PYTHONUNBUFFERED 1
COPY . /app
WORKDIR /app
CMD python3 server.py
EOF
# 버전 파일 생성
echo "0.0.1" > VERSION
❯ tree
.
├── Dockerfile
├── README.md
├── VERSION
└── server.py
❯ git status
On branch main
Your branch is up to date with 'origin/main'.
Untracked files:
(use "git add <file>..." to include in what will be committed)
Dockerfile
VERSION
server.py
nothing added to commit but untracked files present (use "git add" to track)
❯ git add .
❯ git commit -m "Add dev-app"
[main bc171d0] Add dev-app
3 files changed, 40 insertions(+)
create mode 100644 Dockerfile
create mode 100644 VERSION
create mode 100644 server.py
❯ git push -u origin main
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 8 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (5/5), 1016 bytes | 1016.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
To http://192.168.219.103:3000/yoo/dev-app.git
67549eb..bc171d0 main -> main
branch 'main' set up to track 'origin/main'.

이미지 저장소로 Dockerhub를 사용하겠습니다.


Kind 클러스터 설치
❯ docker ps 02:09:33
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4e11b05e125b jenkins/jenkins "/usr/bin/tini -- /u…" 43 minutes ago Up 23 minutes 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp, 0.0.0.0:50000->50000/tcp, :::50000->50000/tcp jenkins
2f04fce07f2b gogs/gogs "/app/gogs/docker/st…" 43 minutes ago Up 43 minutes (healthy) 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp, 0.0.0.0:10022->22/tcp, :::10022->22/tcp gogs
❯ cat kind-3node.yaml 02:20:09
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
apiServerAddress: "192.168.219.103"
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 30000
- containerPort: 30001
hostPort: 30001
- containerPort: 30002
hostPort: 30002
- containerPort: 30003
hostPort: 30003
- role: worker
- role: worker
Jenkins 플러그인 설치
1. Pipeline stage view
2. Docker pipeline
3. Gogs Webhook plugin




유저 이름을 devops가 아닌 yoo로 바꾸어서 에러가 발생하여 올바른 주소로 변경하였습니다.
pipeline {
agent any
environment {
DOCKER_IMAGE = 'hellouz818/dev-app' // Docker 이미지 이름
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'http://192.168.219.103:3000/yoo/dev-app.git', // Git에서 코드 체크아웃
credentialsId: 'gogs-crd' // Credentials ID
}
}
stage('Read VERSION') {
steps {
script {
// VERSION 파일 읽기
def version = readFile('VERSION').trim()
echo "Version found: ${version}"
// 환경 변수 설정
env.DOCKER_TAG = version
}
}
}
stage('Docker Build and Push') {
steps {
script {
docker.withRegistry('https://index.docker.io/v1/', 'dockerhub-crd') {
// DOCKER_TAG 사용
def appImage = docker.build("${DOCKER_IMAGE}:${DOCKER_TAG}")
appImage.push()
appImage.push("latest")
}
}
}
}
}
post {
success {
echo "Docker image ${DOCKER_IMAGE}:${DOCKER_TAG} has been built and pushed successfully!"
}
failure {
echo "Pipeline failed. Please check the logs."
}
}
}


이미지 풀백 에러가 발생했습니다.
❯ kubectl get deploy,pod -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
deployment.apps/timeserver 0/2 2 0 19s timeserver-container docker.io/hellouz818/dev-app:0.0.1 pod=timeserver-pod
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/timeserver-58468b68-jt89b 0/1 ErrImagePull 0 19s 10.244.1.2 myk8s-worker2 <none> <none>
pod/timeserver-58468b68-wfgls 0/1 ImagePullBackOff 0 19s 10.244.2.2 myk8s-worker <none> <none>
❯ kubectl get secret -A
NAMESPACE NAME TYPE DATA AGE
kube-system bootstrap-token-abcdef bootstrap.kubernetes.io/token 6 14m
❯ kubectl describe pod
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m8s default-scheduler Successfully assigned default/timeserver-58468b68-wfgls to myk8s-worker
Normal Pulling 32s (x4 over 2m7s) kubelet Pulling image "docker.io/hellouz818/dev-app:0.0.1"
Warning Failed 29s (x4 over 2m5s) kubelet Failed to pull image "docker.io/hellouz818/dev-app:0.0.1": failed to pull and unpack image "docker.io/hellouz818/dev-app:0.0.1": failed to resolve reference "docker.io/hellouz818/dev-app:0.0.1": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
Warning Failed 29s (x4 over 2m5s) kubelet Error: ErrImagePull
Normal BackOff 2s (x7 over 2m4s) kubelet Back-off pulling image "docker.io/hellouz818/dev-app:0.0.1"
Warning Failed 2s (x7 over 2m4s) kubelet Error: ImagePullBackOff
❯ kubectl create secret docker-registry dockerhub-secret \
--docker-server=https://index.docker.io/v1/ \
--docker-username=$DHUSER \
--docker-password=$DHPASS
secret/dockerhub-secret created
❯ kubectl get secrets -o yaml
apiVersion: v1
items:
- apiVersion: v1
data:
.dockerconfigjson: eyJhdXRocyI6eyJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8iOnsidXNlcm5hbWUiOiJoZWxsb3V6ODE4IiwicGFzc3dvcmQiOiJkY2tyX3BhdF9pOHhydDlWVm0xaDFKVVdWRVNkOTZpWFMzWnciLCJhdXRoIjoiYUdWc2JHOTFlamd4T0Rwa1kydHlYM0JoZEY5cE9IaHlkRGxXVm0weGFERktWVmRXUlZOa09UWnBXRk16V25jPSJ9fX0=
kind: Secret
metadata:
creationTimestamp: "2025-03-29T19:58:19Z"
name: dockerhub-secret
namespace: default
resourceVersion: "2059"
uid: 1a9d5026-3edb-46c5-81c9-1b2758f5670b
type: kubernetes.io/dockerconfigjson
kind: List
metadata:
resourceVersion: ""
Every 2.0s: kubectl get deploy,rs,pod -o wide yujiyeon-ui-MacBookPro.local: 05:01:05
in 0.088s (0)
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS I
MAGES SELECTOR
deployment.apps/timeserver 2/2 2 2 6m6s timeserver-container d
ocker.io/hellouz818/dev-app:0.0.1 pod=timeserver-pod
NAME DESIRED CURRENT READY AGE CONTAINERS
IMAGES SELECTOR
replicaset.apps/timeserver-545b8cf698 2 2 2 67s timeserver-contain
er docker.io/hellouz818/dev-app:0.0.1 pod=timeserver-pod,pod-template-hash=545b8cf698
replicaset.apps/timeserver-58468b68 0 0 0 6m6s timeserver-contain
er docker.io/hellouz818/dev-app:0.0.1 pod=timeserver-pod,pod-template-hash=58468b68
NAME READY STATUS RESTARTS AGE IP NODE
NOMINATED NODE READINESS GATES
pod/timeserver-545b8cf698-dhffl 1/1 Running 0 40s 10.244.1.3 myk8s-worke
r2 <none> <none>
pod/timeserver-545b8cf698-s6jj5 1/1 Running 0 67s 10.244.2.3 myk8s-worke
r <none> <none>
'스터디 > AEWS' 카테고리의 다른 글
[AEWS] 9주차 EKS 클러스터 업그레이드 - Blue/Green Migration (0) | 2025.04.05 |
---|---|
[AEWS] 8주차 Argo Rollout 이해하기 (0) | 2025.03.30 |
[AEWS] 7주차 AWS EKS Auto Mode (0) | 2025.03.29 |
[AEWS] 7주차 AWS Fargate (0) | 2025.03.29 |
[AEWS] 7주차 Terraform (0) | 2025.03.29 |