전체 목차

 

서비스를 개발하고 운영하다 보면 적어도 개발 환경에서는 잦은 빌드와 배포를 해야 할 수 있습니다.

CI/CD는 코드 변경 사항을 자동으로 빌드, 테스트 및 배포하여 소프트웨어 개발을 자동화하고 가속화하는 방법론으로, Continuous Integration(지속적 통합)과 Continuous Delivery/Deployment(지속적 제공/배포)의 약자입니다.

개발중인 프로잭트를 형상관리에 커밋 하면 각 노드에서 실행되고 있는 pod로 자동으로 배포 할 수 있도록 구성하면 잦은 배포 수고를 덜 수 있습니다.

 

안정적인 운영을 위해 소스 Repository를 dev, stg, prd 등 으로 나눠 놓고 각각의 배포, 빌드 정책을 정교하게 설정하는 것을 추천 드립니다. 여기서는 main 레포지토리만 존재 하므로 기본 적인 설정만 해보겠습니다.

1. 준비

여기서는 Git Hub에서 소스 형상관리를 하고 빌드된 이미지를 Docker Hub에 올리도록 하겠습니다.

  • Git Hub 계정 및 Repository : 이전에 작성한 소스를 관리할 Repository를 새로 생성해보겠습니다.
  • Docker Hub 계정 및 Repository : 이전 챕터 부터 사용한 레포지토리를 그대로 사용합니다.
  • Kubeconfig : 이전에 구축한 쿠버네티스 설정을 이용하시면 됩니다.

2. Git Hub에 소스 올리기

Git Hub에 새로운 Repository 를 생성하면 나오는 Quick Start 내용 순서 대로 

Project 루트 폴더에서 실행하면 소스 배포는 간단하게 끝납니다.

 

echo "# hello-kubernetes-api" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin https://github.com/basscraft/hello-kubernetes-api.git
git push -u origin main

 

3. Docker Hub 와 Git Hub 연결

3.1. Docker Hub Access Token 생성

Git Hub Actions를 이용해서 이미지를 빌드하고 Docker 에 배포하려면 Docker 쪽에 접근 권한이 있어야 합니다.

우측 상단 계정 아이콘을 클릭하면 Account setting 메뉴가 나옵니다.


좌측 메뉴 Persnal access tokens를 선택하고 리스트 상단 우측에 Generate new token 버튼을 클릭하면 새로운 토큰을 생성 할 수 있습니다.

 

  • Access token description : 이 토큰의 용도를 적습니다. 나중에 토큰이 많아지면 관리를 위해서 알아 볼 수 있게 적습니다. (예: github-actions-k8s)
  • Expiration date : 이 토큰의 만료 기한 지정 합니다.
  • Access permissions : Git Hub Actions에서 이미지를 빌드하고 푸시해야 하므로 Read, Write, Delete 권한이 있어야 합니다.

생성된 Access Token 을 메모장에 복사해 둡니다.

 

3.2. Git Hub Secrets 등록

2에서 생성한 Git Hub Repository의 Setting > Secrets and variables > Actions 메뉴로 이동

Ne repository secret 으로 등록 합니다.

 

두개의 항목을 생성합니다.

  • DOCKERHUB_USERNAME : <도커 사용자 ID>
  • DOCKERHUB_TOKEN : <3.1에서 생성되 access token>

 

3.3. 워크플로우 파일 생성

로컬 프로잭트의 루트 /.github/workflows/ 디렉토리를 만들고 Git Hub Actions 워크 플로우 파일(main.yml)을 생성합니다.

 

 

my-spring-project/ (프로젝트 루트)
├── .github/              <-- 폴더 이름 앞에 마침표(.)가 붙는 숨김 폴더입니다.
│   └── workflows/        <-- 반드시 복수형(workflows)이어야 합니다.
│       └── main.yml      <-- 여기에 어제 만든 워크플로우 내용을 저장합니다.
├── src/
├── build.gradle
└── Dockerfile

 

main.yml 의 내용은 아래 내용을 참고 해서 환경에 맞도록 수정하시면 됩니다.

name: CI/CD Multi-Arch Build

