CI-CD

CI/CD

박성훈CloudWave 2024. 8. 11. 23:32

출처: https://www.atlassian.com/devops

CI/CD 포스팅을 작성하게 된 계기

좋은 서비스란 무엇일까요? 짧은 시간동안 부하를 견딜 수 있는 서비스? 무엇보다 고객에게 유의미하고 빠른 응답을 주는 서비스? 모두 정답입니다.

하지만 이번 포스팅에서는 서비스의 지속 가능성(Sustainability)에 대해서 다뤄보도록 하겠습니다.

 

지속 가능성에서 가장 중요한 것은 무엇일까요? 현대적인 아키텍쳐는 점점 추상화되는 과정을 거칩니다. 그리고 이 추상화는 보다 가볍고 간편하고 빠른 시스템을 위해 관심사를 분리하는 방향으로 개선됩니다. 지속 가능성도 이와 같은 개념입니다.

 

우리가 동적인 요청을 처리하기 위해 스프링 프레임워크를 통해 개발을 했다고 가정해보겠습니다. 우리가 작성한 로직에 유저가 접근하기 위해서 이 코드를 배포하는 과정을 거쳐야 합니다. 자바에서는 명령어를 통해 코드를 실행가능한 jar파일로 빌드하고.. 서버에 접속하고.. 직접 배포하고.. 기타 등등 익숙하다면 빠르겠지만 과연 이게 좋은 방법일까요? 이 작업은 사실 꽤나 귀찮은 지점들이 있고 사실 그렇게 빠르지도 않습니다.

 

만약 빌드, 테스트, 딜리버리, 배포 이 귀찮은 과정들을 개발계에서는 신경쓰지 않아도 된다고 한다면, 그저 코드만 작성해서 push했을 때 이 모든 작업들이 이뤄진다면 적절한 관심사의 분리가 될 수 있을 것입니다. 여기가 DevOps의 진가가 발휘하는 대목입니다.


DevOps란 무엇인가?

DevOps란 Development Operations의 약어로, 소프트웨어 개발과 운영을 통합하여 효율성, 협력, 속도, 안정성을 개선하는 개발 및 운영 방법론입니다. 또한 DevOps에서 CI/CD는 가장 핵심적인 요소로써 프로세스에 관심사를 둡니다.

 

CI 란 무엇인가?

CI는 지속적인 통합입니다. 자동화된 빌드 및 테스트가 수행된 후, 개발자가 코드 변경 사항을 중앙 리포지토리에 정기적으로 병합하는 과정인데요. 이 과정은 개발자가 한 명뿐이더라도 버전관리를 위해 꼭 필요한 과정입니다. 여기서 핵심은 빌드와 테스트를 통해 코드를 테스트한다는 점입니다. 옵션을 주기 나름이겠지만 이 과정을 제대로 구성해놓으면 개발자가 놓친 부분일 지라도 사전에 찾을 수 있겠죠.

 

CD란 무엇인가?

이제 저희의 주된 관심사입니다. CD는 지속적인 배포(Deploy) 혹은 전달(Delivery)로 해석됩니다. 즉, 빌드된 무언가를 우리가 원하는 서버에 배포하는 것을 뜻합니다. CD를 구성하고 나면 우리는 CI를 통해 통합된 이미지를 수동으로 원하는 서버에 배포할 필요가 없게 됩니다.


기술비교

자 이제 기술에 대해 알아봤으니 CI/CD파이프라인을 구축하기 위한 기술을 선택해보겠습니다. 저희는 몇 가지 기술에 대해 선택해야 했습니다. CI/CD 도구는 많았고, 비용과 효율성의 측면에서 비교를 해야했습니다. 아래 글은 이 과정에서 참고했던 글입니다.

https://insight.infograb.net/blog/2024/07/31/cicd-trends/

 

지금 알아야 할 CI/CD 트렌드 5가지

이 글은 글로벌 IT 기업과 업계 전문가가 제시한 2024년 CI/CD 트렌드 가운데 5가지를 꼽았습니다. CI/CD 도구 사용 시 개발 생산성 향상, 여러 셀프 호스팅 CI/CD 도구 사용 시 배포 성과 저하, AI와 머

insight.infograb.net

 

