# Table of Contents

WARNING

이 포스트는 공식 문서 (opens new window)를 참고하여 작성되었습니다.

# CI/CD 파이프라인

Github ActionsArgo CD를 사용하여 스프링부트 프로젝트를 쿠버네티스 클러스터에 배포한다. 공식 문서에서 제공하는 CI/CD 파이프라인의 흐름은 다음과 같다.

  1. 개발자는 소스코드 리포지토리에 푸시한다.
  2. 소스코드 리포지토리의 GitHub Actions가 동작하여 도커 이미지를 생성하고 ECR에 푸시한다.
  3. GitHub Actions는 메니페스트 리포지토리에 이미지를 쿠버네티스 클러스터에 배포하기 위한 메니페스트 파일을 생성한다.
  4. Argo CD가 메니페스트 리포지토리 변화를 감지하여 쿠버네티스 클러스터에 반영한다.

# GitOps

위처럼 배포, 운영과 관련된 모든 절차를 선언적으로 코드화하여 Git에서 관리하는 것을 GitOps라고 한다.

GitOps의 핵심은 두 개의 저장소(소스코드 저장소, 메니페스트 저장소)을 사용하는 것이다.

# 환경

  • Spring Boot
  • Github Actions
  • AWS ECR
  • Multi AWS EC2 Kubernetes cluster (Not EKS)
  • Argo CD

# CI 구축하기

CI(Continuous Integration)을 먼저 구축해보자.

Dockerfile은 다음과 같다.

FROM openjdk:8-jdk-alpine
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-Dspring.profiles.active=prod",  "-jar","/app.jar"]

Github Actions 스크립트는 다음과 같다.

# main.yml
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Add properties file
        run: echo '${{ secrets.APPLICATION_PROD_PROPERTIES }}' > ./src/main/resources/application-prod.properties

      # Jar 빌드
      - name: Build with Gradle
        run: ./gradlew clean build

      # AWS IAM 인증
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      # AWS ECR 로그인
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      # 이미지 태그 생성
      - name: Make image tag
        id: image
        run: |
          VERSION=$(echo ${{ github.sha }} | cut -c1-8)
          echo VERSION=$VERSION
          echo "::set-output name=version::$VERSION"

      # 도커 이미지 빌드 & AWS ECR에 푸시
      - name: Build and Push images to AWS ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
          IMAGE_TAG: ${{ steps.image.outputs.version }}
        run: |
          echo "::set-output name=ecr_repository::$ECR_REPOSITORY"
          echo "::set-output name=image_tag::$IMAGE_TAG"
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

빌드가 성공했다면 ECR에 이미지가 저장되었을 것이다.

# 네임스페이스 생성하기

쿠버네티스 클러스터에 네임스페이스를 생성한다. 모든 오브젝트는 이 네임스페이스에서 실행된다.

$ kubectl create namespace <NAMESPACE>

# ECR 접근 권한 설정

ECR에서 쿠버네티스로 이미지를 Pull 하려면 접근권한 등록이 필요하다. 먼저 클러스터에서 AWS IAM 사용자의 Access key, Secret key를 등록하자.

$ aws configure
AWS Access Key ID [None]: <ACCESS_KEY>
AWS Secret Access Key [None]: <SECRET_KEY>
Default region name [None]: <REGION>
Default output format [None]: 

그 다음 AWS ECR에 로그인한다.

$ aws ecr get-login-password --region <REGION> | docker login --username AWS --password-stdin <AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com

로그인 정보는 ~/.docker/config.json에 저장된다.

$ cat ~/.docker/config.json

이제 ECR에 접근하기 위한 비밀번호를 얻기 위해 다음 명령어를 입력한다.

$ aws ecr get-login-password --region <REGION>

그리고 이 비밀번호를 사용하여 위에서 생성한 네임스페이스에 Secret을 생성한다.

$ kubectl create secret docker-registry regcred --namespace=<NAMESPACE> --docker-server=<SERVER> --docker-username=AWS --docker-password=<PASSWORD>



 

