야 너두 할 수 있어! GitHub Actions!

김준엽·2022년 12월 10일
0

부스트캠프

목록 보기
3/4

개발자라면 모두 사용하는 GitHub에서 출시한 DevOps분야에서 가장 핫한 키워드 중 하나인 CI/CD를 도와주는 서비스 입니다.

CI/CD에 대한 자세한 사항이 궁금하시면 추가적으로 포스팅을 하겠습니다.
제가 생각하는 CI/CD의 핵심적 키워드는 "무중단 배포"라고 생각합니다.

기존에도 CI/CD를 사용하는 툴은 여러가지가 있었으나 (Jenkins, Circle CI 등)
개발자라면 모두 사용하는 github에서도 새로운 툴을 출시하고 비교적 간단하고 접근성이 좋은 유리한 이점 덕분에 사용하는 개발자들이 늘어나고 있다고 합니다!

그래서 저희의 PRV 프로젝트에서는 github actions를 이용한 CI/CD를 구축하였고, 이를 토대로 정리를 해볼까 합니다.

GitHub Actions?

github action은 하나의 작업 이상을 수행하는 프로세스를 Workflows라고 정의하고 있습니다.
그 안에서 Workflows가 실행되는 특정 상황을 Event라고 정의하며, 이 안에 일어나는 하나하나의 작업 단위를 Job이라고 정의합니다.
이 작업에서 일어나는 하나하나의 단계들을 Step 이라고 합니다.

워크플로는 하나 이상의 작업을 실행하는 구성 가능한 자동화된 프로세스입니다. 워크플로는 리포지토리에 체크 인된 YAML 파일에서 정의되며, 리포지토리의 이벤트로 트리거될 때 실행되거나 수동으로 또는 정의된 일정에 따라 트리거될 수 있습니다. (공식문서)

이렇게 정의된 글만 보면 저는 머리가 아픕니다.. 이해를 잘 못하겠어요.
그래서 저는 앞으로 GitHub Actions에서 일어나는 일들을 자동차를 만드는 공장에 비유해서 설명을 하겠습니다.(맞는 비유인지는 모르겠네요.)

WorkFlows = 자동차를 만드는 공장
Events = 해당 공장에 자동차를 주문
Jobs = 특정 자동차를 만드는 작업
Steps = 한 자동차를 만드는데 필요한 단계(바퀴 단다거나, 문을 만든다거나)
Runners = 해당 공장을 돌리는데 필요한 기계나 컨베이어 벨트

한 공장에서 자동차를 만드는데 한가지 종류의 자동차만 만들지는 않죠?
(A,B,C의 자동차를 만드는 공장이라고 생각해봅시다.)

이제 해당 공장에 주문이 들어옵니다.
B자동차 주문이 들어오면 이 공장이 주문을 받고 자동차 제작을 들어가겠죠? (Workflows가 특정 Events에 실행)
하지만 D자동차 주문이 발생하면 해당 공장은 자동차를 제작 하지 않습니다.
이러한 자동차의 주문이 Event입니다.

이제 B 자동차를 제작하는 일이 Jobs 입니다.
이 Jobs에는 A자동차, B자동차, C자동차를 만드는 레시피들이 있을 겁니다.
A자동차를 만드는 방법과 B자동차를 만드는 세부적인 방법은 다르겠죠? 이러한 부분들을 정의해주는 단계가 Steps입니다.
한 자동차를 만들기 위해 바퀴를 단다거나, 문을 만들어서 단다거나, 핸들을 단다거나 여러 steps들이 있을 거예요

이러한 공장이 잘 돌아갈 수 있게 도와주는 기계들 (컨베이어 벨트)와 같은 역할을 해주는 것이 Runner입니다.

github에서는 트리거될 때 워크플로를 실행하는 서버입니다. 라고 설명하고 있네요.

다음과 같은 개념들을 생각하고 각 단계를 실제 저희가 CI/CD를 구축한 코드를 보면서 한번 각 단계를 살펴볼게요 :)