on:
  push:
    branches: [ "main" ] # 메인 브랜치에 푸시될 때 실행

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v3

    - name: Set up JDK 21
      uses: actions/setup-java@v3
      with:
        java-version: '21'
        distribution: 'temurin'
        cache: gradle

    - name: Build with Gradle
      run: ./gradlew bootJar

    - name: Set up QEMU
      uses: docker/setup-qemu-action@v2

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2

    - name: Login to Docker Hub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}

    - name: Build and push Multi-Arch Image
      uses: docker/build-push-action@v4
      with:
        context: .
        platforms: linux/amd64,linux/arm64
        push: true
        tags: basscraft/hello-kubernetes-api:latest

 

참고 1. Git Hub Secret Veriables

username: ${{ secrets.DOCKERHUB_USERNAME }}

password: ${{ secrets.DOCKERHUB_TOKEN }}
이 값은 빌드가 실행되는 순간 Git Hub Actions 시스템이 3.2.에서 등록한 값을 직접 불러다 치환해서 빌드 하므로 신경 쓸필요 없습니다.

참고 2. 본 글에서는 master(amd64)와 worker node(arm64) 의 CPU 아키텍처가 다르기 때문에 약간 더 복잡한 내용으로 구성되었음을 참고 하시기 바랍니다.

 

4. CI/CD 테스트

이전에 작성한 HelloController.java 파일을 수정해서 자동 반영되는지 확인해 보겠습니다.

@Slf4j
@RestController
public class HelloController {

    @GetMapping("/")
    public Map<String, String> hello() {
        log.info("Hello Kubernetes! (CI/CD Success)");
        return Map.of(
                "message", "Hello Kubernetes! (CI/CD Success)",
                "java_version", System.getProperty("java.version"),
                "os_arch", System.getProperty("os.arch"),
                "node_name", System.getenv().getOrDefault("NODE_NAME", "Unknown")
        );
    }
}

 

log 파일에도 로그를 남기고 Return 값도 Map으로 바꿨습니다.

 

아래 접은 글에 빌드&배포 하면서 발생한 문제 및 해결 방식은. 관심 있는 분만 보세요.

더보기

빌드 실패 1

수정 사항을 GitHub에 Push 후 GitHab 레파지토리의 Actions 텝에서 아래와 같이 빌드 실패가 확인 되었습니다.

 

하나의 내용을 확인해 보면

Process completed with exit code 126.

 

제미나이의 도움을 받아 보니 리눅스 환경에서 126 에러는 보통 "파일에 실행권한이 없을 때 발생" 한다고 합니다.

GitHub Action 서버가 gradlew(Gradle Wrapper)파일을 실행하려고 했으나, 이 파일에 실행 권한이 없어서 빌드가 시작조차 되지 못한 것이라고 합니다.

 

해결 방법 1.1.  git 명령으로 명시적으로 실행 권한을 주는 방법

로컬 PC의 개발 환경에서 아래 명령을 실행합니다. (저의 경우 이것으로 했습니다.)

# 1. gradlew 파일에 실행 권한 추가
git update-index --chmod=+x gradlew

# 2. 변경 사항 커밋 및 푸시
git commit -m "Fix: Add execution permission to gradlew"
git push origin main

 

 해결 방법 1.2. 워크플로우 파일에 권한 부여 설정을 추가하는 방법

워크플로우(main.yml) 파일에 빌드 직전에 권한을 부여하도록 수정하는 방법입니다.

- name: Set up JDK 21
      uses: actions/setup-java@v3
      with:
        java-version: '21'
        distribution: 'temurin'
        cache: gradle

    # 이 단계를 추가하여 권한 문제를 해결합니다
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    - name: Build with Gradle
      run: ./gradlew bootJar -x test

 

1.1. git 명령을 실행하고 나서 GitHub Repository 의 Actions 텝에서 보니 빌드가 정상적으로 진행되고 있습니다.

 약 4분 여 지난 후 빌드가 끝나고 초록색으로 바뀌었습니다.

 

 

이후 kubectl rollout restart deployment hello-kubernetes-api 명령으로 각 pod에게 변경된 이미지로 다시 시작 하도록 재시작 명령을 내렸습니다.

 

배포 오류

GitHub Push 후 로컬에서 pod의 상태를 살펴 보면

PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl get pods -w
NAME                                    READY   STATUS    RESTARTS        AGE
hello-kubernetes-api-86b75bb954-ghczq   1/1     Running   7 (4h39m ago)   5d18h
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl rollout restart deployment hello-kubernetes-api
deployment.apps/hello-kubernetes-api restarted
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl get pods -o wide                               
NAME                                    READY   STATUS              RESTARTS        AGE     IP          NODE             NOMINATED NODE   READINESS GATES
hello-kubernetes-api-86b75bb954-ghczq   1/1     Running             7 (4h40m ago)   5d18h   10.1.0.42   docker-desktop   <none>           <none>
hello-kubernetes-api-d7cd8dcd6-m2qvt    0/1     ContainerCreating   0               12s     <none>      docker-desktop   <none>           <none>
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl get pods -o wide
NAME                                   READY   STATUS    RESTARTS   AGE   IP          NODE             NOMINATED NODE   READINESS GATES
hello-kubernetes-api-d7cd8dcd6-m2qvt   1/1     Running   0          24s   10.1.0.43   docker-desktop   <none>           <none>
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl get svc
NAME                           TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
hello-kubernetes-api-service   NodePort    10.106.139.61   <none>        80:32420/TCP   5d18h
kubernetes                     ClusterIP   10.96.0.1       <none>        443/TCP        5d18h
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl get pods -o wide
NAME                                   READY   STATUS    RESTARTS   AGE     IP          NODE             NOMINATED NODE   READINESS GATES
hello-kubernetes-api-d7cd8dcd6-m2qvt   1/1     Running   0          7m51s   10.1.0.43   docker-desktop   <none>           <none>
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi>

 

실행 로그를 보면 4시간 전에 실행된 pod가 rollout restart 후에 새로운 컨테이너가 생성되고나서 새로운 pod로 변경 된 것 까지 확인 됩니다.

 

하지만 http://192.168.2.100:32311 로 접속하면 이전에 작업한 내용과 동일한 내용이 나오고 각 pod 별로 남기도록 한 /var/log/hello-k8s/app.log 파일에도 같은 내용이 찍히고 있습니다.

수정된 내용이 반영이 되지 않았습니다.

 

문제 원인

PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl config get-contexts
CURRENT   NAME                          CLUSTER          AUTHINFO           NAMESPACE
*         docker-desktop                docker-desktop   docker-desktop     
          kubernetes-admin@kubernetes   kubernetes       kubernetes-admin   
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl config use-context kubernetes-admin@kubernetes
Switched to context "kubernetes-admin@kubernetes".
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl config get-contexts                           
CURRENT   NAME                          CLUSTER          AUTHINFO           NAMESPACE
          docker-desktop                docker-desktop   docker-desktop     
*         kubernetes-admin@kubernetes   kubernetes       kubernetes-admin   
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi>

 

원인은 로컬 Docker 에서 바라보고 있는 context가 로컬의 docker-desktop로 선택되어 있어서 쿠버네티스 클러스터에 배포된 것이 아니고 로컬 docker에 배포가 된 것 입니다.

 

컨텍스트를 쿠버네티스 클러스터로 변경하고 다시 이미지 배포(빌드는 성공적으로 끝났으니 컨테이너를 재시작 하면 새로운 이미지를 받아서 실행할 것 입니다.)

PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl rollout restart deployment hello-kubernetes-api
deployment.apps/hello-kubernetes-api restarted
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl get pods -o wide                               
NAME                                    READY   STATUS              RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
hello-kubernetes-api-7bc68f498f-jnlr4   0/1     ContainerCreating   0          11s   <none>        node2   <none>           <none>
hello-kubernetes-api-989745789-7xfdf    1/1     Running             0          22h   10.244.1.43   node1   <none>           <none>
hello-kubernetes-api-989745789-qqd9g    1/1     Running             0          22h   10.244.1.44   node1   <none>           <none>
hello-kubernetes-api-989745789-x7ch6    1/1     Running             0          22h   10.244.1.42   node1   <none>           <none>
nginx-bf5d5cf98-82fh9                   1/1     Running             0          22h   10.244.1.45   node1   <none>           <none>
nginx-bf5d5cf98-9dmsl                   1/1     Running             0          22h   10.244.1.41   node1   <none>           <none>
nginx-bf5d5cf98-whzjz                   1/1     Running             0          22h   10.244.1.46   node1   <none>           <none>
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl get pods -o wide
NAME                                    READY   STATUS              RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
hello-kubernetes-api-7bc68f498f-jnlr4   0/1     ContainerCreating   0          21s   <none>        node2   <none>           <none>
hello-kubernetes-api-989745789-7xfdf    1/1     Running             0          22h   10.244.1.43   node1   <none>           <none>
hello-kubernetes-api-989745789-qqd9g    1/1     Running             0          22h   10.244.1.44   node1   <none>           <none>
hello-kubernetes-api-989745789-x7ch6    1/1     Running             0          22h   10.244.1.42   node1   <none>           <none>
nginx-bf5d5cf98-82fh9                   1/1     Running             0          22h   10.244.1.45   node1   <none>           <none>
nginx-bf5d5cf98-9dmsl                   1/1     Running             0          22h   10.244.1.41   node1   <none>           <none>
nginx-bf5d5cf98-whzjz                   1/1     Running             0          22h   10.244.1.46   node1   <none>           <none>
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl get pods -o wide
NAME                                    READY   STATUS        RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
hello-kubernetes-api-7bc68f498f-8cgwh   1/1     Running       0          41s   10.244.2.51   node3   <none>           <none>
hello-kubernetes-api-7bc68f498f-ccmxq   0/1     Pending       0          9s    <none>        node1   <none>           <none>
hello-kubernetes-api-7bc68f498f-jnlr4   1/1     Running       0          92s   10.244.3.28   node2   <none>           <none>
hello-kubernetes-api-989745789-7xfdf    1/1     Terminating   0          22h   10.244.1.43   node1   <none>           <none>
hello-kubernetes-api-989745789-qqd9g    1/1     Running       0          22h   10.244.1.44   node1   <none>           <none>
hello-kubernetes-api-989745789-x7ch6    1/1     Terminating   0          22h   10.244.1.42   node1   <none>           <none>
nginx-bf5d5cf98-82fh9                   1/1     Running       0          22h   10.244.1.45   node1   <none>           <none>
nginx-bf5d5cf98-9dmsl                   1/1     Running       0          22h   10.244.1.41   node1   <none>           <none>
nginx-bf5d5cf98-whzjz                   1/1     Running       0          22h   10.244.1.46   node1   <none>           <none>
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi> kubectl get pods -o wide
NAME                                    READY   STATUS    RESTARTS   AGE   IP            NODE    NOMINATED NODE   READINESS GATES
hello-kubernetes-api-7bc68f498f-8cgwh   1/1     Running   0          26m   10.244.2.51   node3   <none>           <none>
hello-kubernetes-api-7bc68f498f-ccmxq   1/1     Running   0          26m   10.244.1.47   node1   <none>           <none>
hello-kubernetes-api-7bc68f498f-jnlr4   1/1     Running   0          27m   10.244.3.28   node2   <none>           <none>
nginx-bf5d5cf98-82fh9                   1/1     Running   0          23h   10.244.1.45   node1   <none>           <none>
nginx-bf5d5cf98-9dmsl                   1/1     Running   0          23h   10.244.1.41   node1   <none>           <none>
nginx-bf5d5cf98-whzjz                   1/1     Running   0          23h   10.244.1.46   node1   <none>           <none>
PS C:\Users\user\Projects\k8sTest\HelloKubernates\api\HelloKubernetesApi>

 

실행 로그를 보면 rollout restart 후 pod가 하나씩 생성(ContainerCreating)되면서 새로운 pod로 교체되고 이전 것은 종료 (Terminating)되는 모습을 볼 수 있습니다. 

 

삽질 끝.

 

교훈. AI는 위대하다

후기. 이러니 AI 한테 존대말을 쓸 수 밖에 없음

 

5. 변경된 배포본 확인

브라우저로 접속해 보면 변경된 내용이 잘 출력됩니다.

 

이제 GitHub의 레파지토리 main에 PUSH를 하면 알아서 빌드를 해서 이미지를 교체됩니다.

kubectl rollout restart ... 명령으로 컨테이너가 각 새로운 pod를 생성하고 기존의 pod는 종료 하면서 새로운 이미지로 서비스를 하게 됩니다.

 

 

여기까지 해서 쿠버네티스 클러스커 구축 글 리스트는 완료입니다.

추가로 필요 한 것이 있으면 번외로 진행할 예정입니다.

혹시 여기까지 따라오신 분들 진짜 고생하셨고, 원하시는 결과물을 얻으셨길 기원합니다.

감사합니다.

 

다음에는 kuberflow 를 알아보도록 할까... 합니다.

끝.

+ Recent posts