Skip to content

CI CD 문서

ChanWoo Hyun edited this page Nov 23, 2023 · 1 revision

CI/CD란?

CI(Continuous Integration): 지속적인 통합. 빌드와 테스트를 지속적으로 진행하는 것

CD(Continuous Delivery): 지속적인 배포. 한마디로 배포 자동화

정리하자면, CI는 모든 개발이 끝난 후 코드 품질을 관리하는 이전의 방식을 해소하기 위해 나타난 개념으로, 코드 변경 사항이 정기적으로 빌드 및 테스트되어 공유 리포지토리에 통합되는 과정을 통해 계속 품질을 유지하면서 개발을 진행하는 방법이다.

CD는 CI의 연장선으로, CI 프로세스를 통과한 코드 버전을 마지막에 "배포"하는 과정을 말한다. 코드 변경 사항이 CI를 성공적으로 통과하면 수동 개입 없이 자동으로 배포함으로써 능률적으로 배포를 진행할 수 있다.

그렇다면 왜 CI/CD가 필요한가?

반복되는 작업을 최소화하여 속도효율을 극대화하기 위함이다. 개발자는 반복되는 작업을 최소화하고, 코드 변경 사항이 공유 리포지토리에 통합되는 것을 보장받으며, 코드 변경 사항이 통합되면 자동으로 배포되는 것을 보장받는다.

가장 많이 쓰이는 CI/CD 도구로는 무엇이 있고 특징과 장/단점은?

젠킨스:

  • 특징:
    • 오픈 소스 자동화 서버.
    • 다양한 도구 및 기술과 통합할 수 있는 풍부한 플러그인 지원.
    • 모든 프로젝트에 대한 빌드, 배포 및 자동화 지원.
  • 장점:
    • 크고 활성화된 커뮤니티.
    • 다양한 플러그인으로 높은 사용자 정의성.
    • 분산 빌드와 병렬 실행 지원.
  • 단점:
    • 학습 곡선이 다소 가파릅니다.
    • 수동으로 유지보수 및 업데이트가 필요합니다.

서클CI:

  • 특징:
    • 클라우드 기반 CI/CD 플랫폼.
    • YAML을 사용한 간편한 구성.
    • 컨테이너화된 빌드를 위한 도커 지원.
  • 장점:
    • 빠른 설정 및 구성.
    • 버전 컨트롤 시스템과의 훌륭한 통합.
    • 빠른 빌드를 위한 병렬 처리 지원.
  • 단점:
    • 무료 티어 자원이 제한적입니다.
    • 고급 기능은 유료 플랜이 필요할 수 있습니다.

팀시티:

  • 특징:
    • 온프레미스 및 클라우드 배포 옵션을 제공하는 CI/CD 서버.
    • 포괄적인 빌드 및 배포 파이프라인 관리.
    • 사용자 친화적인 웹 기반 UI.
  • 장점:
    • 다양한 빌드 도구 및 언어에 대한 훌륭한 지원.
    • 강력한 빌드 체인 구성.
    • 자세한 빌드 히스토리 및 분석 제공.
  • 단점:
    • 서버 설치 및 유지보수가 필요합니다.
    • 일부 기능이 작은 프로젝트에는 과하게 느껴질 수 있습니다.

깃허브 액션:

  • 특징:
    • 깃허브 리포지토리에 직접 통합된 자동화 도구.
    • 워크플로 구성을 위해 YAML 사용.
    • 응용 프로그램 빌드, 테스트 및 배포 지원.
  • 장점:
    • 깃허브 리포지토리와의 강력한 통합.
    • 깃허브 프로젝트에 대한 간편한 설정.
    • 공개 리포지토리에 대한 무료 CI/CD 분을 제공.
  • 단점:
    • 무료 플랜에서는 병렬 처리가 제한적입니다.
    • 독립된 CI/CD 도구보다 기능이 상대적으로 제한될 수 있습니다.

우리의 선택은?

우리는 깃허브 액션을 ci/cd 툴로써 사용하기로 결정했다. 그 이유는 다음과 같다.

  1. 깃허브 액션은 깃허브 리포지토리에 직접 통합된 자동화 도구이기 때문에, 깃허브 리포지토리와의 강력한 통합을 제공한다. 또한 깃허브 프로젝트에 대한 간편한 설정을 제공한다.

  2. 깃허브 액션은 공개 리포지토리에 대한 무료 CI/CD 분을 제공한다.

  3. YAML을 통한 간단한 구성과 낮은 학습 곡선 또한 깃허브 액션을 선택한 이유이다.

  4. 커뮤니티 및 사용자가 많기에 참고할 자료가 많다.

