본문 바로가기
카테고리 없음

[AEWS-3기] Kubernetes CI/CD - Jenkins, Gogs, ArgoCD 배포

by james_janghun 2025. 3. 29.

 

Jenkins 및 Gogs 설치하기

docker-compose를 통해서 컨테이너로 설치한다.

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 명령어를 통해서 두 개의 컨테이너를 실행시키고 정보를 확인해보자.

docker compose up -d
docker compose ps

for i in gogs jenkins ; do echo ">> container : $i <<"; docker compose exec $i sh -c "whoami && pwd"; echo; done

 

Gogs 컨테이너 초기 설정

먼저 초기 설정 페이지에 접속한다.

http://127.0.0.1:3000/install

 

해당 설정에서는 SQLite를 통해서 했으나 본인이 관리하는 DB로 맞춰서 설정하면 된다. 또한 도메인도 사용하는 도메인이 있다면 해당 도메인을 통해서 사용하면 된다.

 

 

추가설정에는 이메일이나 관리자를 설정하는 옵션이 있다. 관리자 설정까지 진행해준다.

 

바로 로그인해주면 우리만의 private git repository가 생성되게 된다.

 

이제 로그인 후에 Setting에서 Application에서 새 토큰을 생성버튼을 눌러준다.

 

토큰 이름은 devops로 하였고, 해당 토큰을 잘 복사해둔다.

 

24bbb920ed416902053b03de0c5ae0a963fb5363

 

리포지토리 생성

이제 실제 리포지토리를 생성해보자. 일반적인 회사에 맞게 개발팀용과 데브옵스팀용으로 나눠 진행하고자한다.

다시 대시보드로 돌아와 +버튼을 누르면 리포지토리 생성이 가능하다.

 

개발팀용 리포지토리

dev-app 라는 이름의 프라이빗 리포지토리로 생성했다.

README 설정 등 사실 github 등 리포지토리를 생성해본 사람들이라면 설정자체가 비슷해서 크게 어려운 것은 없다.

 

근데 이거 보다보니 한국어 번역이 이상하던데, 가시성 항목의 원문은 다음과 같다. 한국어를 사용하는 사람들은 반드시 확인하기 바란다.

Private -> 비공개

Unlisted -> 리스트 자체가 표시되지 않음

 

 

저장소를 만들면 github랑 완전 일치하는 모습이 나온다. 주소도 확인 잘 해놓기 바란다.

 

 

동일한 방식으로 데브옵스팀 용으로도 만들었다.

 

Jenkins 컨테이너 초기 설정

일단 http://127.0.0.1:8080 로 접속해서 초기 화면에 접속한다. 이때 다음 명령어를 통해서 initalAdminPassword를 받아와 입력해준다.

docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
56020c6ea65742c693d74147d090b701

 

 

본인이 Jenkins를 잘 다루는 사람이라면 Select plugins to install을 통해서 커스텀으로 설치하면 더욱 전문화해서 사용할 수 있을 것이다. 그러나 처음 사용하는 사람이라면 suggested 옵션을 골라서 바로 설치하기 바란다.

 

 

설치되는 플러그인과 설치 과정을 보여준다. 시간은 약 5분정도 소요되는 것 같다.

 

 

초기 admin 설정

초기 설정을 마친다.

Jenkins config 설정이다. 해당 URL을 통해서 접근하는 것을 허용하게 될 것이다. 마치 ingress 설정과 동일한 절차이기 때문에, 만약 퍼블릭하게 열 생각이 있다면 퍼블릭 IP를 아닐 경우 프라이빗 IP를 넣어주면 된다.

 

Jenkins 환경설정 - 자격증명 설정

Jenkins 관리 -> Credentials -> Globals -> Add credentials

1. Gogs Repo 자격증명(gogs-crd)

kind Username with password
Username  
Password  
ID  

 

 

2. 도커허브 자격증명(dockerhub-crd)

 

kind Username with password
Username  
Password  
ID  

 

 

 

 

3. k8s 클러스터 자격증명(k8s-crd)

 

 

kind Secret file
file kubeconfig 파일
ID k8s-crd

 

 

 

Jenkins 파이프라인 생성

