1. 개요
2. CI/CD란
CI/CD(지속적 통합/지속적 제공 또는 배포)는 자동화를 통해 실현 가능한 소프트웨어 개발 방식입니다. 빈번하고 안정적으로 이루어지는 업데이트를 통해 지속적으로 코드를 제공하여 릴리스 주기를 단축합니다.
CI
CI는 주로 주로 코드의 빌드와 테스트 과정을 자동화하는 데 사용됩니다. 대게 많은 프로젝트에서 pull_request를 트리거로서 사용하며, ci를 통과하면 (즉, 테스트 과정을 성공하면) merge를 할 수 있도록 설정하기도 합니다.
ci.yml을 설정이 정말 간단합니다.
name: Backend Server CI
on:
push:
branches:
- main
pull_request:
branches:
- main
- develop
types: [ opened, synchronize ]
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: action checkout
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Set up environment variables
run: |
echo "OPENAI_BASE_URL=${{ secrets.OPENAI_BASE_URL }}" >> $GITHUB_ENV
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> $GITHUB_ENV
echo "cloud.aws.s3.bucket=${{ secrets.CLOUD_AWS_S3_BUCKET }}" >> $GITHUB_ENV
echo "cloud.aws.stack.auto=${{ secrets.CLOUD_AWS_STACK_AUTO }}" >> $GITHUB_ENV
echo "cloud.aws.region.static=${{ secrets.CLOUD_AWS_REGION_STATIC }}" >> $GITHUB_ENV
echo "cloud.aws.credentials.access-key=${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESSKEY }}" >> $GITHUB_ENV
echo "cloud.aws.credentials.secret-key=${{ secrets.CLOUD_AWS_CREDENTIALS_SECRETKEY }}" >> $GITHUB_ENV
echo "naver.client-id=${{ secrets.NAVER_CLIENT_ID }}" >> $GITHUB_ENV
echo "naver.client-secret=${{ secrets.NAVER_CLIENT_SECRET }}" >> $GITHUB_ENV
echo "naver.base-url=${{ secrets.NAVER_BASE_URL }}" >> $GITHUB_ENV
echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV
echo "DB_DATABASE=${{ secrets.DB_DATABASE }}" >> $GITHUB_ENV
echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV
echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Run tests with Gradle
env:
SPRING_PROFILES_ACTIVE: test
run: ./gradlew test
SQL
복사
다만, API 키 등의 민감한 정보들이 있어 Github Secrets에 등록하였으며 이를 불러오는 과정이 추가되었습니다.
CD
반면 CD는 파일은 배포 과정을 자동화하는 데 사용됩니다. 이번 프로젝트의 경우 Docker 컨테이너를 사용해서 배포를 진행했고, 자연스럽게 도커 허브에 빌드된 애플리케이션을 올리는 과정이 추가되었습니다.
cd.yml
name: Backend Server CD
on:
push:
branches:
- main
- develop
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Set up environment variables
run: |
echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> $GITHUB_ENV
echo "DB_DATABASE=${{ secrets.DB_DATABASE }}" >> $GITHUB_ENV
echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> $GITHUB_ENV
echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> $GITHUB_ENV
echo "OPENAI_BASE_URL=${{ secrets.OPENAI_BASE_URL }}" >> $GITHUB_ENV
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> $GITHUB_ENV
echo "CLOUD_AWS_S3_BUCKET=${{ secrets.CLOUD_AWS_S3_BUCKET }}" >> $GITHUB_ENV
echo "CLOUD_AWS_STACK_AUTO=${{ secrets.CLOUD_AWS_STACK_AUTO }}" >> $GITHUB_ENV
echo "CLOUD_AWS_REGION_STATIC=${{ secrets.CLOUD_AWS_REGION_STATIC }}" >> $GITHUB_ENV
echo "CLOUD_AWS_CREDENTIALS_ACCESSKEY=${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESSKEY }}" >> $GITHUB_ENV
echo "CLOUD_AWS_CREDENTIALS_SECRETKEY=${{ secrets.CLOUD_AWS_CREDENTIALS_SECRETKEY }}" >> $GITHUB_ENV
echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> $GITHUB_ENV
echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> $GITHUB_ENV
echo "NAVER_BASE_URL=${{ secrets.NAVER_BASE_URL }}" >> $GITHUB_ENV
- name: Generate .env file for Docker
run: |
echo "DB_HOSTNAME=${{ secrets.DB_HOSTNAME }}" >> .env
echo "DB_DATABASE=${{ secrets.DB_DATABASE }}" >> .env
echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" >> .env
echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> .env
echo "OPENAI_BASE_URL=${{ secrets.OPENAI_BASE_URL }}" >> .env
echo "OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" >> .env
echo "CLOUD_AWS_S3_BUCKET=${{ secrets.CLOUD_AWS_S3_BUCKET }}" >> .env
echo "CLOUD_AWS_STACK_AUTO=${{ secrets.CLOUD_AWS_STACK_AUTO }}" >> .env
echo "CLOUD_AWS_REGION_STATIC=${{ secrets.CLOUD_AWS_REGION_STATIC }}" >> .env
echo "CLOUD_AWS_CREDENTIALS_ACCESSKEY=${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESSKEY }}" >> .env
echo "CLOUD_AWS_CREDENTIALS_SECRETKEY=${{ secrets.CLOUD_AWS_CREDENTIALS_SECRETKEY }}" >> .env
echo "NAVER_CLIENT_ID=${{ secrets.NAVER_CLIENT_ID }}" >> .env
echo "NAVER_CLIENT_SECRET=${{ secrets.NAVER_CLIENT_SECRET }}" >> .env
echo "NAVER_BASE_URL=${{ secrets.NAVER_BASE_URL }}" >> .env
- name: Verify .env file creation
run: cat .env
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Execute Gradle build and tests
run: ./gradlew clean build
env:
DB_HOSTNAME: ${{ secrets.DB_HOSTNAME }}
DB_DATABASE: ${{ secrets.DB_DATABASE }}
DB_USERNAME: ${{ secrets.DB_USERNAME }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
CLOUD_AWS_S3_BUCKET: ${{ secrets.CLOUD_AWS_S3_BUCKET }}
CLOUD_AWS_STACK_AUTO: ${{ secrets.CLOUD_AWS_STACK_AUTO }}
CLOUD_AWS_REGION_STATIC: ${{ secrets.CLOUD_AWS_REGION_STATIC }}
CLOUD_AWS_CREDENTIALS_ACCESSKEY: ${{ secrets.CLOUD_AWS_CREDENTIALS_ACCESSKEY }}
CLOUD_AWS_CREDENTIALS_SECRETKEY: ${{ secrets.CLOUD_AWS_CREDENTIALS_SECRETKEY }}
NAVER_CLIENT_ID: ${{ secrets.NAVER_CLIENT_ID }}
NAVER_CLIENT_SECRET: ${{ secrets.NAVER_CLIENT_SECRET }}
NAVER_BASE_URL: ${{ secrets.NAVER_BASE_URL }}
- name: Set up Docker Build
uses: docker/setup-buildx-action@v2
- name: Set up SSH Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.EC2_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
- name: Copy files to EC2
run: |
scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa .env ec2-user@${{ secrets.EC2_HOST }}:/home/ec2-user/.env
scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ./docker-compose.yml ec2-user@${{ secrets.EC2_HOST }}:/home/ec2-user/docker-compose.yml
scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ./deploy.sh ec2-user@${{ secrets.EC2_HOST }}:/home/ec2-user/deploy.sh
- name: Docker build and push to Docker Hub and copy docker-compose.yml to server
run: |
sudo docker login -u ${{ secrets.DOCKER_ID }} -p ${{ secrets.DOCKER_PASSWORD }}
cd ./api-server
sudo docker buildx build --push --platform linux/amd64 -t ${{ secrets.DOCKER_USERNAME }}/server .
sudo docker push ${{ secrets.DOCKER_USERNAME }}/server
cd ../batch
sudo docker buildx build --push --platform linux/amd64 -t ${{ secrets.DOCKER_USERNAME }}/batch .
sudo docker push ${{ secrets.DOCKER_USERNAME }}/batch
- name: Deploy to Prod
uses: appleboy/ssh-action@master
with:
key: ${{ secrets.EC2_PRIVATE_KEY }}
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
port: 22
script: |
sudo docker login -u ${{ secrets.DOCKER_ID }} -p ${{ secrets.DOCKER_PASSWORD }}
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/server
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/batch
sudo chmod +x deploy.sh
sudo chmod +x docker-compose.yml
sudo ./deploy.sh
sudo docker image prune -f
YAML
복사
조금 복잡할 수 있기 때문에 코드를 조금 더 자세히 살펴보겠습니다.
먼저 .pem 키를 복사하는 과정입니다.
- name: Set up SSH Key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.EC2_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
YAML
복사
Github Actions은 러너라고 하는 가상의 리눅스 환경에서 실행됩니다. 우리는 이 환경에서 EC2 인스턴스로 접근할 필요가 있습니다. EC2 인스턴스에 접속하는 대표적인 방법은 인스턴스 생성 시 발급받은 .pem 키를 사용하는 것입니다. 하지만 로컬 환경에서 Github Actions의 러너 환경으로 미리 .pem 키를 옮길 수 없기 때문에, 다음 명령어를 통해 .pem 키를 러너 환경에 추가합니다:
다음으로 중요한 부분은 도커 허브로 빌드된 애플리케이션을 push 하는 것입니다.
- name: Docker build and push to Docker Hub and copy docker-compose.yml to server
run: |
sudo docker login -u ${{ secrets.DOCKER_ID }} -p ${{ secrets.DOCKER_PASSWORD }}
cd ./api-server
sudo docker buildx build --push --platform linux/amd64 -t ${{ secrets.DOCKER_USERNAME }}/server .
sudo docker push ${{ secrets.DOCKER_USERNAME }}/server
cd ../batch
sudo docker buildx build --push --platform linux/amd64 -t ${{ secrets.DOCKER_USERNAME }}/batch .
sudo docker push ${{ secrets.DOCKER_USERNAME }}/batch
YAML
복사
이제 할 일은 EC2 인스턴스로 접속한 뒤 도커 허브에서 방금 전 push된 이미지를 pull 받는 것입니다.
- name: Deploy to Prod
uses: appleboy/ssh-action@master
with:
key: ${{ secrets.EC2_PRIVATE_KEY }}
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
port: 22
script: |
sudo docker login -u ${{ secrets.DOCKER_ID }} -p ${{ secrets.DOCKER_PASSWORD }}
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/server
sudo docker pull ${{ secrets.DOCKER_USERNAME }}/batch
sudo chmod +x deploy.sh
sudo chmod +x docker-compose.yml
sudo ./deploy.sh
sudo docker image prune -f
YAML
복사