하지만, 깃허브 액션은 무료 플랜에서는 병렬 처리가 제한적이고, 독립된 CI/CD 도구보다 기능이 상대적으로 제한될 수 있다는 단점이 있다.

따라서, 지금의 경우에는 깃허브 액션을 사용하는 것을 선택했지만, 병렬 처리나 배포 속도가 중요한 환경에 대해서는 다른 도구를 사용 방향을 생각해야 한다.

git action 개념

  1. workflow repository에 추가할 수 있는 일련의 자동화된 커맨드 집합 하나 이상의 Job으로 구성, push, pr같은 이벤트에 의해 실행되게 할 수 있고 시간을 걸 수도 있다. 빌드, 테스트, 배포 등 각 역할에 맞는 workflow를 추가 가능. .github/workflows 디렉터리에 yaml 형식 으로 저장
  2. event workflow를 실행시키는 event ex) commit, push, pr
  3. Job 동일한 Runner에서 실행되는 여러 Step의 집합 기본적으로 하나의 workflow 내의 여러 job은 독립적 실행. but 순서를 둘 수 있음
  4. step command를 실행할 수 있는 각각의 task. shell command, action이 될 수 있다. 하나의 job 내에서 각각의 step은 다양한 task로 인해 생성된 데이터를 공유할 수 있다.
  5. Runner job을 실행시키기 위한 app Runner app은 가상 환경이다. 메모리 및 용량 제한이 존재.

Testing

테스팅을 위해 내 로컬에 래포를 하나 생성했다.

https://github.com/h9661/git-action-practice

테스팅 서버는 동하님이 만든 네이버 클라우드 서버에서 작용시킬 것이다.

나는 다음의 과정으로 깃 액션을 테스트하려 한다.

  1. 테스트 코드 실행
    1. 테스트가 모두 통과되면, 2번으로 진행
  2. 빌드된 파일로 다커 이미지 생성
  3. 이미지를 배포 서버에서 실행

이것을 위한 git action 코드를 작성해보자

# main branch에 push, pull request가 발생할  실행
on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

# 테스트 코드 실행
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Use Node.js 18.x
        uses: actions/setup-node@v2
        with:
          node-version: 18.x

      - name: npm install
        run: npm install

      - name: npm test
        run: npm test

      - name: npm run build
        run: npm run build

image

일단 1번에 대해서 코드를 작성해 봤다. 처음에 node version이 12일 때 test에서 실패가 나왔었다. node version을 18로 바꾸니 정상적으로 test가 진행되었다. 이제 2번에 대해서 시도해보자

우선 .dockerignore 로 이미지를 생성할 때 제외할 폴더들에 대해서 명시해준다.

node_modules/
dist/

.git
.gitingore
.dockerignore
Dockerfile

그 다음, Dockerfile 을 생성해주자.

# 베이스 이미지로 node:18-alpine 사용
# 일반 node.js보다 가벼워서 도커 빌드 용량이 감소한다.(1.2GB -> 350MB)
FROM node:18-alpine

# 명령어를 실행할 work dir 생성
RUN mkdir -p /app
WORKDIR /app

# 프로젝트 전체를 work dir로 복사
ADD . /app/

# npm install
RUN npm install

# npm run build
RUN npm run build

# 포트 개방
EXPOSE 3000

# 서버 실행
ENTRYPOINT npm run start:prod

docker build -t [tagname] .

현재 폴더를 원하는 tagname으로 빌드해서 image 생성. 빌드 후에 docker images를 확인할 수 있다.

docker run --name [containername] -dp 80:3000 [tagname]:latest

containername cotainer에서 내가 만든 image를 run.

-dp 옵션을 통해 로컬의 외부 80포트를 3000번 포트로 포트포워딩

그리고 환경변수를 설정해줘야 한다. 예를들어 도커 계정이름과 원격서버 주소 포트, 깃허브 토큰 등등…

내 정보를 여기에 쓸 순 없으니, 알아서 등록하자. 쉽다.

env:
  # Docker Hub에 로그인하기 위한 환경 변수
  REGISTRY: ghcr.io
  DOCKER_IMAGE: ${{ github.repository }}
  DOCKER_CONTAINER: server

imageghcr.io에 올릴 것이다. 또한 docker image이름을 레포 이름으로 하고, 컨테이너를 server라고 지을 것이다.

