[GitHub Action] 으로 CI/CD 파이프라인 구축하기

Github Action이란?

GitHub Actions는 GitHub 저장소 내에서 직접 빌드, 테스트 및 배포 파이프라인을 자동화할 수 있는 CI/CD(지속적 통합 및 지속적인 배포) 플랫폼이다.

 

 

사용하는 이유

공식 문서에는 GitHub Actions를 사용하는 몇 가지 이유와 목적이 설명되어 있다.

  • 워크플로우 자동화: Github Action은 CI/CD(지속적 통합 및 지속적인 배포)를 자동화하여 개발 프로세스를 더욱 효율적이고 빠르게 만들어 준다. 이는 코드의 빌드, 테스트, 배포를 한 곳에서 관리할 수 있게 해준다. 더불어 이러한 자동화를 통해 개발 프로세스 속도를 크게 높이고 수동 오류를 줄일 수 있다.
  • 이벤트 기반 트리거: Github Action은 특정 이벤트(예: 푸시, 풀 요청, 이슈 생성 등)에 응답하여 워크플로우를 자동으로 시작한다.
  • 깃허브 통합: Github Action은 깃허브에 직접 통합되어 외부 CI/CD 서비스를 사용할 필요가 없다. 이로 인해 설정 프로세스가 단순화되고 모든 것이 한 곳에 보관되므로 권한과 보안을 더 쉽게 관리할 수 있다.
  • 보안 및 secret 관리: Github Action은 API 키, AWS 자격 증명 등의 민감한 정보를 안전하게 저장하고 사용할 수 있다. secret은 암호화되어 워크플로에서 필요한 리소스에 안전하게 액세스할 수 있다.

GitHub Actions의 주요 목적은 소프트웨어 개발 프로세스를 자동화하고 개선하여 더 빠르고 효율적이며 오류 가능성을 줄이는 것이다. 따라서 Actions는 GitHub에 직접 통합되어 개발자가 코드 통합 및 테스트에서 배포 및 전달에 이르기까지 워크플로우를 자동화할 수 있는 편리하고 강력한 도구를 제공한다.

 

 

관련 용어

워크플로우

GitHub Actions에서 특정 작업을 자동화하는 방법을 정의한 것이다. 워크플로우는 .yml 또는 .yaml 파일로 구성되며, 이 파일은 워크플로우가 어떻게 동작해야 하는지를 설명합니다. 워크플로우는 특정 이벤트(예: 푸시, 풀 요청, 이슈 생성 등)에 응답하여 자동으로 시작되고, 설정된 작업들을 한 번에 실행한다. 이를 통해 코드의 빌드, 테스트, 배포 등의 작업을 자동화할 수 있다.

이벤트(event)

GitHub Actions 워크플로우는 특정 GitHub 이벤트에 응답하여 시작된다. 이 이벤트는 푸시, 풀 요청, 이슈 생성 등의 GitHub에서 발생하는 특정 조건을 의미히먀, 워크플로우 파일의 on 필드를 사용하여 지정할 수 있다.

작업(job)

워크플로우에서 수행되는 개별 작업을 의미한다. 워크플로우는 하나 이상의 작업으로 구성될 수 있으며, 이러한 작업은 동시에 또는 순차적으로 실행된다. 각 작업은 고유한 ID를 가지며, 워크플로우 파일의 jobs 필드를 사용하여 지정할 수 있다.

단계(step)

각 작업은 여러 단계로 구성된다. 각 단계는 워크플로우에서 수행할 특정 작업을 정의하며, run 또는 uses를 이용하여 정의한다. run은 쉘 명령을 실행하라는 지시이며, uses는 특정 액션을 사용하라는 의미이다.

 

 