$ kubectl get secret -n <NAMESPACE>
NAME                  TYPE                                  DATA   AGE
default-token-jvv9p   kubernetes.io/service-account-token   3      168m
regcred               kubernetes.io/dockerconfigjson        1      15m

생성한 Secret은 포스트 뒷 부분에서 사용한다.

# 메니페스트 리포지토리 구축

메니페스트 파일은 도커 이미지를 쿠버네티스 클러스터에 어떤 식으로 배포할지를 선언한 파일이며, 보통 쿠버네티스 오브젝트를 선언한 YAML 설정파일Kustomize 관련 파일을 의마한다. 메니페스트 리포지토리에는 이 파일이 저장된다.

이제 메니페스트 리포지토리를 생성하고 CI/CD 파이프라인을 구축해보자.

먼저 로컬 PC에서 다음과 같은 구조의 디렉토리를 생성한다.

$ tree
.
├── base
│   ├── deployment.yml
│   ├── kustomization.yml
│   └── service.yml
└── overlays
    └── prod
        └── kustomization.yml

base/deployment.yml은 다음과 같다. 위해서 생성한 Secret을 반드시 등록해준다.





















 
 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: springboot-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: springboot-app-label
  template:
    metadata:
      name: 
      labels: 
        app: springboot-app-label
    spec:
      containers:
        - name: springboot-app
          image: <YOUR_AWS_IAM_USER_ID>.dkr.ecr.<YOUR_ECR_REGION>.amazonaws.com/<YOUR_ECR_REPOSITORY>:0.0
          ports:
          - containerPort: 8080
      imagePullSecrets:
      - name: regcred

base/service.yml은 다음과 같다.

apiVersion: v1
kind: Service
metadata:
  name: springboot-service
spec:
  ports:
    - name: springboot-service-port
      port: 8080
      targetPort: 8080
  selector:
    app: springboot-app-label
  type: NodePort

base/kustomization.yml은 다음과 같다.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yml
  - service.yml

overlays/prod/kustomization.yml은 다음과 같다.

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: <YOUR_AWS_IAM_USER_ID>.dkr.ecr.<YOUR_ECR_REGION>.amazonaws.com/<YOUR_ECR_REPOSITORY>
  newName: <YOUR_AWS_IAM_USER_ID>.dkr.ecr.<YOUR_ECR_REGION>.amazonaws.com/<YOUR_ECR_REPOSITORY>
  newTag: abcdefg
resources:
- ../../base

이제 깃헙에 매니페스트용 사설 리포지토리를 생성하고 코드를 Push한다.

DANGER

소스코드에 AWS IAM 사용자의 ID가 포함되므로 반드시 사설 리포지토리로 생성한다.

이제 소스코드 리포지토리에 코드를 Push했을 때 매니페스트용 리포지토리로 매니페스트 파일들을 푸시하도록 Github Actions 스크립트를 추가한다.

# main.yml
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:

      # 생략 ...

      - name: Setup Kustomize
        uses: imranismail/setup-kustomize@v1

      - name: Checkout kustomize repository
        uses: actions/checkout@v2
        with:
          repository: <GITHUB_USERNAME>/<GITHUB_REPOSITORY_NAME>
          ref: main
          token: ${{ secrets.MANIFEST_REPO_PERSONAL_ACCESS_TOKEN }}
          path: <GITHUB_REPOSITORY_NAME>

      - name: Update Kubernetes resources
        run: |
          echo ${{ steps.login-ecr.outputs.registry }}
          echo ${{ steps.build-image.outputs.ecr_repository }}
          echo ${{ steps.build-image.outputs.image_tag }}
          cd <GITHUB_REPOSITORY_NAME>/overlays/prod/
          kustomize edit set image ${{ steps.login-ecr.outputs.registry}}/${{ steps.build-image.outputs.ecr_repository }}=${{ steps.login-ecr.outputs.registry}}/${{ steps.build-image.outputs.ecr_repository }}:${{ steps.build-image.outputs.image_tag }}
          cat kustomization.yml

      - name: Commit files
        run: |
          cd <GITHUB_REPOSITORY_NAME>
          git config --global user.email "<GITHUB_EMAIL>"
          git config --global user.name "<GITHUB_USENNAME>"
          git commit -am "Update image tag"
          git push -u origin main