# 테스트 성공시, 도커 이미지 빌드
  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source code
        uses: actions/checkout@v2

      - name: Set up docker build
        id: buildx
        uses: docker/setup-buildx-action@v1

      - name: Login To ghcr
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GIT_TOKEN }}

      - name: Build and Push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.DOCKER_IMAGE }}:latest

이 과정을 통해, [ghcr.io](http://ghcr.io) 에 도커 이미지가 올라간다.

image

성공적으로 도커 이미지가 생성되어 내 package 에 올라간 모습이다.

마지막으로, 서버에 접속해서 해당 이미지를 받은 다음 그것을 실행만 해주면 자동 배포 끝이다!

이제, 2번에 해당하는 git action 코드를 작성해주자.

우선 클라우드 서버에 snap install docker 로 도커를 설치해줬다. 그리고 코드를 작성했다.

# 도커 이미지 빌드 성공시,
  deploy:
    needs: build

    runs-on: ubuntu-latest

    steps:
      - name: checkout source code
        uses: actions/checkout@v2

      - name: deploy
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          port: ${{ secrets.REMOTE_PORT }}
          password: ${{ secrets.REMOTE_PASSWORD }}
          script: |
            docker stop ${{ env.DOCKER_CONTAINER }} || true
            docker rm ${{ env.DOCKER_CONTAINER }} || true
						docker rmi ${{ env.REGISTRY }}/${{ env.DOCKER_IMAGE }}:latest || true

            docker pull ${{ env.REGISTRY }}/h9661/${{ env.DOCKER_IMAGE }}:latest

            docker run -d \
              -p 80:3000 \
              -p 443:443 \
              --name ${{ env.DOCKER_CONTAINER }} \
              --restart=always \
              ${{ env.REGISTRY }}/h9661/${{ env.DOCKER_IMAGE }}:latest

실행해보자

image

오류가 발생했다. ssh 접속까지는 잘 된 것 같은데, script 부분에서 에러가 난 것 같다.

docker stop server || true는 server라는 컨테이너가 실행 중이면, 중지하고 아니라면 그냥 지나치라는 의미이다 그 다음 줄도 마찬가지인데, 여기서 에러가 발생한 것은 아닐 것이다.

docker pull ... 부분에서 에러가 발생한 것 같다.

지금 경로가

[https://ghcr.io/v2/h9661/h9661/git-action-practice/manifests/latest](ghcr.io/h9661/h9661/git-action-practice:latest)

이렇게 되어있는데 뭔가 문제가 있다. h9661이 두번 들어가서 이미지를 찾지 못하는 것 같다.

수정해주자.

docker pull ${{ env.REGISTRY }}/${{ env.DOCKER_IMAGE }}:latest

이렇게 수정해주었다.

수정해주고, 시도해보았는데 다시 이런 에러가 발생한다.

err: docker: Error response from daemon: Head "https://ghcr.io/v2/h9661/git-action-practice/manifests/latest": unauthorized.

이런 에러가 나온다. 그래서 공식 문서를 살펴봤다.

Introduction to GitHub Packages - GitHub Docs

로그인이 안된 상태로 pull을 하면 unauthorized가 된다. 따라서 로그인을 해주었다.

docker login ${{ env.REGISTRY }} -u ${{ github.actor }} -p ${{ secrets.GIT_TOKEN }}

또 에러 발생..!!

docker: Error response from daemon: driver failed programming external connectivity on endpoint server (8962d5b9ed45d371e7b09f6ab34d5bdff19a9f959a5f10e3ac4ef87bdf83d731): Error starting userland proxy: listen tcp4 0.0.0.0:80: bind: address already in use.

80포트가 사용중이라서 안된다고 한다. 아마 엔진엑스를 구동중이라 그런 것 같다. 포트를 3000번 포트로 변경 후 다시 시도해보자.

image

성공,,,!!!!

지금까지 작성한 것에서, 테스팅과 빌드를 실패했을 때 pr을 닫는 기능을 넣어주지 않았다. 이 기능을 넣어주고 도입하자.

test workflow를 다음과 같이 수정해주었다.

test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Use Node.js 18.x
        uses: actions/setup-node@v2
        with:
          node-version: 18.x

      - name: npm install
        run: npm install

      - name: npm test
        run: npm test

      - name: if test fail, close pr request
        if: failure()
        uses: actions/github-script@v6
        with:
          github-token: ${{ secrets.GIT_TOKEN }}
          script: |
            const pull_number = context.payload.pull_request.number;
            const updated_title = `🚨 [WIP] ${context.payload.pull_request.title}`;

            await github.rest.pulls.createReview({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number,
              body: '테스트 실패로 인해, 머지할 수 없습니다.'
              event: 'REQUEST_CHANGES',
            });

            await github.rest.pulls.update({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number,
              title: updated_title,
              state: 'closed',
            });

      - name: npm run build
        run: npm run build

      - name: if build fail, close pr request
        if: failure()
        uses: actions/github-script@v6
        with:
          github-token: ${{ secrets.GIT_TOKEN }}
          script: |
            const pull_number = context.payload.pull_request.number;
            const updated_title = `🚨 [WIP] ${context.payload.pull_request.title}`;

            await github.rest.pulls.createReview({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number,
              body: '테스트 및 빌드 실패로 인해, 머지할 수 없습니다.'
              event: 'REQUEST_CHANGES',
            });

            await github.rest.pulls.update({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number,
              title: updated_title,
              state: 'closed',
            });

그리고, test가 실패하게끔 하여 한번 pr을 날려보자.

성공했다.! 사진을 못찍었는데, 아무튼 성공함 ㅋ

도입

자, 이제 로컬에서 작업한 것을 우리의 작업 upstream 레포에 적용시키기 위해 main 브렌치에 저 파일을 수정하여 푸시해주자..!!

image

먼저 secret키를 레포지토리에 추가했다. 이제 코드만 약간 수정해서 root branch에 올려주기만 하면 된다.

근데 테스트 실패하면 자동으로 머지 실패하게 코드를 작성했는데, git token같은 경우는 사람마다 다 달라서, 저 부분 지우고 그냥 main branch에 protection rule을 걸어서 머지 안되게 해야겠다.

image

룰을 추가해준 모습. 먼저 action을 추가 해야, 어떤 action이 통과되어야 pr을 할 수 있는지 설정할 수 있따. action을 후다닥 작성해준 후 어서 머지해주자.

# main branch에 push, pull request가 발생할  실행
on:
  pull_request:
    branches:
      - main

env:
  # 도커 레지스트리  이미지 정보, 컨테이너 이름
  REGISTRY: ghcr.io
  DOCKER_IMAGE: ${{ secrets.GIT_ID }}/api-server
  DOCKER_CONTAINER: api-server

jobs:
  # 테스트 코드 실행
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Use Node.js 18.x
        uses: actions/setup-node@v4
        with:
          node-version: 18.x

      - name: npm install
        run: npm install

      - name: npm test
        run: npm test

      - name: npm run build
        run: npm run build

  # 테스트  빌드 성공시 도커 이미지 빌드
  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout source code
        uses: actions/checkout@v2

      - name: Set up docker build
        id: buildx
        uses: docker/setup-buildx-action@v1

      - name: Login To ghcr
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.GIT_ID }}
          password: ${{ secrets.GIT_TOKEN }}

      - name: Build and Push
        id: docker_build
        uses: docker/build-push-action@v2
        with:
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.DOCKER_IMAGE }}:latest

  # 도커 이미지 빌드 성공시, 서버에 배포
  deploy:
    needs: build

    runs-on: ubuntu-latest

    steps:
      - name: checkout source code
        uses: actions/checkout@v2

      - name: deploy
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.REMOTE_HOST }}
          username: ${{ secrets.REMOTE_USER }}
          port: ${{ secrets.REMOTE_PORT }}
          password: ${{ secrets.REMOTE_PASSWORD }}
          script: |
            docker stop ${{ env.DOCKER_CONTAINER }} || true
            docker rm ${{ env.DOCKER_CONTAINER }} || true
            docker rmi ${{ env.REGISTRY }}/${{ env.DOCKER_IMAGE }}:latest || true

            docker login ${{ env.REGISTRY }} -u ${{ github.actor }} -p ${{ secrets.GIT_TOKEN }}
            docker pull ${{ env.REGISTRY }}/${{ env.DOCKER_IMAGE }}:latest
            docker run -d \
              -p 3000:3000 \
              -p 443:443 \
              --name ${{ env.DOCKER_CONTAINER }} \
              --restart=always \
              ${{ env.REGISTRY }}/${{ env.DOCKER_IMAGE }}:latest

이렇게 작성했다. 문제가 발생할 수도 있지만 금방 수정할 수 있을 것 같다.

ci/cd api server 파이프라인은 구축 성공했다!

Clone this wiki locally