주요 명령어

  • name: 워크플로우의 이름을 정의하거나 작업 단계별 이름을 지정한다.
  • on: 워크플로우가 어떤 GitHub 이벤트에 의해 실행될지 명시할 때 사용한다. 예를 들어, push, pull_request 등의 이벤트에 대응하도록 설정할 수 있다.
  • jobs: 워크플로우에서 수행할 작업들을 정의할 때 사용한다. 각각의 작업은 고유한 id를 가지며, 여러 단계(steps)를 포함할 수 있다.
  • runs-on: 작업이 실행될 운영체제를 지정한다. 예를 들어, ubuntu-latest는 가장 최신의 Ubuntu 버전에서 작업을 실행하겠다는 의미이다.
  • steps: 각 작업이 수행할 단계들을 정의한다. 단계는 다음 두 가지 유형 중 하나가 될 수 있습니다.
    • uses: 특정 액션을 사용하도록 지정한다. 예를 들어, actions/checkout@v2는 GitHub의 'checkout' 액션을 사용하여 워크스페이스에 저장소의 코드를 복사하도록 지시한다.
    • run: 쉘 명령을 실행하도록 지시한다. 이는 워크스페이스에서 직접 스크립트 또는 명령어를 실행하게 한다.

 

 

내가 작성한 CI/CD yml 파일

현재상황

  • 원래라면 main은 배포를 위한 최종 코드가 존재하고 release 브랜치를 통해 배포하는 것이 일반적이다. CI/CD를 전반적으로 익히고 있는 중이기에 main을 release로 생각하고 진행했다.
  • 지금 진행하고 있는 프로젝트의 경우엔 상황 상 dev용 서버를 따로 두지 않고 진행 중이다. 그렇기에 DB도 배포용 하나다. 따라서 CI과정 중에 build와 test를 위해서 Runner 안에 Docker를 통해 DB를 설정하고 그걸 사용하게끔 작성했다.
  • CD의 경우는 단순 EC2에 DockerHub에서 image를 받아서 실행하고 있다. 이에 문제점은 Docker Container를 종료하고 이미지를 새로 받아서 다시 실행하기에 무중단 배포가 되지 않는다는 점과 EC2가 불필요한 리소스를 낭비하고 있다.

 

CI(Continuous Integration)

CI 과정은 코드의 변경사항을 자동으로 빌드하고 테스트하는 것을 목적으로 한다. 이를 통해 코드의 변경된 부분이 기존 코드와 잘 통합되는지, 버그가 발생하지는 않는지를 확인할 수 있다.

    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'corretto'

      - name: Set application-prod.yml
        uses: microsoft/variable-substitution@v1
        with:
          files: ./src/main/resources/application-prod.yml
        env:
          spring.datasource.url: ${{secrets.DB_URL}}
          spring.datasource.username: ${{secrets.DB_USERNAME}}
          spring.datasource.password: ${{secrets.DB_PASSWORD}}

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

      # Permission denied 해결을 위해 쓰기 권한 부여
      - name: Grant execute permission for Gradlew
        run: chmod +x ./gradlew

      # Spring Boot Build & test
      - name: Build with Gradle Wrapper
        run: ./gradlew build -x test
        
      - name: 단위 테스트, 통합 테스트
        run: ./gradlew test
  1. 소스 코드 체크아웃: actions/checkout@v4
    • GitHub 저장소에서 소스 코드를 체크아웃하여 작업 디렉토리에 복사한다.
  2. JDK 설정: actions/setup-java@v4
    • Java 기반 프로젝트를 빌드하고 실행하기 위해 필요한 Java 개발 환경을 준비한다.
  3. 환경 변수 설정: microsoft/variable-substitution@v1
    • application-prod.yml 파일 내의 특정 변수들을 GitHub Secrets에서 가져온 값으로 대체한다.
    • 배포 환경 설정을 적용하기 위한 과정이다. (CI 과정에서는 사용하지 않지만 추후 배포 과정에서 해당 파일을 Dockerfile에서 추가 설정을 하여 Docker가 /config/application.yml로 만든다.)
  4. Gradle 설정: gradle/actions/setup-gradle@v3.1.0
    • 빌드 및 테스트를 위한 Gradle 환경을 준비하는 과정이다.
  5. 실행 권한 부여: chmod +x ./gradlew
    • Gradle Wrapper 스크립트에 실행 권한을 부여한다.
    • 리눅스 기반 시스템에서 스크립트 실행 권한이 없어 발생하는 "Permission denied" 오류를 해결하기 위함이다.
  6. Gradle 빌드: ./gradlew build -x test
    • 소스 코드의 빌드 가능성의 성공 여부를 검증한다.
    • gradlew(=gradlew)는 새로운 환경에서 프로젝트를 설정할 때 java나 gradle을 설치하지 않고 바로 빌드할 수 있게 해주는 역할을 한다.
  7. Gradle 테스트: ./gradlew test
    • 테스트 성공 여부를 검증한다.

 

 