해당 글에서는 셀프호스팅과 완전관리형 서비스의 적절한 조합을 추천합니다. 그 근거가 우리의 의견과 일맥상통 했습니다. CI/CD뿐만이 아니라 우리 서비스의 다른 시스템에서도 같은 논리가 적용되었는데요.

그것은 바로 "줄건 줘" 논리입니다.

 

셀프호스팅의 가장 큰 장점은 비용최적화와 자유롭고 복잡한 기능을 커스터마이징할 수 있다는 것이 장점이라고 생각합니다. 하지만 우리는 첫번째로 모두 초보였습니다. 또한 저희에게 주어진 시간이 3주가 채 안됐습니다. 이에 따라 복잡한 파이프라인을 구성하지 않을 것임이 확실했고, 어지간하면 완전관리형 서비스를 쓰자고 판단했습니다.

 

이에 따라 기술선택이 필요했던 카테고리에서 우리가 선택했던 기술스택과 그 이유에 대해서 간단히 설명드리겠습니다.

 

1. Container Image Registry

- Docker Hub vs ECR

저에게 가장 익숙한 저장소는 Docker Hub였습니다. 이전 프로젝트에서도 Docker Hub를 생각없이 사용했구요. 하지만 우리는 최소한의 보안 요구사항을 준수해야 했습니다. Docker Hub는 모두가 볼 수 있는 공간입니다. 따라서 Docker Hub Registry 이미지를 사용하여 특정 서버에 프라이빗하게 구성하거나 깃랩 레지스트리 같은 프라이빗한 레지스트리를 사용한다고 합니다. 하지만 우리는 이미지 레지스트리까지 구축할 시간이 없었습니다. 이에 따라 AWS의 완전관리형 레지스트리인 ECR을 선택했습니다.

 

2. CI 도구

- GitLab vs Github

여기서도 "줄건 줘" 논리가 발동했습니다. 이미 이미지 레지스트리도 완전관리형 서비스를 사용하겠다고 다짐하기도 했고, 우리는 복잡한 테스트 빌드 파이프라인을 요구하지 않았습니다. 이에 따라 저에게 익숙한 Github Action을 사용하기로 했습니다. 자연스럽게 깃허브로 Code를 관리하기로 선택했습니다.

 

3. CD 도구

- ArgoCD

이제 CD도구를 선택해야 했는데요. GitOps를 구현하기 위한 도구인 ArgoCD를 선택했습니다. ArgoCD는 Kubernetes 애플리케이션의 자동 배포를 위한 오픈소수 도구로 Git저장소의 Manifest 파일의 변경을 읽어 이미지를 자동으로 배포해줍니다. 깃 저장소와 쿠버네티스를 동기화하는 것입니다.

 

이제 기술 선택을 했으니 정해진 아키텍처를 그려보겠습니다.


최종 아키텍쳐

글을 작성하려고 보니 그림에서 오해가 있을 수 있는 부분에 대해 짚어보자면 Dev, Ops 모두 Code변경 전에 Jira에 이슈를 발행해야 하고, 둘 다 Code를 변경할 수 있습니다. Dev의 관심사는 애플리케이션 코드이고, Ops의 관심사는 Manefest Code이기에 나누려고 하다보니 화살표 방향이 혼동될 수 있겠네요.

하여튼 개발자와 관리자가 코드를 수정하면 깃허브 액션을 통해 CI를 수행합니다.

( 애플리케이션 코드를 변경하면 ECR에 이미지를 업데이트 되고, Manifest파일이 수정됩니다. )

( Manifest 파일이 수정되면 이미지를 업데이트 하지는 않습니다. )

ArgoCD는 Manifest 파일의 상태를 확인하고 있다가 파일의 변경을 감지하면 ECR에서 이미지를 Pull해와 Kustomize에 환경별 구성된 파일에 따라 각각 배포합니다.


개발과정

1. ECR 구성

ECR 리포지토리를 생성합니다. 리퍼지토리 만드는 방법은 쉽기 때문에 공식문서 링크를 첨부했습니다. https://docs.aws.amazon.com/ko_kr/AmazonECR/latest/userguide/repository-create.html

ECR생성

 

ECR에 업로드 된 이미지 목록