Event

# workflow의 이름
name: Auto deploy to NCP
# 해당 workflow가 실행되는 트리거
on:
  push:
    branches:
      - dev

위의 코드에서 event는 on의 아래부분 입니다.
여기에는 push, commit, pull-request등 다양한 github의 이벤트들을 추가 할 수 있습니다.
저희 코드의 예시로는
"dev 브랜치에 push가 일어나면 Auto deploy to NCP를 실행 시켜줘!" 로 해석 하면 될 것 같습니다.

Jobs

.github/workflows/deploy_dev.yml

jobs:
  changes:
	...
  push_to_registry_be:
    ...
  push_to_registry_fe:
    ...
  pull_from_registry:
    ...

저희 프로젝트의 jobs들 입니다. 총 4개의 jobs들이 있네요
changes job을 한번 살펴볼까요?

jobs:
  changes:
    name: Diff check
    runs-on: ubuntu-latest
    permissions:
      pull-requests: read
    outputs:
      backend: ${{ steps.filter.outputs.backend }}
      frontend: ${{ steps.filter.outputs.frontend }}
    steps:
      ...

저희 조에서는 docker와 NCP를 이용하여 CI/CD 환경을 구축했기 때문에 다음과 같은 설정이 되어있네요.
이것이 의미하는 바는 workflows 는 linux의 가상머신이나, docker container 위에서도 실행이 가능 하다는 의미입니다.

Steps

이제 job을 어떻게 실행할 것인지 step을 정의 해야 합니다.
위와 같은 changes의 step을 살펴 볼까요?

    steps:
      #변경된 코드를 내려받기
      - uses: actions/checkout@v3
      #filter를 사용
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            backend:
              - 'backend/**'
            frontend:
              - 'frontend/**'

해당 코드는 dev브랜치의 변경된 코드를 frontend 와 backend폴더로 분기하여 내려받는 부분입니다. (FE 와 BE를 추후에 따로 build합니다.)

전체 코드(change)

name: Auto deploy to NCP

on:
  push:
    branches:
      - dev

jobs:
  changes:
    name: Diff check
    runs-on: ubuntu-latest
    permissions:
      pull-requests: read
    outputs:
      backend: ${{ steps.filter.outputs.backend }}
      frontend: ${{ steps.filter.outputs.frontend }}
    steps:
      - uses: actions/checkout@v3
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            backend:
              - 'backend/**'
            frontend:
              - 'frontend/**'

Other Job