CD(Continuous Deployment)

CD 과정은 자동으로 배포하는 것을 목적으로 한다. 이를 통해 새로 변경된 코드를 안정적으로 배포 환경에 반영한다.

      # Docker 이미지 build
      - name: docker image build
        run: docker build -t ${{secrets.DOCKERHUB_USERNAME}}/${{secrets.DOCKERHUB_IMAGE_NAME}} .

      # DockerHub 로그인
      - name: docker login
        uses: docker/login-action@v3.0.0
        with:
          username: ${{secrets.DOCKERHUB_USERNAME}}
          password: ${{secrets.DOCKERHUB_TOKEN}}

      # DockerHub push
      - name: docker Hub push
        run: docker push ${{secrets.DOCKERHUB_USERNAME}}/${{secrets.DOCKERHUB_IMAGE_NAME}}

      # GET GitHub IP
      - name: GET GitHub IP
        id: ip
        uses: haythem/public-ip@v1.2

        # Configure AWS Credentials - 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: ap-northeast-2 # AWS EC2 지역명 기입 (ap-northeast-2: 서울)

        # Add github ip to AWS
      - name: Add GitHub IP to AWS
        run: |
          aws ec2 authorize-security-group-ingress --group-id ${{secrets.AWS_SG_ID}} --protocol tcp --port 22 --cidr ${{steps.ip.outputs.ipv4}}/32

        # AWS EC2 Server Connect & Docker 명령어 실행 
      - name: AWS EC2 Connection
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{secrets.EC2_HOST}}
          username: ${{secrets.EC2_USERNAME}}
          key: ${{secrets.EC2_KEY}}
          port: ${{secrets.EC2_SSH_PORT}}
          timeout: 60s
          script: | #Docker Image명 기입, port 기입
	          sudo docker stop springboot-prac
	          sudo docker rm springboot-prac
            sudo docker pull ${{secrets.DOCKERHUB_USERNAME}}/${{secrets.DOCKERHUB_IMAGE_NAME}}
            sudo docker run -it -d -p 8080:8080 --name ${{secrets.DOCKERHUB_IMAGE_NAME}} ${{secrets.DOCKERHUB_USERNAME}}/${{secrets.DOCKERHUB_IMAGE_NAME}}

        # REMOVE Github IP FROM security group (9)
      - name: Remove IP FROM security group
        run: |
          aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
  1. Docker 이미지 빌드:
    • docker build 명령어를 사용하여 Docker 이미지를 빌드한다.
    • 빌드한 이미지는 애플리케이션을 실행하는 데 필요한 모든 것을 포함하고 있다.
  2. DockerHub 로그인 (docker/login-action@v3.0.0):
    • DockerHub에 로그인하여 이미지를 DockerHub 리포지토리에 푸시할 수 있는 권한을 얻는다.
  3. Docker 이미지 푸시:
    • docker push 명령어를 사용하여 빌드된 Docker 이미지를 DockerHub에 푸시한다.
  4. GitHub IP 가져오기 (haythem/public-ip@v1.2):
    • GitHub Actions 러너의 공개 IP 주소를 가져온다.
    • 이 주소는 AWS 보안 그룹에 임시로 추가하기 위해 사용된다.
  5. AWS Credentials 설정 (aws-actions/configure-aws-credentials@v1):
    • AWS에 접근하기 위한 자격 증명을 설정한다.
  6. GitHub IP를 AWS 보안 그룹에 추가:
    • EC2 인스턴스에 SSH 접속을 허용하기 위해 GitHub Actions Runner의 IP 주소를 AWS 보안 그룹에 추가한다.
  7. AWS EC2 인스턴스에 SSH 접속 및 Docker 컨테이너 실행 (appleboy/ssh-action@v0.1.6):
    • EC2 인스턴스에 SSH로 접속하여 새로운 Docker 이미지를 기반으로 컨테이너를 실행한다.
  8. GitHub IP를 AWS 보안 그룹에서 제거:
    • 보안을 위해 배포 작업이 완료된 후에는 GitHub Actions Runner의 IP 주소를 AWS 보안 그룹에서 제거한다.

 

 

