[CI/CD] GitOps 환경 구축하기(3) Jenkins

우노·2024년 9월 15일
0

Practice & Trouble Shooting

목록 보기
16/20

GitHub에 이어 이미지 빌드로 CI를 담당하고 CD를 트리거하는 역할로 Jenkins를 선택했다. 기존에 CI/CD를 위해 사용했던 Github Actions와의 비교는 아래에 서술했다.

아키텍처

아키텍처의 젠킨스 내부에 있는 아이콘 하나마다 파이프라인을 생성하여 백엔드/AI CI, CD 트리거를 위해 총 4개의 파이프라인을 활용하였다.

Jenkins

프로젝트에서 Jenkins의 대표적인 역할은 GitHub 레포지토리의 변경 사항을 확인하고 파이프라인을 실행해서 다음을 수행하는 것이었다.

  1. 이미지를 DockerHub에 빌드/푸시
  2. 새로운 빌드에 따라 바뀐 이미지 태그를 manifest 저장소에 반영
  3. Slack으로 파이프라인별 성공/실패 알림

Master-Agent

이번 프로젝트에서는 활용하지 못했지만 프로젝트를 마감하기 전에 진행했던 해커톤에서 Jenkins의 master-agent 구조를 알게되고 적용하려고 애썼다. 애쓴 결과는 그냥 인스턴스 에이전트 연결 성공, 스팟 인스턴스 에이전트 연결 실패였다.

master-agent 구조는 빌드를 명령하는 master과 빌드를 수행하는 agent를 두어 책임을 분리하고 master의 안정성을 보장하는 구조이다. 더하여 스팟 인스턴스로 에이전트를 둔다면 master를 크게 가져가지 않고 필요할 때만 agent를 띄워 비용을 절감할 수 있을 것이다. 세부적으로는 stage마다 다른 agent를 사용하여 부담을 줄일 수도 있다.

프로젝트 중에 스프링 이미지 빌드가 리소스 소모가 컸던 탓인지 젠킨스가 아예 멈춰서 인스턴스 중지/시작 하면서 데이터가 날아가는 건 아닌지 마음 졸였던 걸 생각하면.. master-agent 구조의 필요성을 체감하게 된 것 같다.

Declarative vs Scripted

나는 Declarative Pipepline을 활용해서 파이프라인을 정의했다. 아직 Scripted Pipeline을 잘 몰라서 제대로 비교하기는 어렵지만 알고있는 내용은

  • Declarative: 기본 제공 기능을 활용하여 비교적 쉽게 파이프라인 정의 가능
  • Scripted: 러닝커브가 있지만 더 자유롭고 세부적으로 파이프라인 정의 가능

모듈(플러그인)을 가져다 그대로 쓰냐 모듈 코드를 직접 고쳐 쓰느냐의 차이라고 느꼈고 나는 Jenkins를 처음 쓰고 동작하도록 하는 것이 우선이었으며 혼자 클라우드? 근데 이제 마감시간에 쫓기는.. 그리 복잡한 기능을 수행하지 않을 것이었기에 Scripted보다 Declarative를 선택하여 파이프라인을 작성했다.

환경 설정

Plugins

Jenkin의 장점은 다양한 플러그인을 지원한다는 점이다. 나는 초기 세팅 시 설치한 추천 플러그인을 포함하여 다음의 플러그인을 활용하였다.

  • Github Integration (Webhook 설정 등)
  • Docker Pipeline (CI)
  • Parameterized Trigger (CD 트리거 이미지 태그 전달)
  • Slack Notification

Credentials

  • Amazon ECR 액세스 키 (이미지 push/pull)
  • GitHub 액세스 키 (Webhook)
  • GitHub ssh 키 (프라이빗 레포지토리)
  • Slack 알림 키

GitHub

Jenkins에서 GitHub의 코드 변경사항에 대한 알림을 받기 위해서 Webhook을 연결했다.

  • Server 설정
    앞서 만든 GitHub Credential로 GitHub 연결
  • 레포지토리마다 GitHub Webhook 추가 http://ip:port/github-webhook/

Slack

Jenkins pipeline syntax에 따라 파이프라인이 종료되면 수행할 스테이지를 post에 정의할 수 있다. post 파이프라인이 종료되면 슬랙에 성공/실패 알림을 보내도록 설정했다.