이처럼 ECR은 CI를 통해 빌드된 이미지가 업로드 됩니다.

 

2. Github Action (CI)

이제는 CI를 위한 workflows 스크립트를 작성해줍시다.

name: Continuous Integration

on:
  push:
    branches: ["main"] # main 브랜치로 push 될 때 해당 파이프라인이 트리거됩니다.
    paths-ignore:
      - 'xinfra/eks/**' 
	# 이 부분은 사실 치명적으로 잘못 짜여진 파이프라인의 산출물입니다.
    # xinfra/eks/ 경로 아래의 파일은 무시합니다.
    # 모든 파이프라인이 끝나면 해당 경로 아래 있는 Manifest파일을 수정하는데
    # 만약 무시하지 않으면 무한CI열차에 탑승하게 되기 때문입니다.
    # 근본적인 원인은 하나의 루트 리퍼지토리에서 관리하기 때문이며 분리를 했어야 했지만 시간상 넘어갔습니다.

permissions:
      id-token: write
      contents: read
      
# 환경변수 주입
env:
  REPOSITORY: ${{ secrets.REPOSITORY }}
  IMAGE_TAG: :${{ github.ref_name }}-${{ github.sha }}
  REGISTRY_USERNAME: ${{ github.actor }}
  AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
  AWS_REGION: ${{ secrets.AWS_REGION }}
  AWS_ROLE: ${{ secrets.AWS_ROLE }}

# 빌드 및 이미지 배포 과정 정의
jobs:
  Build:
    name: Build & Delivery
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          token: ${{secrets.TOKEN_GITHUB}}
          submodules: true
     
      # *.yml 파일을 서브모듈로 관리하기 때문에 해당 과정이 필요합니다.
      - name: Git Submodule Update
        run: |
          git submodule update --remote --recursive
      
      - name: Set up Java JDK
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: 17
        
      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew
        shell: bash
        
      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@v3

	  # 빌드
      - name: Build with Gradle
        id: buildWithGradle
        run: ./gradlew clean build -x test
        shell: bash

	  # Docker 이미지 태그를 지정합니다.
      - name: lowercase the image tag & repository
        run: |
          echo "REPOSITORY=$(echo $REPOSITORY | tr '[:upper:]' '[:lower:]')" >> ${GITHUB_ENV}
          echo "IMAGE_TAG=$(echo $IMAGE_TAG | tr '[:upper:]' '[:lower:]')" >> ${GITHUB_ENV}
	  
      # 우리는 PROD, DEV, 또한 차별화된 비즈니스 로직을 위해 EU서버에는 다른 이미지를 배포하고 있어 이에 따라 따로 이미지를 저장합니다.
      - name: Set Spring Image Environment Variable
        run: |
          echo "PROD_IMAGE=${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.REPOSITORY }}${{ env.IMAGE_TAG }}-prod" >> ${GITHUB_ENV}
          echo "DEV_IMAGE=${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.REPOSITORY }}${{ env.IMAGE_TAG }}-dev" >> ${GITHUB_ENV}
          echo "EU_IMAGE=${{ env.AWS_ACCOUNT_ID }}.dkr.ecr.${{ env.AWS_REGION }}.amazonaws.com/${{ env.REPOSITORY }}${{ env.IMAGE_TAG }}-eu" >> ${GITHUB_ENV}

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ${{env.AWS_REGION}}
          role-to-assume: ${{ env.AWS_ROLE }}
          role-session-name: GitHubActionsSession
    
      - name: Login to AWS ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

 	  # Docker 이미지 빌드
      - name: Build Prod Image
        run: docker build --no-cache -t ${{ env.PROD_IMAGE }} -f ./xinfra/docker/DockerfileProd .

      - name: Build Dev Image
        run: docker build --no-cache -t ${{ env.DEV_IMAGE }} -f ./xinfra/docker/DockerfileDev .

      - name: Build EU Image
        run: docker build --no-cache -t ${{ env.EU_IMAGE }} -f ./xinfra/docker/DockerfileEu .

	  # ECR에 업로드
      - name: Push to ECR
        run: |
          docker push ${{ env.PROD_IMAGE }}
          docker push ${{ env.DEV_IMAGE }}
          docker push ${{ env.EU_IMAGE }}

	  # Manifest 파일의 이미지 태그 업데이트
      - name: Update Kustomize with new images
        run: |
          sed -i "s|image:.*-prod:latest|image: ${{ env.PROD_IMAGE }}|g; s|image:.*-prod|image: ${{ env.PROD_IMAGE }}|g" ./xinfra/eks/overlays/prod/patch.yaml
          sed -i "s|image:.*-dev:latest|image: ${{ env.DEV_IMAGE }}|g; s|image:.*-dev|image: ${{ env.DEV_IMAGE }}|g" ./xinfra/eks/overlays/dev/patch.yaml
    
	  # 변경된 Kustomize 파일을 커밋하고 원격 저장소에 Push. 이 과정이 있기에 위에서 별도의 처리를 해주었다.
      - name: Commit and Push Changes
        run: |
          git config --local user.email "github-actions@github.com"
          git config --local user.name "GitHub Actions"
          git add ./xinfra/eks/overlays/prod/patch.yaml ./xinfra/eks/overlays/dev/patch.yaml
          git commit -m "Update kustomization files with new image tags"
          git push origin ${{ github.ref }}

 