Github Secret에 MANIFEST_REPO_PERSONAL_ACCESS_TOKEN를 추가한다. PAT(Personal Access Token) (opens new window)은 이 포스트를 참고하여 생성할 수 있다.

이제 소스코드 리포지토리에 다시 한번 푸시해보자. 메니페스트용 리포지토리에 KubernetesKustomize 설정파일이 생성되면 성공한 것이다.

# Argo CD를 사용하여 배포하기

Argo CD는 메니페스트 리포지토리가 변경되면 이를 감지하여 쿠버네티스 클러스터에 반영하는 역할을 한다.

먼저 쿠버네티스 클러스터에서 Argo CD를 설치하자.

// 네임스페이스 생성
$ kubectl create namespace argocd

// Argo CD와 관련된 오브젝트 설치
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

Argo CD와 관련된 오브젝트가 argocd 네임스페이스에 설치된다.

$ kubectl get all -n argocd
NAME                                                    READY   STATUS    RESTARTS   AGE
pod/argocd-application-controller-0                     1/1     Running   0          37h
pod/argocd-applicationset-controller-79f97597cb-gntdl   1/1     Running   0          37h
pod/argocd-dex-server-6fd8b59f5b-gwwr9                  1/1     Running   0          37h
pod/argocd-notifications-controller-5549f47758-4gqxv    1/1     Running   0          37h
pod/argocd-redis-79bdbdf78f-k8ftl                       1/1     Running   0          37h
pod/argocd-repo-server-5569c7b657-2df8d                 1/1     Running   0          37h
pod/argocd-server-664b7c6878-krfp7                      1/1     Running   0          37h

NAME                                              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
service/argocd-applicationset-controller          ClusterIP   10.107.177.230   <none>        7000/TCP                     37h
service/argocd-dex-server                         ClusterIP   10.99.2.29       <none>        5556/TCP,5557/TCP,5558/TCP   37h
service/argocd-metrics                            ClusterIP   10.106.79.143    <none>        8082/TCP                     37h
service/argocd-notifications-controller-metrics   ClusterIP   10.107.128.107   <none>        9001/TCP                     37h
service/argocd-redis                              ClusterIP   10.99.61.196     <none>        6379/TCP                     37h
service/argocd-repo-server                        ClusterIP   10.105.139.69    <none>        8081/TCP,8084/TCP            37h
service/argocd-server                             ClusterIP   10.96.104.252    <none>        80:30609/TCP,443:30992/TCP   37h
service/argocd-server-metrics                     ClusterIP   10.110.209.117   <none>        8083/TCP                     37h

NAME                                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/argocd-applicationset-controller   1/1     1            1           37h
deployment.apps/argocd-dex-server                  1/1     1            1           37h
deployment.apps/argocd-notifications-controller    1/1     1            1           37h
deployment.apps/argocd-redis                       1/1     1            1           37h
deployment.apps/argocd-repo-server                 1/1     1            1           37h
deployment.apps/argocd-server                      1/1     1            1           37h

NAME                                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/argocd-applicationset-controller-79f97597cb   1         1         1       37h
replicaset.apps/argocd-dex-server-6fd8b59f5b                  1         1         1       37h
replicaset.apps/argocd-notifications-controller-5549f47758    1         1         1       37h
replicaset.apps/argocd-redis-79bdbdf78f                       1         1         1       37h
replicaset.apps/argocd-repo-server-5569c7b657                 1         1         1       37h
replicaset.apps/argocd-server-664b7c6878                      1         1         1       37h

Argo CD는 웹 브라우저에서 GUI 형태로 관련된 오브젝트를 관리할 수 있도록 Argo CD API server를 제공한다.

Argo CD가 설치되면 기본적으로 API server 접근하기 위한 서비스가 Cluster IP타입으로 설정된다. 클러스터 외부에서도 API server에 접근할 수 있도록 서비스 타입을 NodePort로 바꿔주자.

$ kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "NodePort"}}'








 