post {
    success {
        slackSend (
            channel: '#클라우드', 
            color: '#00FF00', 
            message: "BUILD SUCCESS: ${REPO} BUILD Job ${env.JOB_NAME} [${env.BUILD_NUMBER}]"
        )
    }
    failure {
        slackSend (
            channel: '#클라우드', 
            color: '#FF0000', 
            message: "BUILD FAIL: ${REPO} BUILD Job ${env.JOB_NAME} [${env.BUILD_NUMBER}]"
        )
    }
}

이제서야 생각하는 아쉬운 점은 에러가 발생한 스테이지의 로그까지 슬랙으로 보냈다면 추가적인 확인 작업이 더 줄어들지 않았을까 싶다. 마감 직전에 개발팀이 에러를 궁금해하면 직접 보내주곤 했었는데 다음에 파이프라인을 작성할 때는 개발팀에서 로그를 바로 확인할 수 있도록 해야겠다.

Pipeline

총 4개의 개별 파이프라인으로 구성했다.

기본적인 틀이 같고 관리하는 조직이 같은 만큼 개별 파이프라인이 아닌 Organization으로 관리했어도 좋았을 것 같다. BE와 AI가 같은 틀에서 세부적인 값만 다른 만큼 파이프라인을 용도로 분류하여 2가지로 소개하겠다.

CI

앞서 GitHub Webhook을 설정했으니 GitHub hook trigger for GITScm polling을 설정하여 GitHub에 수정사항이 생기면 Jenkins에 알림이 전송될 때 자동으로 빌드 파이프라인을 실행하게 만들 수 있다.

BE, AI 해당 레포에 Jenkinsfile을 업로드하여 활용하였고 CI 파이프라인에서는 각 스테이지에서 다음을 수행해주었다.

  • Git Checkout (+ Submodule init)
  • Remove Previous Docker Image
  • Build Docker Image (v0.0.${BUILD_NUMBER})
  • Run Test Container
  • Call Health Check API
  • Test Env Cleanup
  • Health Check
  • Push to ECR
  • Trigger CD Job
    ➡️ Slack Notification

가장 트러블슈팅과 고민이 많았던 곳이다.

일단 사소하지만 ECR 업로드 후에 이전 이미지를 삭제할지 다음 빌드 때 삭제할지도 고민이 있었다. 만약의 상황(ECR 레포지토리를 날리거나 이미지가 제대로 안 올라가는 등)을 대비해 다음 빌드 때 삭제하는 것으로 작성하였다.

다음으로 이미지가 정상적으로 동작하는지를 위해 Run Test Container에서 빌드한 이미지를 실행해서 /health에서 응답을 받으면 일단 컨테이너를 정리하고 응답 상태코드를 확인하고 ECR 업로드 스테이지로 넘어갈지를 결정한다.
하지만 여기서 발생한 문제는 컨테이너의 실행과 내부 애플리케이션 실행은 별개의 문제라는 것이다. 컨테이너가 정상 실행되어도 바로 다음 스테이지로 넘어간다면 당연하게도 애플리케이션은 준비가 되지 않은 상태기 때문에, 특히 스프링을 구동에 시간이 걸리기 때문에 어떤 응답 상태코드도 받아올 수 없다. 한 번 정상 빌드 후 업로드 했는데 파드 실행에서 health 체크가 깨지는 것을 보고 이미지 테스트의 필요성을 느껴 개발팀에 /health로 GET 요청을 하면 200을 반환하는 API를 요청했다.

그래서 처음에는 sleep(10) sleep(20)을 걸고 상태코드를 받아왔는데 무작정 기다리는 것이 비효율적인 것 같아 5초씩 끊어서 확인했다. 현재 작성된 버전의 치명적인 오류라면 timeout이 없어서 정말 문제가 생겼을 때 파이프라인이 끝없이 돌 것이라는 점이다. 이 점을 수정해야할 것 같다.

마지막으로 빌드한 이미지 태그를 CD 파이프라인에 파라미터로 전달하며 파이프라인을 종료한다.

CD Trigger

앞선 CI 파이프라인에서 파라미터로 이미지 태그를 받아오면 해당 태그로 메니페스트 레포지토리를 업데이트한다. 젠킨스 워크스페이스의 YAML 파일을 수정하고 이를 GitHub에 푸시하여 구현한다. 메니페스트 레포가 업데이트되면 ArgoCD가 이를 추적하여 Sync를 맞추거나 OutOfSync를 표시하여 상태가 바뀌었음을 알린다.