아래 이미지는 공부 열심히 해야하겠다는 것을 상기시키게 하기 위한 우여곡절 스크린샷

ㅜ.ㅜ

 

3. argoCD 구성

작업 디렉토리 정의

# 작업디렉토리 정의
[ec2-user@ip-10-0-0-201 argocd]$ pwd
/home/ec2-user/k8s/argocd

# kustomization.yaml 생성
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: argocd

resources:
  - https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
  
# Kustomize 파일 적용
[ec2-user@ip-10-0-0-201 argocd]$ kubectl apply -k .

argocd 설치

설치 후 Pod를 확인해보겠습니다.

[ec2-user@ip-10-0-0-201 argocd]$ kubectl get pods -n argocd
NAME                                                READY   STATUS    RESTARTS   AGE
argocd-application-controller-0                     1/1     Running   0          66s
argocd-applicationset-controller-6649456bbc-l8rxn   1/1     Running   0          66s
argocd-dex-server-55b56b7b7c-wh7fr                  1/1     Running   0          66s
argocd-notifications-controller-67677889f-gdjth     1/1     Running   0          66s
argocd-redis-5d467f9f48-whpds                       1/1     Running   0          66s
argocd-repo-server-859bbf65b4-dtwb2                 1/1     Running   0          66s
argocd-server-69c6d5dfcb-gdhp9                      1/1     Running   0          66s

[ec2-user@ip-10-0-0-201 argocd]$ kubectl get svc -n argocd
NAME                                      TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
argocd-applicationset-controller          ClusterIP   10.100.52.209    <none>        7000/TCP,8080/TCP            67s
argocd-dex-server                         ClusterIP   10.100.43.161    <none>        5556/TCP,5557/TCP,5558/TCP   67s
argocd-metrics                            ClusterIP   10.100.120.238   <none>        8082/TCP                     67s
argocd-notifications-controller-metrics   ClusterIP   10.100.191.153   <none>        9001/TCP                     67s
argocd-redis                              ClusterIP   10.100.188.46    <none>        6379/TCP                     67s
argocd-repo-server                        ClusterIP   10.100.255.212   <none>        8081/TCP,8084/TCP            67s
argocd-server                             ClusterIP   10.100.164.238   <none>        80/TCP,443/TCP               67s
argocd-server-metrics                     ClusterIP   10.100.66.44     <none>        8083/TCP                     67s

각각에 대해 간단하게 설명하겠습니다.

Pod 설명

  • argocd-application-controller: Argo CD의 애플리케이션 컨트롤러로, 선언적 방식으로 애플리케이션을 관리
  • argocd-applicationset-controller: ApplicationSet CRD를 관리하는 컨트롤러로, 여러 애플리케이션의 반복적인 배포를 관리
  • argocd-dex-server: Argo CD의 인증 서버로, 외부 인증 공급자와의 연동을 처리
  • argocd-notifications-controller: 알림 기능을 담당하는 컨트롤러
  • argocd-redis: Redis 캐시를 제공하는 서비스로, Argo CD의 데이터 캐싱에 사용
  • argocd-repo-server: 애플리케이션의 매니페스트를 Git 리포지토리에서 가져와서 처리하는 서버
  • argocd-server: Argo CD의 핵심 서버로, API 서버와 Web UI를 제공하여 사용자와 상호작용. argocd-server pod를 클러스터 외부에서 접속하여 관리한다.