pipeline {
    agent any
    environment {
        DOCKER_IMAGE = '<자신의 도커 허브 계정>/dev-app' // Docker 이미지 이름
    }
    stages {
        stage('Checkout') {
            steps {
                 git branch: 'main',
                 url: 'http://<자신의 집 IP>:3000/devops/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."
        }
    }
}

 

쿠버네티스 디플로이먼트 오브젝트 배포

빌드 후 테스트가 진행가능한 애플리케이션을 배포하겠습니다.

DHUSER=kingdom0220 #dockerhub 계정

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: timeserver
spec:
  replicas: 2
  selector:
    matchLabels:
      pod: timeserver-pod
  template:
    metadata:
      labels:
        pod: timeserver-pod
    spec:
      containers:
      - name: timeserver-container
        image: docker.io/$DHUSER/dev-app:0.0.1
        livenessProbe:
          initialDelaySeconds: 30
          periodSeconds: 30
          httpGet:
            path: /healthz
            port: 80
            scheme: HTTP
          timeoutSeconds: 5
          failureThreshold: 3
          successThreshold: 1
EOF

 

 

 

 

 

 

 

 

ArgoCD

초기 패스워드 확인

Argocd는 초기패스워드를 secret에 저장하여 확인할 수 있는 전략을 취했다. 매우 현명하다. Secret이기 때문에 base64를 통해 디코딩해야한다.

kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d ;echo

 

실제로 이렇게 확인된 비밀번호를 통해서 접속하면 된다.

admin / dfOGiG4Ax6X9zNME 이런식으로 접속해 보겠다.

 

가장 먼저 할 일은 User Info에 들어가서 Update Password 버튼을 통해서 비밀번호를 변경하자.

 

 

ops-deploy repo 등록

Setting -> Repositories -> Connect repo에 들어가서 git 리포지토리를 등록한다.

해당 리포지토리에서 트리거를 받아서 진행하게 된다.

 

이 successful이 떠야한다.

 

 

일단 ops-deploy에 예시 코드를 작성해보자.

#
cd cicd-labs

TOKEN=24bbb920ed416902053b03de0c5ae0a963fb5363
MyIP=192.168.0.33
git clone http://devops:$TOKEN@$MyIP:3000/devops/ops-deploy.git
cd ops-deploy

#
git --no-pager config --local --list
git config --local user.name "devops"
git config --local user.email "a@a.com"
git config --local init.defaultBranch main
git config --local credential.helper store
git --no-pager config --local --list
cat .git/config

#
git --no-pager branch
git remote -v

#
VERSION=1.26.1
mkdir nginx-chart
mkdir nginx-chart/templates

cat > nginx-chart/VERSION <<EOF
$VERSION
EOF

cat > nginx-chart/templates/configmap.yaml <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}
data:
  index.html: |
{{ .Values.indexHtml | indent 4 }}
EOF

cat > nginx-chart/templates/deployment.yaml <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}
    spec:
      containers:
      - name: nginx
        image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
        ports:
        - containerPort: 80
        volumeMounts:
        - name: index-html
          mountPath: /usr/share/nginx/html/index.html
          subPath: index.html
      volumes:
      - name: index-html
        configMap:
          name: {{ .Release.Name }}
EOF

cat > nginx-chart/templates/service.yaml <<EOF
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}
spec:
  selector:
    app: {{ .Release.Name }}
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 30000
  type: NodePort
EOF

cat > nginx-chart/values-dev.yaml <<EOF
indexHtml: |
  <!DOCTYPE html>
  <html>
  <head>
    <title>Welcome to Nginx!</title>
  </head>
  <body>
    <h1>Hello, Kubernetes!</h1>
    <p>DEV : Nginx version $VERSION</p>
  </body>
  </html>

image:
  repository: nginx
  tag: $VERSION

replicaCount: 1
EOF

cat > nginx-chart/values-prd.yaml <<EOF
indexHtml: |
  <!DOCTYPE html>
  <html>
  <head>
    <title>Welcome to Nginx!</title>
  </head>
  <body>
    <h1>Hello, Kubernetes!</h1>
    <p>PRD : Nginx version $VERSION</p>
  </body>
  </html>

image:
  repository: nginx
  tag: $VERSION

replicaCount: 2
EOF

cat > nginx-chart/Chart.yaml <<EOF
apiVersion: v2
name: nginx-chart
description: A Helm chart for deploying Nginx with custom index.html
type: application
version: 1.0.0
appVersion: "$VERSION"
EOF


# 
helm template dev-nginx nginx-chart -f nginx-chart/values-dev.yaml
helm template prd-nginx nginx-chart -f nginx-chart/values-prd.yaml
DEVNGINX=$(helm template dev-nginx nginx-chart -f nginx-chart/values-dev.yaml | sed 's/---//g')
PRDNGINX=$(helm template prd-nginx nginx-chart -f nginx-chart/values-prd.yaml | sed 's/---//g')
diff <(echo "$DEVNGINX") <(echo "$PRDNGINX")

#
git add . && git commit -m "Add nginx helm chart" && git push -u origin main

 

 

애플리케이션 생성

이제 이 상태로 application에 등록한다.

 

 

 

 