한가지 예시만 봐서는 저는 이해가 힘들더라구요.
위에서 보았던 push_to_registry_be job을 한번 살펴봅시다.

  push_to_registry_be:
  	#해당 job의 이름
    name: (BE) Build & Push
    #job의 실행 조건
    needs: changes
    if: ${{ needs.changes.outputs.backend == 'true' }}
    # ubuntu 환경에서 실행 됩니다.
    runs-on: ubuntu-latest
    steps:
    	# 코드 업데이트
      - name: Checkout
        uses: actions/checkout@v3
        # docker 빌드를 위한 작업
      - name: Set up Docker Buildx 1
        uses: docker/setup-buildx-action@v2
        # 저희 개발서버의 NCP로그인을 위한 action
      - name: Login to NCP Container Registry
        uses: docker/login-action@v2
        # 환경변수를 위한 registry ID, PW 주입
        with:
          registry: ${{ secrets.NCP_CONTAINER_REGISTRY }}
          username: ${{ secrets.NCP_ACCESS_KEY }}
          password: ${{ secrets.NCP_SECRET_KEY }}
        # docker를 빌드하고 푸쉬
      - name: build and push
        uses: docker/build-push-action@v3
        with:
          context: ./backend
          file: ./backend/Dockerfile
          push: true
          # 버전 관리를 위한 태깅
          tags: |
            ${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-backend:latest
            ${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-backend:${{ github.run_number }}
          cache-from: type=registry,ref=${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-backend:latest
          cache-to: type=inline

저희는 docker compose를 활용하여 개발서버를 구축했기 때문에 docker에 대한 사전 지식이 있으셔야 원할한 이해를 하실 것 같습니다.
이에 대한 자세한 사항은 저희 팀원이 쓴 NCP Container Registry를 활용하여 CI/CD 환경 구축하기 부분을 참고하시면 좋습니다.

전체코드

name: Auto deploy to NCP

on:
  push:
    branches:
      - dev

jobs:
  changes:
    name: Diff check
    runs-on: ubuntu-latest
    permissions:
      pull-requests: read
    outputs:
      backend: ${{ steps.filter.outputs.backend }}
      frontend: ${{ steps.filter.outputs.frontend }}
    steps:
      - uses: actions/checkout@v3
      - uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            backend:
              - 'backend/**'
            frontend:
              - 'frontend/**'
  push_to_registry_be:
    name: (BE) Build & Push
    needs: changes
    if: ${{ needs.changes.outputs.backend == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Set up Docker Buildx 1
        uses: docker/setup-buildx-action@v2
      - name: Login to NCP Container Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ secrets.NCP_CONTAINER_REGISTRY }}
          username: ${{ secrets.NCP_ACCESS_KEY }}
          password: ${{ secrets.NCP_SECRET_KEY }}
      - name: build and push
        uses: docker/build-push-action@v3
        with:
          context: ./backend
          file: ./backend/Dockerfile
          push: true
          tags: |
            ${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-backend:latest
            ${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-backend:${{ github.run_number }}
          cache-from: type=registry,ref=${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-backend:latest
          cache-to: type=inline

  push_to_registry_fe:
    name: (FE) Build & Push
    needs: changes
    if: ${{ needs.changes.outputs.frontend == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Set up Docker Buildx 1
        uses: docker/setup-buildx-action@v2
      - name: Login to NCP Container Registry
        uses: docker/login-action@v2
        with:
          registry: ${{ secrets.NCP_CONTAINER_REGISTRY }}
          username: ${{ secrets.NCP_ACCESS_KEY }}
          password: ${{ secrets.NCP_SECRET_KEY }}
      - name: build and push
        uses: docker/build-push-action@v3
        with:
          context: ./frontend
          file: ./frontend/Dockerfile
          push: true
          tags: |
            ${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-frontend:latest
            ${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-frontend:${{ github.run_number }}
          cache-from: type=registry,ref=${{ secrets.NCP_CONTAINER_REGISTRY }}/prv-frontend:latest
          cache-to: type=inline
          secrets: |
            "REACT_APP_BASE_URL=${{ secrets.REACT_APP_BASE_URL_DEV }}"

  pull_from_registry:
    name: Connect server ssh and pull from container registry
    needs: [push_to_registry_be, push_to_registry_fe]
    if: |
      always() && 
      (needs.push_to_registry_be.result == 'success' || needs.push_to_registry_fe.result == 'success')
    runs-on: ubuntu-latest
    steps:
      - name: connect ssh
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.DEV_HOST }}
          username: ${{ secrets.DEV_USERNAME }}
          password: ${{ secrets.DEV_PASSWORD }}
          port: ${{ secrets.DEV_PORT }}
          script: |
            docker compose --env-file ${{ secrets.ENV_FILENAME_DOCKER_COMPOSE_DEV }} pull
            docker compose --env-file ${{ secrets.ENV_FILENAME_DOCKER_COMPOSE_DEV }} up -d
            docker image prune -f

해당 코드의 작업은
DiffCheck => FE,BE 폴더에서 코드의 변경사항이 있는 부분만 Build => Dev 배포 서버로 Push 로 이루어져 있습니다.


다음과 같이 FE만 수정했을 때는 FE만 빌드되고 push를 하는 아름다운 모습을 볼 수 있습니다.

저희의 프로젝트가 궁금하다면?
https://github.com/boostcampwm-2022/web18-PRV

(Ref)

profile
꾸준히 성장하는 개발자 지망생

0개의 댓글