짚고 넘어가기

왜 다들 permissions: read로 할까?

GitHub Actions 워크플로우에서 permissions 설정은 GitHub 토큰의 권한을 조절하는 데 사용된다. 예를 들어, **permissions: read**는 워크플로우가 저장소의 콘텐츠를 읽을 수 있도록 하지만, 쓰기 작업은 허용하지 않는다. 이 설정은 GitHub 저장소에 대한 액세스 권한과 관련이 있으며, 워크플로우가 실행되는 가상 머신 내에서 파일 시스템 권한에는 영향을 주지 않습니다.

 

Permission denied 해결을 위해 쓰기 권한 부여

GitHub Actions Runner에서 gradlew 파일을 사용하여 Gradle 명령어를 실행하려면, 먼저 이 파일이 실행 가능해야 합니다. 따라서, 워크플로우에서 gradlew 파일을 실행하기 전에 **chmod +x ./gradlew**를 통해 명시적으로 실행 권한을 부여해야 한다.

 

Github Actions를 실행하는 너는 누구냐? (feat. Runner)

GitHub Actions를 실행하면, 실제로 작업이 처리되는 곳은 GitHub에서 제공하는 'Runner'라고 불리는 가상 환경이다. Runner는 GitHub Actions workflow에서 정의된 작업들을 처리하기 위한 특정한 컴퓨터 환경을 의미한다.

Runner안에는 기본적으로 Docker, 프로그래밍 언어 및 컴파일러, 패키지 관리자(ex. npm), Git, 빌드 및 배포 도구(ex. Gradle), 테스트 및 CI 도구 (ex. Junit ), 컨테이너 및 가상화 도구 (ex. Kubernetes)등이 설치되어 있다. ( 물론 OS 마다 다를 수 있다.)

Runner는 워크플로우가 시작될 때 생성되며, 워크플로우가 종료되면 자동으로 종료됩니다. 따라서, 각각의 워크플로우 실행은 격리된 환경에서 이루어지며, 워크플로우 간에 서로 영향을 미치지 않는다.

 

 

현재의 문제점

  • 무중단 배포 미지원: 현재의 배포 과정은 새로운 Docker 이미지를 가져온 후 기존 컨테이너를 중지하고 제거한 다음 새 컨테이너를 실행하는 방식이다. 이 방식은 서비스 중단 시간이 발생하므로, 사용자 경험에 영향을 줄 수 있다.
  • 배포 과정의 안정성: Docker 이미지를 EC2 인스턴스에 직접 배포하는 방식은 간단하고 직관적이지만, 배포 과정에서 실패가 발생할 경우 롤백이 어렵다.

 

내가 작성한 CI/CD 파일

name: Java CI/CD with Gradle