참고) 애플리케이션 SYNC(동기화) 옵션은 다음과 같다.

  • SET DELETION FINALIZER: 애플리케이션이 삭제될 때 관련 Kubernetes 리소스를 함께 삭제하도록 finalizer를 설정한다. 이 옵션이 없으면 Argo CD 애플리케이션만 삭제되고 배포된 리소스는 남아있게 된다.
  • SKIP SCHEMA VALIDATION: Kubernetes 리소스 스키마 유효성 검사를 건너뛴다. 사용자 정의 리소스나 새로운 API 버전을 사용할 때 유용한다.
  • PRUNE LAST: 동기화 과정에서 더 이상 필요 없는 리소스를 제거(prune)하는 작업을 가장 마지막에 수행한다. 새로운 리소스를 먼저 생성하고 난 후 오래된 리소스를 제거하여 서비스 중단을 최소화할 수 있다.
  • RESPECT IGNORE DIFFERENCES: .argocd-allow-hook-resource-modification 주석이 있는 리소스는 Argo CD의 자동 동기화 과정에서 무시한다. 수동으로 수정된 리소스를 보존하고 싶을 때 유용하다.
  • AUTO-CREATE NAMESPACE: 클러스터에 네임스페이스가 없을 시 Argo CD에 입력한 이름으로 자동 생성한다.
  • APPLY OUT OF SYNC ONLY: 현재 동기화 상태가 아닌 리소스만 배포한다. 불필요한 리소스 적용을 줄여 동기화 성능을 향상시킨다.
  • SERVER-SIDE APPLY: 서버 측 적용(server-side apply) 기능을 사용한다. 클라이언트가 아닌 Kubernetes API 서버에서 변경 사항을 처리하여 충돌 해결 및 리소스 관리가 개선된다.
  • PRUNE PROPAGATION POLICY: 리소스 제거 시 적용할 전파 정책을 설정한다. 'foreground'는 종속 리소스가 모두 삭제된 후 부모 리소스를 삭제한다.
    • Foreground : 부모(소유자, deployment) 자원을 먼저 삭제한다.
    • Background : 자식(종속자, pod) 자원을 먼저 삭제한다.
    • Orphan : 소유자는 삭제되었으나, 종속자가 삭제되지 않은 경우, 자원을 삭제한다.
  • REPLACE: 기존 리소스를 패치(patch)하지 않고 완전히 교체한다. 리소스 상태를 완전히 초기화하고 싶을 때 유용하다.
  • RETRY: 동기화 실패 시 자동으로 재시도한다. 일시적인 네트워크 문제나 리소스 충돌이 있을 때 유용하다.

 

애플리케이션 상세 정보 확인

정보 확인시 outofSync가 발생하고 있다. 현재 git 리포지토리와 실제가 차이가 나는것으로 해당 부분을 확인하기 위해 diff 를 눌러준다

 

 

어떤 부분에 차이가 나는지 확인할 수 있다.

 

이는 CLI를 통해서도 쉽게 알 수 있다.

kubectl get applications -n argocd
kubectl describe applications -n argocd dev-nginx

 

 

Sync

SYNC 버튼을 통해서 K8S 운영 환경에 반영해보자. 

 

누르자마자 아주 빠르게 반영되는 것을 확인할 수 있다.

 

 

코드 변경 배포

자 우리는 이제 1.26.1에서 1.26.2로 버전이 업그레이드 되도록 진행해보자.

먼저 다음 코드를 모두 복사해서 현재 ops-deploy 리포지토리에 있는 nginx의 버전을 1.26.1에서 1.26.2로 업그레이드 하는 배포를 진행해보자.

#
VERSION=1.26.2

cat > nginx-chart/VERSION <<EOF
$VERSION
EOF

cat > nginx-chart/values-dev.yaml <<EOF
indexHtml: |
  <!DOCTYPE html>
  <html>
  <head>
    <title>Welcome to Nginx!</title>
  </head>
  <body>
    <h1>Hello, Kubernetes!</h1>
    <p>DEV : Nginx version $VERSION</p>
  </body>
  </html>

image:
  repository: nginx
  tag: $VERSION

replicaCount: 2
EOF

cat > nginx-chart/values-prd.yaml <<EOF
indexHtml: |
  <!DOCTYPE html>
  <html>
  <head>
    <title>Welcome to Nginx!</title>
  </head>
  <body>
    <h1>Hello, Kubernetes!</h1>
    <p>PRD : Nginx version $VERSION</p>
  </body>
  </html>

image:
  repository: nginx
  tag: $VERSION

replicaCount: 2
EOF

cat > nginx-chart/Chart.yaml <<EOF
apiVersion: v2
name: nginx-chart
description: A Helm chart for deploying Nginx with custom index.html
type: application
version: 1.0.0
appVersion: "$VERSION"
EOF

