GitHub Action을 통해 CICD 해보기

이제 CI/CD를 위한 모든 세팅이 끝났다. GitHub Action 스크립트를 작성해 실제로 CI/CD 파이프라인을 작동시키면 된다.

Dockerfile 작성

가장 먼저 우리의 서비스 애플리케이션의 컨테이너 이미지를 만들기 위한 Dockerfile을 작성해보자.

plain-jar 생성하지 않기

티어리스트 프로젝트는 gradle을 빌드 시스템으로 이용하고 있다. gradle을 빌드 시 jar와 plain-jar을 함께 생성한다. Dockerfile이 /build/libs에서 wildcard인 *를 통해 jar파일을 가져올 수 있게 plain-jar 생성을 막아보자.

build.gradle에 다음과 같이 설정하면 된다.

1
2
3
jar {  
    enabled = false  
}

위와 같이 추가하고 다시 빌드해보면 plain-jar가 생성되지 않음을 확인할 수 있다.

Dockerfile 작성

프로젝트 루트에 Dockerfile을 다음과 같이 작성한다.

1
2
3
4
5
6
FROM eclipse-temurin:17.0.10_7-jre-focal as build
WORKDIR /app
COPY  build/libs/*.jar app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]

eclipse-temurin:17.0.10_7-jre-focal이미지를 기반으로 jar파일을 가져오고 컨테이너 실행 시 해당 jar파일을 실행한다. 포트는 8080을 열어두었다.

GitHub Action 스크립트 작성

PR 요청 시 파이프라인

PR 요청 시 Sonarqube로 부터 피드백을 받을 수 있게 위와 같이 구성할 것이다.

  1. 개발자가 source repository에 PR을 작성한다.
  2. GitHub Action 트리거가 작동한다.
  3. Build와 Test를 마치고 Sonarqube에 정적 분석을 요청한다.
  4. Sonarqube의 플러그인은 정적 분석 결과를 PR Comment로 추가한다.
  5. 개발자는 피드백에 따라 코드를 리팩토링 하고 푸시한다.
  6. 다시 2번으로 돌아간다.

위와 같이 피드백을 받고 피드백을 수용하는 과정을 거칠 수 있도록 구성할 것이다.

프로젝트 루트에 .github/workflows 폴더를 만든다.

pr-sonarbot.yaml파일에 다음과 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
name: pr-sonarbot  
  
on:  
  pull_request:  
    types: [opened, synchronize, reopened]  
jobs:  
  check-code-smell:  
  
    runs-on: ubuntu-latest  
  
    steps:  
      - name: Checkout  
        uses: actions/checkout@v4  
  
      - name: Setup Java17  
        uses: actions/setup-java@v4  
        with:  
          distribution: temurin  
          java-version: 17  
  
      - name: Setup Gradle  
        uses: gradle/gradle-build-action@v3  
  
      - name: Cache SonarQube packages  
        uses: actions/cache@v4  
        with:  
          path: ~/.sonar/cache  
          key: $-sonar  
          restore-keys: $-sonar  
  
      - name: Cache Gradle packages  
        uses: actions/cache@v4  
        with:  
          path: ~/.gradle/caches  
          key: $-gradle-$  
          restore-keys: $-gradle  
  
      - name: Analyze with Sonarqube  
        env:  
          SONAR_TOKEN: $  
          SONAR_HOST_URL: $  
        run: ./gradlew build sonar --info  
  
      - name: SonarQube Quality Gate check  
        id: sonarqube-quality-gate-check  
        uses: sonarsource/sonarqube-quality-gate-action@master  
        timeout-minutes: 5  
        with:  
          scanMetadataReportFile: build/sonar/report-task.txt  
        env:  
          SONAR_TOKEN: $  
          SONAR_HOST_URL: $ #OPTIONAL

빌드 후 소나큐브에 정적 분석을 요청하는 간단한 워크플로우이다. 이제 해당 Repository에 PR을 날려보면 아래와 같이 정적 분석 결과를 코멘트로 확인할 수 있다.

Merge 시 파이프라인

PR의 개발 및 리팩토링이 끝나고 머지가 되면 위와 같은 CI/CD 워크플로우가 작동한다.

  1. develop 브랜치에 변경사항이 생기면 GitHub Action 트리거가 작동한다.
  2. 서비스 애플리케이션을 빌드 테스트한 후 Sonarqube에 정적 분석을 맡긴다.
  3. Quality Gate를 통과하지 못하면 워크플로우는 종료되고 Discord에 알림을 보낸다.
  4. Quality Gate 통과 시 Dockerfile을 기반으로 컨테이너 이미지를 생성해 Docker Registry에 등록한다.
  5. GitOps 레포지토리에 Docker Image의 태그를 업데이트한다.
  6. ArgoCD는 GitOps Repository에 변경을 감지하고 매니페스트 파일들을 쿠버네티스 클러스터에 배포한다.

프로젝트 루트에서 .github/workflows/pr-sonarbot.yaml 파일에 다음과 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
name: ci  
  
on:  
  push:  
    branches: [ develop ]  
jobs:  
  ci:  
  
    runs-on: ubuntu-latest  
  
    steps:  
      - name: Checkout  
        uses: actions/checkout@v4  
  
      - name: Setup Java17  
        uses: actions/setup-java@v4  
        with:  
          distribution: temurin  
          java-version: 17  
  
      - name: Setup Gradle  
        uses: gradle/gradle-build-action@v3  
  
      - name: Build With Gradle  
        run: ./gradlew clean build  
  
      - name: Test With Gradle  
        run: ./gradlew test  
  
      - name: Cache SonarQube packages  
        uses: actions/cache@v4  
        with:  
          path: ~/.sonar/cache  
          key: $-sonar  
          restore-keys: $-sonar  
  
      - name: Cache Gradle packages  
        uses: actions/cache@v4  
        with:  
          path: ~/.gradle/caches  
          key: $-gradle-$  
          restore-keys: $-gradle  
  
      - name: Analyze with Sonarqube  
        env:  
          SONAR_TOKEN: $  
          SONAR_HOST_URL: $  
        run: ./gradlew build sonar --info  
  
      - name: SonarQube Quality Gate check  
        id: sonarqube-quality-gate-check  
        uses: sonarsource/sonarqube-quality-gate-action@master  
        timeout-minutes: 5  
        with:  
          scanMetadataReportFile: build/sonar/report-task.txt  
        env:  
          SONAR_TOKEN: $  
          SONAR_HOST_URL: $
  
      - name: Docker meta  
        id: meta  
        uses: docker/metadata-action@v5  
        with:  
          images: duk9741/tierlist-api  
          tags: |  
            $  
  
      - name: Set up QEMU  
        uses: docker/setup-qemu-action@v3  
  
      - name: Set up Docker Buildx  
        uses: docker/setup-buildx-action@v3  
  
      - name: Login to DockerHub  
        if: github.event_name != 'pull_request'  
        uses: docker/login-action@v3  
        with:  
          username: $  
          password: $  
  
      - name: Build and push  
        uses: docker/build-push-action@v5  
        with:  
          context: .  
          push: $  
          tags: $  
          labels: $  
          platforms: |  
            linux/amd64            linux/arm64            linux/arm/v7  
      - name: Checkout Git Ops Repo  
        uses: actions/checkout@v4  
        with:  
          repository: tierlist-projects/tierlist-git-ops  
          token: $  
  
      - name: Update Git Ops Repo  
        run: |  
          pwd          ls          git config --global user.email "actions@github.com"          git config --global user.name "GitHub Actions"          sed -i "s+duk9741/tierlist-api.*+$+g" app/Deployment.yaml  
          git add .          git commit -m "action: update image tag to $"  
          git push origin main  
      - name: Send Webhook to Discord  
        uses: sarisia/actions-status-discord@v1  
        if: always()  
        with:  
          webhook: $

눈여겨 볼 점은 다음과 같다.

  • 속도를 위해 캐싱을 사용한다.
  • M2 Mac에서 동작하기 위해서 Docker Buildx를 통해 ARM 등의 여러 플랫폼으로 빌드한다.
  • 도커 이미지의 태그는 GitHub Action run_number로 한다.
  • 도커 레지스트리에 컨테이너 이미지를 등록할 때 DOCKERHUB_USERNAMEDOCKERHUB_PASSWORD를 사용한다. 이를 레포지토리 Secret에 등록해야 한다.
  • 이미지 PUSH가 완료되면 GitOps 레포지토리에 접속해 이미지의 태그를 변경한다.
  • 마지막으로 결과를 Discord로 보낸다. 세부 설정은 sarisia/actions-status-discord를 확인해 보자. 레포지토리 Secret에 Discord Webhook을 생성해 넣어줘야 한다.

위와 같이 설정하면 GitHub action 설정을 마친 것이다.

GitOps 레포지토리 매니페스트 등록

Spring Boot 앱을 위한 매니페스트를 GitOps Repository를 등록해보자.

레포지토리 루트에 app디렉토리를 만들고 Deployment.yaml파일을 다음과 같이 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tierlist-api-deployment
  namespace: tierlist-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tierlist-api
  template:
    metadata:
      labels:
        app: tierlist-api
      annotations:
    spec:
      serviceAccountName: internal-app
      containers:
        - name: tierlist-api
          image: duk9741/tierlist-api:27
          env:
          - name: REDIS_HOST
            valueFrom:
              configMapKeyRef:
                name: tierlist-configmap
                key: redis_host
          - name: REDIS_PORT
            valueFrom:
              configMapKeyRef:
                name: tierlist-configmap
                key: redis_port
          - name: GMAIL_USERNAME
            valueFrom:
              secretKeyRef:
                name: tierlist-gmail-secret
                key: username
          - name: GMAIL_PASSWORD
            valueFrom:
              secretKeyRef:
                name: tierlist-gmail-secret
                key: password
          - name: JWT_SECRET
            valueFrom:
              secretKeyRef:
                name: tierlist-jwt-secret
                key: secret
          ports:
            - containerPort: 8080

눈여겨 볼점은 다음과 같다.

  • repllcas는 1로 설정하고 있다.
  • 환경변수를 위해 ConfigMap을 작성해 환경 변수를 설정하고 있다.
  • 민감한 정보는 Secret을 이용해 설정하고 있다.

세부적인 배포 전략은 수정할 수 있다. replica를 늘린다던가 env로 profile정보를 넣을 수도 있다. 입맛에 맞게 변경해서 사용해보자.

Secret은 민감한 정보이므로 GitOps 레포지토리에서 관리하면 안된다. Vault를 이용하던지 기본적인 Secret을 쿠버네티스에서 생성해 이용하자.

app 디렉토리에 ConfigMap.yaml을 설정해 ArgoCD가 앱과 함께 배포할 수 있게 설정할 수 있다. 예시는 다음과 같다.

1
2
3
4
5
6
7
8
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: tierlist-api
  name: tierlist-configmap
data:
    redis_host: redis-service
    redis_port: '6379'

ConfigMap의 유의할 점은 숫자는 바로 쓰지 못하므로 따옴표로 감싸주어야 한다는 점이다.

이제 서비스와 Ingress를 설정해주자.

Service.yaml에 다음과 같이 작성했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
  name: tierlist-api-service
  namespace: tierlist-api
spec:
  selector:
    app: tierlist-api
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: NodePort

Ingress.yaml에 다음과 같이 작성했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tierlist-api-ingress
  namespace: tierlist-api
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-passthrough: "true"
    nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
spec:
  ingressClassName: nginx
  rules:
  - host: [원하는 host]
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: tierlist-api-service
            port:
             number: 80
  tls:
    - hosts:
      - [원하는 host]
      secretName: tierlist-api-cert

특이점은 Spring Boot 앱이 http를 사용하기 때문에 backend-protocolHTTP로 설정해주었다는 것이다.

이제 마지막으로 해당 매니페스트들을 바라볼 수 있게 ArgoCD APP을 생성해보자. Sonarqube ArgoCD APP 생성을 참고해 APP을 생성할 수 있다.

이제 develop에 Merge 또는 push를 하면 다음과 같이 GitHub Action 워크플로우가 작동한다.

또한 ArgoCD를 확인해보면 아래와 같이 매니페스트들이 잘 배포되었음을 확인할 수 있다.

회고

이로써 Kubernets, ArgoCD, GitHub Action, Sonarqube를 통한 CICD 구축기를 마친다. 엄청난 오류와 씨름하고 얻어낸 결과이다. 하지만 다 해놓고 나니 안정적으로 서비스가 운영될 수 있음에 뿌듯함을 느꼇다.

추후에 도입하고 싶은 것은 다음과 같다.

  • docker jib 도입
  • vault 도입

위는 프로젝트가 어느정도 안정화가 된 후 천천히 도입해볼 생각이다. 이로써 길고 길었던 티어리스트 프로젝트 CICD 구축기를 마친다.

댓글남기기