해당 파이프라인을 구현하면서 리눅스의 sed 명령어를 처음 알게되었다. 리눅스 파일시스템 뿐만 아니라 명령어도 공부가 더 필요하다고 느꼈다.

vs GitHub Actions

Jenkins를 쓰면서 지금까지 써왔던 GitHub Actions와 비교하지 않을 수 없었다.

분리 및 제약

가장 큰 차이는 역시 GitHub 종속적인지 아닌지이다. Jenkins는 외부 서버로 동작하여 다양한 플러그인을 지원하고 GitLab 등의 Git 저장소 뿐만 아니라 Linux와 서버에서 할 수 있는 거의 모든 동작을 구현할 수 있다. 그리고 master-agent 구조를 통해 파이프라인, 스테이지별로 노드와 책임을 분리할 수 있다.

하지만 GitHub Actions는 GitHub 레포지토리가 필요하고 내가 GitHub Actions를 활용할 때는 플러그인을 그대로 적용하거나 설정하기보다는 marketplace에서 가져다썼다. 실행도 GitHub Actions가 알아서 실행해준다. 사용하기 쉽고 편리한만큼 비교적 자유로운 설정이 어렵다고 느껴진다.

서버 인스턴스

하지만 GitHub Actions는 서버 인스턴스가 필요없다. GitHub에서 설정해두면 알아서 워크플로가 실행된다. Jenkins가 아무리 스팟 인스턴스로 가져가서 비용을 절감한다고 해도 master를 가지는 이상 0이 될 수는 없다.
별도의 서버 인스턴스 없이 CI/CD 등의 워크플로 실행을 자동화할 수 있다는 것은 큰 장점이다.

보안

CI에서는 ECR, DockerHub에 이미지를 push하는 과정, CD에서는 ssh 연결을 통해 어떤 동작을 수행하는 등 연결에는 비밀값이 필요하고 포트를 열어두는 등의 설정이 필요하다.

Jenkins는 Credential을 통해 연결에 필요한 값들을 관리하고 GitHub Actions에서는 ENV를 등록하거나 암호화한 파일을 업로드하여 비밀값을 관리할 수 있다. 이 점에서는 각자 장단점이 있기 때문에 큰 차이가 있다고 말하고 싶지는 않다.

다만 ssh 연결 면에서 Github Actions < Jenkins 라고 생각한다. Jenkins는 직접 VPC에 배치할 수 있으므로 배포 서버를 프라이빗에 두어 외부에서 연결할 수 없게 설정하여도 Jenkins는 연결하게 만들 수 있다. 하지만 GitHub Actions는 그 자체로 외부에서 연결해야하므로 배포 서버가 인터넷에 노출되고 22번 포트는 열려있어야하며 GitHub 레포지토리에 제한을 잘 걸어두지 않는다면 악의적인 수정에 의해 배포 서버가 영향을 받을 수도 있다.

마무리

개선할 점

CI 과정에는 테스트가 포함되는데 테스트 과정이 부실하다고 느꼈다.
Jenkins 파이프라인은 리눅스 위에서 jenkins:jenkins가 수행하기 때문에 GitHub에서 가져온 ./gradlew clean build 실행이 sudo가 아니어서 막히는 등의 권한 문제가 발생했었다. 이러한 권한을 하나하나 확인하면서 뜯어보고 싶다. chmod chown 지옥
그리고 급하게 진행하고 트러블슈팅하기 급급해서 항상 익숙한 파이프라인만 생성해서 활용했는데 조직에서 가져오거나 다양한 아이템을 활용해보고 싶다.
가장 중요한 개선사항으로는 master-agent로 파이프라인, 스테이지별 agent를 고민하고 스팟 인스턴스를 활용하여 비용 절감을 체감하고 싶다.

글을 쓰다보니 곧 리소스를 전부 날려야해서 무엇을 했는지 정리하는 글에 가까워진 것 같다. 리소스를 날리더라도 이 글을 바탕으로 어떻게 고도화할 수 있는지를 고민해보고 싶다. 다음 글은 GitOps CD 툴인 ArgoCD 활용기로 이어진다!

부족하거나 잘못된 부분이 있다면 편하게 조언, 지적 부탁드립니다. 🙇🏻‍♀️

profile
기록하는 감자

0개의 댓글