#
git add . && git commit -m "Update nginx version $(cat nginx-chart/VERSION)" && git push -u origin main

 

 

버전 1.26.2로 변경하게 되면 다음과 같이 OutOfSync가 나타나게 된다.

기본적으로 3분(180초) 마다 자동 refresh하기 때문에, 일반적으로는 3분마다 체킹한다. 만약 해당 부분을 수정하고 싶다면 argocd-cm에서 timeout.reconciliation의 값을 조정하면 된다.

kubectl get cm -n argocd argocd-cm -o yaml | grep timeout
  timeout.hard.reconciliation: 0s
  timeout.reconciliation: 180s

 

아직까지는 1.26.1 버전임을 확인할 수 있다.

자 이제 Sync를 눌러보자. dev-nginx v2가 배포된 것을 확인할 수 있다.

 

접속해봐도 1.26.2로 변경된 것을 확인할 수 있다.

 

 

Webhook을 통한 Argo CD 반영

webhook 설정할 때 주의사항이 있다. 로컬 호스트에서 반영할 경우 반드시 다음으 수정해야한다.

지금 이렇게 프라이빗 IP를 통해서 진행해보니 에러가 계속 발생했다.

webhook broken: "Payload URL resolved to a local network address that is implicitly blocked."

 

이 오류를 찾다보니 다음 깃허브이슈를 발견했다.

https://github.com/gogs/gogs/discussions/7048#discussioncomment-2930780

 

이곳에서 해당 코드 변경을 이야기하더라구요.

기본적으로 localhost에 대한 허용이 없습니다. /data/gogs/conf/app.ini 를 수정하고 컨테이너를 재시작하면 됩니다.

[security]
# specific network address, separated by commas, no port needed
LOCAL_NETWORK_ALLOWLIST = *

 

 

따라서 다음과 같이 webhook 등록이 가능해집니다.

 

자 이제 해당 테스트 앱을 먼저 생성해보고, webhook 반영이잘되는지 확인해봅시다.

#
echo $MyIP

cat <<EOF | kubectl apply -f -
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: dev-nginx
  namespace: argocd
  finalizers:
  - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    helm:
      valueFiles:
      - values-dev.yaml
    path: nginx-chart
    repoURL: http://$MyIP:3000/devops/ops-deploy
    targetRevision: HEAD
  syncPolicy:
    automated:
      prune: true
    syncOptions:
    - CreateNamespace=true
  destination:
    namespace: dev-nginx
    server: https://kubernetes.default.svc
EOF


#
kubectl get applications -n argocd dev-nginx
kubectl get applications -n argocd dev-nginx -o yaml | kubectl neat
kubectl describe applications -n argocd dev-nginx
kubectl get pod,svc,ep,cm -n dev-nginx

#
curl http://127.0.0.1:30000
open http://127.0.0.1:30000

 

replica count자체를 수정하는 방식으로 늘려보자. 극단적으로 12개로 파드 갯수를 늘려보자.

cd cicd-labs/ops-deploy/nginx-chart

#
sed -i -e "s|replicaCount: 2|replicaCount: 12|g" values-dev.yaml
git add values-dev.yaml && git commit -m "Modify nginx-chart : values-dev.yaml" && git push -u origin main
watch -d kubectl get all -n dev-nginx -o wide

 

즉각적으로 반영되는 것을 확인할 수 있다.

 

 

(참고) 혹 바로 반영되지 않는다면 다음과 같은 방식으로 트러블 슈팅을 해보기 바란다.

1. gogs 웹훅의 로그를 확인한다.

웹훅 아래 보면 최근 Deliveries라는 항목으로 웹훅에 대한 로그를 제공하고 있다.

 

문제가 생기면 다음과 같이 에러 응답에 대한 내용을 살펴볼 수 있고, 재전송도 가능하다.

 

만약 해당 응답이 200으로 정상 처리되었다면 이제 ArgoCD의 문제일 수 있다.

 

2. ArgoCD 로그 확인하기

기본적으로 해당 웹훅에 대한 정보는 argocd-server에서 로그를 받고 있다.

kubectl logs -f deployment/argocd-server -n argocd

 

다음과 같이 정상적으로 로그를 받았을 경우 이렇게 Received push event를 확인할 수 있다.

이때 주의할 점은 내가 등록한 repository와 실제 웹훅에서 오는 external 도메인과 일치해야한다는 점이다.

나의 경우 정상적으로 처리되는데도 불구하고 반영이 안되길래 찾다보니, 도착되는 url은 localhost:3000인데, argocd에서 등록된 url 정보는 172.30.1.3이기 때문에 불일치하여 반영되지 않았다. 따라서 이 둘을 일치시켜야한다.