[AWS & Github Actions] CI/CD 파이프라인 구축 (Spring + Docker)
진행중인 프로젝트의 데브서버에 CI/CD 파이프라인을 구축하는 작업을 맡게 되었다.
이전 포스팅에서 AWS EC2에 스프링 프로젝트를 Docker 컨테이너 사용과 함께 가장 심플한 형태로 배포해 본 적이 있다.
이번 포스팅에서의 목표는 해당 내용의 연장선으로 자동적으로 Git에 소스코드가 Push되면 이를 자동으로 배포하는 CI/CD 파이프라인을 구축해보려고 한다.
(+ 데브서버이기에 AWS 프리티어 사용 한도 이내에서만 하는 것을 목표로 했다. 따라서 AWS CodeDeploy 등 다른 과금이 나가는 서비스를 사용하지 않고 기본제공 EC2 + S3만을 사용한다)
Docker에 대해서 기본적인 이해가 있다고 가정하고, 아래의 AWS 배포 과정의 연장선으로 진행하기 때문에 이해가 되지 점이 있다면 아래의 포스팅을 참고하면 좋을 것 같다. 다만 아래의 포스팅에서 SSH 설정을 하지 않았는데 AWS 인스턴스에 접속하기 위해서 SSH 설정을 추가해주어야 한다.
<함께 보기>
클라우드 배포 : AWS EC2 + Spring + Docker
https://sjh9708.tistory.com/100
CI/CD 란?
CI는 지속적 통합(Continuous Integration) CD는 지속적 배포(Continuous Deployment)을 나타낸다.
CI, Continuous Integration (지속적 통합): 개발자들이 작업한 코드를 지속적으로 통합하여 통합 문제를 최소화하고, 코드 변경이 일어날 때마다 자동으로 빌드 및 테스트가 실행되며, 코드베이스에 통합시키도록 한다.
CD, Continuous Deployment (지속적 배포) : CI를 통과한 이후, 자동화된 프로세스를 통해 테스트를 통과한 코드를 실제 환경으로 자동으로 프로덕션 환경에 배포하는 파이프라인 구축을 목적으로 한다.
CI/CD를 적용하지 않았을 때에는 수정 사항이 있을 때마다 Docker 이미지를 빌드 및 푸시하고, 클라우드에 접속하여 이미지 업데이트 및 컨테이너 재실행 등의 업데이트가 이루어질 때 마다 번거로운 작업을 해야 했다.
배포를 자동화하게 되면 Git에 Push 하기만 하면 변경내용을 가지고 도커가 자동으로 빌드되고 배포되도록 할 수 있기에 생산성이 향상된다.
GitHub Action
GitHub에서 제공하는 CI/CD 도구로 자동으로 코드를 빌드하고 테스트하며 배포 작업을 실행할 수 있다. Jenkins와 더불어 현재 가장 많이 사용되는 CI/CD 도구이다.
CI/CD 파이프라인 구축
해당 포스팅에서는 위와 같은 CI/CD 시나리오를 구현할 것이다.
1. GitHub의 develop 브랜치에 Spring 프로젝트가 업데이트되어 Push 될 때
2. GitHub Action 머신 안에서 해당 프로젝트를 Gradle을 사용하여 빌드
3. 빌드된 프로젝트를 Docker Image로 빌드하여 DockerHub에 Push한다.
4. GitHub Action 머신 안에서 SSH를 통해서 AWS EC2에 접속한다.
5. AWS 인스턴스에서 DockerHub에서 새로운 이미지를 Pull 받아 이미지를 업데이트하고 컨테이너를 재실행한다.
A. 스프링 프로젝트와 AWS EC2, Docker 관련 준비
우리는 스프링 프로젝트를 AWS에 배포할 것이므로 아래의 내용들을 준비해야 한다. (이전 포스팅 참고)
1. Spring Project 준비 및 스프링 프로젝트를 Docker Image로 빌드할 Dockerfile 준비
2. Dockerfile을 업로드 할 DockerHub 준비
CI/CD 과정에서 DockerHub에 Push하기 위해서 Access Token이 필요해진다. Account Setting -> Security에서 미리 발급받아두자.
3. AWS EC2 인스턴스 생성 및 네트워크 보안 그룹 규칙 설정 (+ SSH 보안접속 설정)
SSH 설정 시 발급받은 PEM Key는 나중에 CI/CD 과정에서 EC2에 접속하기 위해서 필요하므로 미리 보관해두자.
4. EC2 인스턴스에서 docker-compose 파일 준비
EC2 인스턴스에 접속해서 필요한 docker, docker-compose 등을 다운로드 받아주고, 프로젝트의 디렉터리를 하나 만들어주자. 그 안에 docker-compose 파일을 만들어준다. 아래 그림에서는 필요에 따라서 다른 파일들도 있는데 무시해도 된다.
DockerHub로부터 이미지를 업데이트받아 컨테이너 빌드 후 재실행시키기 위한 compose 파일을 작성해주자.
# EC2에 넣어둔 docker-compose.yaml 파일 예시
version: "3.7"
networks:
didacto-network-inner:
driver: bridge
services:
# db, etc...
didacto-app:
image: sjh9708/didacto-app-dev:latest #DockerHub에서 Pull받을 이미지
restart: always
expose:
- 8080
container_name: didacto-app-dev
environment:
# DB Connection 정보 등 환경변수
networks:
- didacto-network-inner
# ...
B. Workflow 파일 생성
GitHub Actions의 조건과 동작은 .github/workflows/ 디렉터리에 위치한 Workflow YML 파일로 정의된다.
이 파일에는 워크플로우의 트리거 조건, 실행할 작업 및 각 작업의 단계을 작성해야 한다.
여기서는 develop 브랜치에 대한 CI/CD를 구축할 것이므로 YML 파일의 이름은 develop으로 하였다.
C. Workflow 파일 작성
1. 트리거(Triggers)
워크플로우는 특정 이벤트가 발생했을 때 트리거된다. 가장 많이 사용되는 방식은 코드가 특정 Branch에 Push되거나 Pull Reqeust가 생성될 때 트리거되도록 설정할 수 있다.
on:
push:
branches:
- develop
develop 브랜치에 코드가 푸시될 때마다 해당 워크플로우가 실행되도록 트리거를 설정해주었다.
2. 작업(Jobs)
트리거 시 실행할 여러 개의 작업(Job)을 작성해준다. 각 작업은 독립적으로 실행된다. GitHub에서 제공하는 가상 머신(VM) 또는 컨테이너 내에서 실행된다. GitHub는 다양한 실행 환경(Ubuntu, Linux, Windows 등)을 제공하며 실행할 환경을 지정할 수도 있다. 우리는 크게 빌드 과정과 배포 과정에 해당하는 Job을 작성할 것이다.
- 단계(Steps) : 작업은 여러 개의 단계(Step)로 구성된다. 각 단계는 특정 작업을 실행하며, 명령어를 실행하거나 외부 액션을 사용할 수 있다. 우리는 스프링 프로젝트를 빌드하고, 도커 이미지를 빌드한 후 DockerHub에 Push, EC2 인스턴스에 접속하여 업데이트 하는 과정 등을 작성하게 될 것이다.
- 액션(Actions) : GitHub Actions 커뮤니티는 다양한 사전 빌드된 액션을 제공한다. 일종의 라이브러리를 사용한다고 생각하면 된다. 예를 들어서 . 예를 들어 "uses: actions/setup-java@v1"은 JDK를 설치하는 작업을 사전에 빌드해 둔 액션이며 uses 키워드를 통해 액션을 사용할 수 있다.
우리의 플로우를 다시 한번 보면서 이를 작성해보자.
빌드 작업 작성
1. 최신 코드베이스 체크아웃
jobs:
build:
runs-on: ubuntu-latest
steps:
# 코드 체크아웃
- name: Checkout repository
uses: actions/checkout@v2
develop 브랜치의 최신 코드를 체크아웃한다. 즉 GitHub Action의 가성머신으로 Repository의 최신 코드를 가져 오는 것이다. uses를 통해서 체크아웃하는 Action을 사용했다.
2. JDK 설치
# JDK 설치
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
Spring App을 빌드하기 위해서 Github Action의 머신에 JDK를 설치한다. uses를 통해서 JDK를 설치하는 Action을 사용했다.
3. Spring 프로젝트 빌드
# Gradle Build
- name: Build with Gradle
run: ./gradlew build
Gradle을 사용해서 Spring 프로젝트를 빌드한다. 만약 Maven을 사용하면 run에 명령어를 mvn으로 입력해주면 된다.
4. DockerHub 로그인
# DockerHub 로그인
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
DockerHub에 이미지를 빌드하여 푸시하기 위해서 우선 인증 과정을 거쳐야 한다.
위에서 발급해둔 ACCESS TOKEN을 Password로 하여 입력하면 되는데, 이는 예민한 정보이므로 Github에서 Secret 환경 변수로 관리할 것이다. 따라서 secrets 키워드를 이용하여 작성하고, 뒤에서 시크릿 환경 변수를 GitHub에 등록해 줄 예정이다.
5. Docker 이미지 빌드 및 DockerHub에 Push
# DockerHub Push
- name: Build and Push Docker image
run: |
docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/didacto-app-dev .
docker push ${{ secrets.DOCKER_HUB_USERNAME }}/didacto-app-dev
이제 DockerHub에 Spring Project를 Dockerfile을 사용해서 이미지로 빌드한 후 업로드해주는 작업을 하고 배포 작업 작성으로 넘어가자.
배포 작업 작성
6. AWS 접속 및 Docker 이미지 업데이트, 컨테이너 재시작
deploy:
runs-on: ubuntu-latest
needs: build
steps:
# AWS 배포
- name: SSH to EC2 and deploy
uses: appleboy/ssh-action@v0.1.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
cd didacto-testserver
sudo docker-compose stop
sudo docker-compose rm -f
sudo docker rmi sjh9708/didacto-app-dev:latest
sudo docker-compose up -d
SSH를 통해 EC2 인스턴스에 접속한다. 이 때 HOST, USERNAME, KEY가 필요한데 이도 마찬가지로 Secret 환경변수로 관리한다.
EC2에 접속한 이후에는 클라우드의 기존 컨테이너를 중지하고 제거한 뒤, 최신 Docker 이미지를 사용하여 컨테이너를 재시작한다.
- cd didacto-testserver: 해당 디렉토리로 이동
- sudo docker-compose stop: 현재 실행 중인 Docker 컨테이너 중지
- sudo docker-compose rm -f: 중지된 Docker 컨테이너 제거
- sudo docker rmi sjh9708/didacto-app-dev:latest: 기존 Docker 이미지를 제거
- sudo docker-compose up -d: Docker Compose를 사용하여 새로운 이미지로 업데이트하여 컨테이너 시작
전체 Workflow 파일
▶ .github/workflows/develop.yml
name: Didacto-core dev CI/CD Pipeline
on:
push:
branches:
- develop
jobs:
build:
runs-on: ubuntu-latest
steps:
# 코드 체크아웃
- name: Checkout repository
uses: actions/checkout@v2
# JDK 설치
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
# Gradle Build
- name: Build with Gradle
run: ./gradlew build
# DockerHub 로그인
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
# DockerHub Push
- name: Build and Push Docker image
run: |
docker build -t ${{ secrets.DOCKER_HUB_USERNAME }}/didacto-app-dev .
docker push ${{ secrets.DOCKER_HUB_USERNAME }}/didacto-app-dev
deploy:
runs-on: ubuntu-latest
needs: build
steps:
# AWS 배포
- name: SSH to EC2 and deploy
uses: appleboy/ssh-action@v0.1.3
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_SSH_KEY }}
script: |
cd didacto-testserver
sudo docker-compose stop
sudo docker-compose rm -f
sudo docker rmi sjh9708/didacto-app-dev:latest
sudo docker-compose up -d
D. 시크릿 환경 변수 설정
워크플로우 실행 중에 필요한 Access Key와 같이 민감한 데이터를 안전하게 처리하기 위해 GitHub에서 환경 변수를 설정해주어야 한다. secrets 키워드를 통해 GitHub 리포지토리에서 보안되어 관리되는 시크릿을 참조할 수 있다.
Repository -> Settings -> Secrets and variables에서 환경 변수에 대한 Key, Value를 추가해주자.
해당 환경변수에는 다음 내용들을 넣어주면 된다.
- DOCKER_HUB_ACCESS_TOKEN : 위쪽에서 언급된 Docker Hub의 Access Token
- DOCKER_HUB_USERNAME : Docker Hub 계정의 ID
- EC2_SSH_KEY : 위쪽에서 언급되었던 PEM Key는 RSA 형식이다. 해당 파일을 열어서 전체 내용을 Value로 사용하면 된다. (== BEGIN == 부터 == END == 까지)
- EC2_HOST, EC2_USER : SSH로 접속할 때 다음과 같이 접속할 것이다. 다음에 해당하는 것이 Host와 User이다.
ssh -i "YOUR_SSH_KEY.pem" EC2_USER@EC2_HOST
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAv1W8VfKl...
... (Base64 인코딩된 키 데이터) ...
-----END RSA PRIVATE KEY-----
트러블 슈팅
ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain
SSH 연결 과정 중 다음과 같은 문제가 발생하고 있었다.
리서칭해 본 결과 이것은 클라이언트가 제공한 인증 방법(공개 키)이 SSH 서버에서 허용되지 않아서 발생하는 문제라고 한다.
서버 설정에서 공개 키 인증이 비활성화되었거나, 특정 키 타입이 허용되지 않아있을 확률이 높아 이것을 먼저 체크하였다.
이를 해결하기 위해서 EC2 서버 내부의 "/etc/ssh/sshd_config" 파일에 두 줄을 추가해준 후 서버를 재시작하니 해결하였다.
PubkeyAuthentication yes
PubkeyAcceptedKeyTypes=+ssh-rsa
SSH 서버가 공개 키 인증 방식을 허용하도록 하고, 허용할 공개키의 타입을 우리 PEM Key가 RSA였으니 ssh-rsa로 지정해주자.
결과 확인
각 작업의 실행 결과는 로그로 기록되며, 사용자는 GitHub 인터페이스를 통해 로그를 확인할 수 있다.
develop 브랜치에 푸시한 이후 Actions 탭을 확인해보면 Workflow에 대한 결과를 확인할 수 있다. 파란불이 들어오면 모든 작업이 성공적으로 수행되었다는 뜻이다. 실제로 클라우드를 통해 접속하면 자동으로 배포되어 있는 것을 확인할 수 있을 것이다.
References
https://docs.github.com/ko/actions
'Cloud > AWS' 카테고리의 다른 글
[AWS] EC2 인스턴스에 Docker 컨테이너 배포하기 (2) | 2023.09.12 |
---|