Service 설명

  • argocd-server: Argo CD의 API 서버와 Web UI에 접근하기 위한 서비스입니다. 80/TCP(HTTP)와 443/TCP(HTTPS) 포트를 사용
  • argocd-repo-server: 애플리케이션의 매니페스트를 처리하는 서버에 대한 내부 서비스
  • argocd-redis: Redis 데이터베이스에 접근하기 위한 내부 서비스
  • argocd-dex-server: Argo CD의 Dex 인증 서버에 대한 서비스로, 인증 요청을 처리
  • argocd-metrics, argocd-server-metrics, argocd-notifications-controller-metrics: 각각 Argo CD의 메트릭스를 수집하기 위한 서비스
  • argocd-applicationset-controller: ApplicationSet 컨트롤러에 대한 내부 서비스

ArgoCD 접근

아래 보이는 것처럼 기본적으로 argocd-server는 ClusterIP로 클러스터 내에서만 접근이 가능하여 외부에서 접속하기 위해서는 별도의 처리가 필요합니다.

[ec2-user@ip-10-0-0-201 argocd]$ kubectl get svc argocd-server -n argocd
NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
argocd-server   ClusterIP   10.100.164.238   <none>        80/TCP,443/TCP   116s

우리는 별도의 개발의 편의성을 위해 LoadBalancer와 연결했지만, 후에는 NorePort타입으로 접근하여 관리했습니다.

 

이제 argocd-server에 접속하면 귀여운 쭈꾸미를 볼 수 있습니다.

 

 

사정상 전체적인 스크린샷을 찍지 못했습니다.. 팀원들에게는 그렇게 잔소리 해놓고... 미리 자료를 만들어놨어야 했는데.. 핑계를 대자면..

첫 번째로 CI/CD는 가장 초반에 구성해놨기에 너무 완벽하게 동작 중이어서 나중에 돌아와서 자료를 만들어도 되겠다 생각했습니다.

두 번째로 자료를 만들려 했을 땐 팀원의 실수로 우리의 클러스터에 네트워크 장애가 발생한 상황이었습니다... (해결했지만 그 때는 또 정신이 없어서 찍지 못했던..) 그렇게 저의 예쁜 ArgoCD 화면은 중간중간 장애가 발생할 때마다 캡쳐해놨던 몇가지 이미지만을 남기게 되었습니다.

Kustomize로 환경 분리하기도 전에 캡쳐했던 스크린샷
배포는 잘됐어요ㅜ
Kustomize적용된 후 장애가 난 상황
발표 전날이라 더 멘붕

전쟁의 서막.. 조용한 팀원의 소리없는 암살로 인해 트러블 슈팅이 안되었고, 하필 부하테스트를 진행했던 타이밍과 겹쳐 엉뚱한 부분을 체크하기 시작하며 미궁에 빠졌던 순간이었습니다. 심지어 발표 전날..ㅎㅎ Route53에서 라우팅 정책에 대한 이슈였습니다.

개선점

1. 현재 저희는 단일 클러스터 내 배포하는 대신 환경을 네임스페이스 단위로 분리합니다. 이는 적절하지 않죠. 비용측면에서의 우위에 있어 이런 선택을 했는데요. 시스템 고도화를 위해 멀티클러스터를 구축하고 배포하는 과정이 있어야 합니다.

 

2. 위에서 언급했듯이 CI 파이프라인에 대해 애플리케이션 리퍼지토리와 Manifest파일을 관리하는 리퍼지토리를 완전히 분리해야합니다.

 

3. 제 뇌를 개선시켜야 합니다. 그렇게 팀원들과 모든 것을 공유한다고 생각했었지만 역시 예상치 못한 위험은 어디에나 도사리고 있었습니다. 또한 조급해하지 않고 미리미리 자료를 정리해놓는 습관을 길러야합니다.