$ kubectl get service -n argocd
NAME                                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
argocd-applicationset-controller          ClusterIP   10.107.177.230   <none>        7000/TCP                     8m40s
argocd-dex-server                         ClusterIP   10.99.2.29       <none>        5556/TCP,5557/TCP,5558/TCP   8m40s
argocd-metrics                            ClusterIP   10.106.79.143    <none>        8082/TCP                     8m40s
argocd-notifications-controller-metrics   ClusterIP   10.107.128.107   <none>        9001/TCP                     8m40s
argocd-redis                              ClusterIP   10.99.61.196     <none>        6379/TCP                     8m40s
argocd-repo-server                        ClusterIP   10.105.139.69    <none>        8081/TCP,8084/TCP            8m40s
argocd-server                             NodePort    10.96.104.252    <none>        80:30609/TCP,443:30992/TCP   8m40s
argocd-server-metrics                     ClusterIP   10.110.209.117   <none>        8083/TCP                     8m40s

이제 클러스터 외부에서 <워커 노드 IP>:<외부노출 포트> 형태로 API server에 접근할 수 있다.

WARNING

EC2로 클러스터를 구축한 경우 보안 그룹에서 해당 포트를 개방해야한다.

제대로 접근한 경우 다음과 같이 로그인 화면이 나온다.

기본 계정은 admin이며, 비밀번호는 다음 명령어로 알아낼 수 있다.

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

물론 커맨드라인으로 Argo CD를 제어할 수 있다. 로컬 PC에 Argo CD CLI를 설치하면 된다.

$ brew tap argoproj/tap 

$ brew install argoproj/tap/argocd

$ argocd version
argocd: v2.3.3+07ac038.dirty
  BuildDate: 2022-03-30T05:20:18Z
  GitCommit: 07ac038a8f97a93b401e824550f0505400a8c84e
  GitTreeState: dirty
  GoVersion: go1.18
  Compiler: gc
  Platform: darwin/amd64
FATA[0000] Argo CD server address unspecified  

$ argocd login <워커 노드IP>:30609
WARNING: server certificate had error: x509: “Argo CD” certificate is not trusted. Proceed insecurely (y/n)? y
Username: admin
Password: 
'admin:login' logged in successfully

비밀번호를 변경해보자.

$ argocd account update-password
*** Enter password of currently logged in user (admin): <기존 비밀번호> 
*** Enter new password for user admin: <새로운 비밀번호>
*** Confirm new password for user admin: <새로운 비밀번호>

이제 Argo CD와 메니페스트 저장소를 연동할 차례다. 리포지토리 탭에서 Repositories를 클릭한다.

CONNECTION REPO USING HTTPS를 선택한다.

퍼블릭 리포지토리인 경우 리포지토리 저장소 주소만 입력하면 저장소가 추가된다.

프라이빗 리포지토리의 경우 SSH을 통해 저장소를 추가해야한다. 먼저 로컬 PC에서 공개키, 개인키 쌍을 생성하자.

$ ssh-keygen -t ecdsa -b 521 -C "<YOUR_EMAIL>"
Generating public/private ecdsa key pair.
Enter file in which to save the key (/Users/yologger/.ssh/id_ecdsa):<KEY_NAME>

키 생성이 완료되면 개인키와 공개키가 생성된다. .pub 확장자가 붙은 키가 공개키, 나머지가 개인키다.

$ ls
<KEY_NAME>.pub
<KEY_NAME>

공개키를 메니페스트 리포지토리에 등록할 차례다. Settings > Deploy Keys > Add deploy key로 이동하여 공개키 파일의 내용을 등록한다.

이제 Argo CD CLI로 프라이빗 리포지토리를 추가할 수 있다.

// argocd repo add git@github.com:<YOUR_ID>/<YOUR_REPOSTIORY>.git --ssh-private-key-path <키 경로>
$ argocd repo add git@github.com:yologger/manifest_repo.git --ssh-private-key-path ./github-ssh-key
Repository 'git@github.com:yologger/manifest_repo.git.git' added

프라이빗 리포지토리가 추가되었는지 확인하자.