on:
  push:
    branches: [ "main" ]

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read
      
    services:
      mysql:
        image: mysql:8.0.36
        env:
          MYSQL_ROOT_PASSWORD: ${{secrets.DB_PASSWORD}}
          MYSQL_DATABASE: ${{secrets.DB_DATABASE}}
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping -h localhost -u yourusername --password=yourpassword"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'corretto'

      - name: Set application-prod.yml
        uses: microsoft/variable-substitution@v1
        with:
          files: ./src/main/resources/application-prod.yml
        env:
          spring.datasource.url: ${{secrets.DB_URL}}
          spring.datasource.username: ${{secrets.DB_USERNAME}}
          spring.datasource.password: ${{secrets.DB_PASSWORD}}

      - name: Setup Gradle
        uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

      # Permission denied 해결을 위해 쓰기 권한 부여
      - name: Grant execute permission for Gradlew
        run: chmod +x ./gradlew

      # Spring Boot Build & test
      - name: Build with Gradle Wrapper
        run: ./gradlew build
        
      - name: 단위 테스트, 통합 테스트
        run: ./gradlew test  

      # Docker 이미지 build
      - name: docker image build
        run: docker build -t ${{secrets.DOCKERHUB_USERNAME}}/${{secrets.DOCKERHUB_IMAGE_NAME}} .

      # DockerHub 로그인
      - name: docker login
        uses: docker/login-action@v3.0.0
        with:
          username: ${{secrets.DOCKERHUB_USERNAME}}
          password: ${{secrets.DOCKERHUB_TOKEN}}

      # DockerHub push
      - name: docker Hub push
        run: docker push ${{secrets.DOCKERHUB_USERNAME}}/${{secrets.DOCKERHUB_IMAGE_NAME}}

      # GET GitHub IP
      - name: GET GitHub IP
        id: ip
        uses: haythem/public-ip@v1.2

        # Configure AWS Credentials - 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: ap-northeast-2 # AWS EC2 지역명 기입 (ap-northeast-2: 서울)

        # Add github ip to AWS
      - name: Add GitHub IP to AWS
        run: |
          aws ec2 authorize-security-group-ingress --group-id ${{secrets.AWS_SG_ID}} --protocol tcp --port 22 --cidr ${{steps.ip.outputs.ipv4}}/32

        # AWS EC2 Server Connect & Docker 명령어 실행 
      - name: AWS EC2 Connection
        uses: appleboy/ssh-action@v0.1.6
        with:
          host: ${{secrets.EC2_HOST}}
          username: ${{secrets.EC2_USERNAME}}
          key: ${{secrets.EC2_KEY}}
          port: ${{secrets.EC2_SSH_PORT}}
          timeout: 60s
          script: | #Docker Image명 기입, port 기입
	          sudo docker stop springboot-prac
	          sudo docker rm springboot-prac
            sudo docker pull ${{secrets.DOCKERHUB_USERNAME}}/${{secrets.DOCKERHUB_IMAGE_NAME}}
            sudo docker run -it -d -p 8080:8080 --name ${{secrets.DOCKERHUB_IMAGE_NAME}} ${{secrets.DOCKERHUB_USERNAME}}/${{secrets.DOCKERHUB_IMAGE_NAME}}

        # REMOVE Github IP FROM security group (9)
      - name: Remove IP FROM security group
        run: |
          aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32

 

 

 

참고자료

https://docs.github.com/ko/actions

 

GitHub Actions 설명서 - GitHub Docs

GitHub Actions를 사용하여 리포지토리에서 바로 소프트웨어 개발 워크플로를 자동화, 사용자 지정 및 실행합니다. CI/CD를 포함하여 원하는 작업을 수행하기 위한 작업을 검색, 생성 및 공유하고 완

docs.github.com

https://velog.io/@jinny-l/gradlew-permission-denied-issue

 

gradlew permission denied 이슈

Githun Actions 테스트 하면서 gradlew permission denied 이슈 발생협업 시 Springboot 세팅을 해주신 팀원분의 개발환경이 Window였다.Window 환경에서 작업해서 소스를 push하면 파일 생성 시 기본 권한이 644로

velog.io

https://hwasurr.com/blog/git-github/github-actions

 

Github Actions를 이용해 CI/CD 파이프라인 구성하기 | Hwasurr's Devlog

Github Actions를 통해 코드 변경사항 반영에 따라 진행되는 workflow를 자동화하는 과정에 대해 기록합니다.

hwasurr.com

https://gihyun.com/120

 

3. gradlew (gradle wrapper) - spring 사용하기

source code: https://github.com/kgmhk/spring-boot-tutorial/tree/gradlew gradlew (gradle wrapper) 란 gradle wrapper 줄여서 gradlew 는 새로운 환경에서 프로젝트를 설정할 때 java나 gradle을 설치하지 않고 바로 빌드할 수 있게

gihyun.com

https://enant.tistory.com/29

 

Github Action 빌드시 contextLoads Failed 오류

spring server를 ec2에 배포하는 과정을 자동화할 방법을 찾다가, github Action을 사용해보기로 했다. Github Action? Github Action을 간단히 설명하면 빌드, 테스트, 배포 등의 작업을 자동화 시켜주는 도구이

enant.tistory.com

https://www.youtube.com/watch?v=uBOdEEzjxzE