$ argocd repo list
TYPE  NAME  REPO                                        INSECURE  OCI    LFS    CREDS  STATUS      MESSAGE  PROJECT
git         git@github.com:yologger/manifest_repo.git   false     false  false  false  Successful 

WARNING

Argo CD는 3분마다 Github와 통신하며 리포지토리 변경사항을 확인한다. 따라서 EC2에 클러스터를 구축한 경우 보안 그룹을 적절히 설정해주어야 한다.

저장소가 추가되었다면 Argo CD API 서버에서 Application 탭 > NEW APP을 선택한다.

Application Name을 입력하고 SYNC POLICYAutomatic으로 설정한다. Automatic은 3분에 한번씩 연결된 저장소가 변경되었는지 자동으로 비교하고 반영하는 옵션이다.

  • Prune Resources: 변경이 발생하여 리소스를 업데이트할 때, 기존 리소스를 삭제하고 새로운 리소스를 생성한다.
  • Self Heal: 오브젝트가 다운되었을 때 Argo CD가 스스로 복구해준다.

등록한 저장소를 추가하고, 저장소 내 메니페스트 파일 경로를 입력한다.

클러스터를 선택하고 사용할 네임스페이스를 지정하고 CREATE 버튼을 누른다.

이제 메니페스트 리포지토리를 반영하여 쿠버네티스 배포를 시작한다.

나타나면 쿠버네티스 배포에 성공했다면 Sync OK 상태가 된다.

$ kubectl get all -n spring
NAME                                         READY   STATUS    RESTARTS   AGE
pod/springboot-deployment-6d7b75c58d-f5hbj   1/1     Running   0          2m4s
pod/springboot-deployment-6d7b75c58d-kl27z   1/1     Running   0          2m1s

NAME                         TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)           AGE
service/springboot-service   NodePort   10.100.154.16   <none>        10000:30070/TCP   10m

NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/springboot-deployment   2/2     2            2           10m

NAME                                               DESIRED   CURRENT   READY   AGE
replicaset.apps/springboot-deployment-6d7b75c58d   2         2         2       2m4s
replicaset.apps/springboot-deployment-7f75b4d546   0         0         0       10m

마지막으로 소스코드를 수정하고 소스코드 리포지토리에 푸시하여 CI/CD 파이프라인이 잘 작동하는지 확인해보자.

# Argo Rollout 적용하기

기본적으로 디플로이먼트는 팟을 롤링 업데이트(Rolling Update) 방식으로 교체한다. Argo Rollout을 적용하면 Blue/Green 방식으로 무중단 배포할 수 있다.

우선 Argo Rollout을 설치한다.

$ kubectl create namespace argo-rollouts

$ kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml

그 다음 kubectlArgo Rollout 플러그인을 설치한다.

$ curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64

$ chmod +x ./kubectl-argo-rollouts-linux-amd64

$ sudo mv ./kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

$ kubectl argo rollouts version
kubectl-argo-rollouts: v1.2.0+08cf10e
  BuildDate: 2022-03-22T00:25:11Z
  GitCommit: 08cf10e554fe99c24c8a37ad07fadd9318e4c8a1
  GitTreeState: clean
  GoVersion: go1.17.6
  Compiler: gc
  Platform: linux/amd64

그리고 디플로이먼트 메니페스트를 다음과 같이 수정한다.

 
 
 
 




















 
 
 
 
 

# apiVersion: apps/v1
apiVersion: argoproj.io/v1alpha1
# kind: Deployment
kind: Rollout 
metadata:
  name: h2h-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: springboot-app-label
  template:
    metadata:
      name: 
      labels: 
        app: springboot-app-label
    spec:
      containers:
        - name: springboot-app
          image: <YOUR_AWS_IAM_USER_ID>.dkr.ecr.<YOUR_ECR_REGION>.amazonaws.com/<YOUR_ECR_REPOSITORY>:0.0
          ports:
          - containerPort: 8080
      imagePullSecrets:
      - name: regcred
  strategy:
    blueGreen: 
      activeService: rollout-bluegreen-active
      previewService: rollout-bluegreen-preview
      autoPromotionEnabled: false