개인 프로젝트의 CICD를 Github 와의 연동이 편하고 무료인 github actions로 구축하기로 하였습니다.
Github Actions 스크립트는 프로젝트의 루트 폴더의 .github > workflows > gradle.yml
에 작성하면 됩니다.
깃허브에서 제공하는 템플릿을 사용해도 됩니다.
Github Actions 스크립트는 CI, CD 2단계로 나누어 진행을 하였습니다.
아래는 CI 단계 전문입니다.
name: CICD with github action and EC2 using docker
on:
push:
branches:
- 'main' # main 브랜치로 푸시 이벤트 발생 시 스크립트가 실행됩니다.
env: # Github에 미리 설정해둔 Secrets 환경 변수를 가져옵니다. 이 단계는 굳이 설정하지 않고 ${{ secrets.*** }}로 secrets 환경변수를 꺼내 써도 됩니다.
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
DOCKER_IMAGE_NAME: ${{ secrets.DOCKER_IMAGE }}
EC2_HOST: ${{ secrets.HOST }}
EC2_SSH_USER: ${{ secrets.SSH_USER }}
PRIVATE_KEY: ${{ secrets.SSH }}
permissions:
id-token: write
jobs: # 1
CI:
runs-on: ubuntu-latest # 2
permissions: # 3
id-token: write
contents: read
steps:
- uses: actions/checkout@v4 # 4
- name: Login to Docker Hub # 5
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_HUB_USERNAME }}
password: ${{ env.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up Docker Buildx # 6
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
file: ./docker/Dockerfile
push: true
tags: ${{env.DOCKER_HUB_USERNAME}}/${{ env.DOCKER_IMAGE_NAME }}:latest
Jobs: CI:
workflow 단계를 지정합니다.
runs-on
으로 빌드에 사용될 ubuntu를 지정해줍니다.
permissions: id-token: write
는 Github OIDC의 접근 허용을 위해 추가하였습니다.
steps에는 각 단계별 사용할 동작을 지정합니다.
uses : actions/checkout@v4
: 지정한 브랜치의 코드를 내려받습니다.
- name: Login to Docker Hub # 5
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_HUB_USERNAME }}
password: ${{ env.DOCKER_HUB_ACCESS_TOKEN }}
Docker Hub로 이미지를 푸시하기 위해 로그인하는 과정입니다.
- name: Set up Docker Buildx # 6
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
file: ./docker/Dockerfile
push: true
tags: ${{env.DOCKER_HUB_USERNAME}}/${{ env.DOCKER_IMAGE_NAME }}:latest
docker/setup-buildx-action@v3
: 멀티 플랫폼 이미지를 빌드하기 위한 필요한 Buildx를 설치합니다.
이후 docker/build-push-action@v6
로 도커 이미지를 빌드한 후, 이전에 로그인했던 Dockerhub로 Push 합니다.
아래는 도커 이미지 빌드에 사용된 Dockerfile입니다.
# 1. Build Image
FROM amazoncorretto:21-alpine-jdk AS builder
WORKDIR /sources
COPY . .
RUN chmod u+w ./api-module/src/main/resources && \
chmod +x gradlew && \
./gradlew clean && \
./gradlew :api-module:build
# ------------------------------
# 2. Production Image
FROM optimoz/openjre-21.0.3:0.4
WORKDIR /app
COPY --from=builder /sources/api-module/build/libs/api-module-0.0.1-SNAPSHOT.jar /app/quiz.jar
CMD ["java", "-Dspring.profiles.active=prod", "-jar", "quiz.jar"]
빌드를 위해 사용된 라이브러리가 최종 컨테이너 실행 시 필요없을 수 있습니다. 이런 라이브러리는 쓸모없이 공간 낭비를 합니다.
멀티스테이지 빌드를 사용하면 컨테이너 실행 시에 필요없는 빌드에 사용된 라이브러리가 모두 삭제된 상태로 컨테이너를 실행 시킬 수 있습니다. 이로 인해 좀 더 가벼운 컨테이너를 사용할 수 있습니다.
FROM amazoncorretto:21-alpine-jdk AS builder
WORKDIR /sources
COPY . .
RUN chmod u+w ./api-module/src/main/resources && \
chmod +x gradlew && \
./gradlew clean && \
./gradlew :api-module:build
빌드 단계입니다.
amazoncorretto:21-alpine-jdk
를 베이스 이미지로 지정하고 builder
라는 이름을 지정합니다.
이후 빌드를 실행할 위치를 WORKDIR /sources
로 지정해주고 gradle build 를 실행해줍니다.
FROM optimoz/openjre-21.0.3:0.4
WORKDIR /app
COPY --from=builder /sources/api-module/build/libs/api-module-0.0.1-SNAPSHOT.jar /app/quiz.jar
CMD ["java", "-Dspring.profiles.active=prod", "-jar", "quiz.jar"]
컨테이너 실행 단계입니다.
실행 환경용 Docker 의 기본 이미지는 optimoz/openjre-21
를 사용해주었습니다. (JRE)
이후 빌드한 이미지 파일을 COPY 명령어로 이동 시켜줍니다. 여기서 --from
옵션을 통해 builder
라는 이미지로부터 복사를 해줍니다.
빌드가 끝났으면 이제 배포를 할 차례입니다.
아래는 CD 스크립트 전문입니다.
CD:
needs: [CI] # 1
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials # 2
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Get Github Action IP # 3
id: ip
uses: haythem/public-ip@v1.3
- name: Add Github Actions IP to SG # 4
run: |
aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
- name: Send deploy.sh to EC2 # 5
uses: appleboy/scp-action@master
with:
host: ${{ env.EC2_HOST }}
username: ${{ env.EC2_SSH_USER }}
key: ${{ env.PRIVATE_KEY }}
source: "deploy/deploy.sh"
target: "/home/ec2-user"
strip_components: 1
overwrite: true
- name: Docker Image Pull and Container run # 6
uses: appleboy/ssh-action@master
with:
host: ${{ env.EC2_HOST }}
username: ${{ env.EC2_SSH_USER }}
key: ${{ env.PRIVATE_KEY }}
script: |
chmod +x deploy.sh
./deploy.sh
- name: Remove Github Action IP from SG # 7
run: |
aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
CD 단계가 실행되기 위해 CI 단계가 성공해야 하기 때문에 needs: [CI]
를 추가해줍니다.
- name: Configure AWS credentials # 2
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ secrets.AWS_REGION }}
AWS 에 접근하기 위해 추가해준 단계입니다.
이 단계는 보안 그룹의 인바운드 설정을 해주기 위해 추가해주었습니다.
배포 스크립트 deploy.sh
를 EC2 로 전달해 주기 위해서는 scp 명령어를 통해 EC2 로 복사를 해주어야 합니다.
문제는 scp 명령어는 ssh 원격 접속 프로토콜을 기반으로 하기 때문에 EC2의 보안 그룹에서 22번 포트를 열어주어야 합니다.
그런데 22번 포트를 활짝 열어놓는것은 너무 찝찝합니다. (집 들어가기 편하려고 집 현관문 열어 놓는 느낌)
이를 위해 배포 전에 github actions IP만 한정적으로 허용해주고 배포가 끝나면 github actions IP를 삭제하는 방식으로 진행하기로 했습니다.
추가적으로 accessKey, secretKey로 접근하는 방법 대신 Github OIDC를 활용하여 접근하는 방식을 활용하였습니다.
- name: Get Github Action IP # 3
id: ip
uses: haythem/public-ip@v1.3
AWS 보안그룹에 github actions IP가 22번 포트 접근을 허용해주기 위해 github actions IP를 구하는 스크립트 입니다.
- name: Add Github Actions IP to SG # 4
run: |
aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
AWS 보안 그룹에 위에서 구한 github actions IP를 대상으로 22번 포트를 허용해주는 명령어입니다.
${{ secrets.AWS_SG_ID }}
는 보안 그룹 ID를 뜻하며(github 시크릿 변수에 미리 설정) {{ steps.ip.outputs.ipv4 }}/32
는 위에서 구한 github actions IP 입니다.
- name: Send deploy.sh to EC2 # 5
uses: appleboy/scp-action@master
with:
host: ${{ env.EC2_HOST }}
username: ${{ env.EC2_SSH_USER }}
key: ${{ env.PRIVATE_KEY }}
source: "deploy/deploy.sh"
target: "/home/ec2-user"
strip_components: 1
overwrite: true
배포 스크립트 deploy.sh
를 EC2 로 전송해줍니다. 이를 위해 appleboy/scp-action
를 활용했습니다. (appleboy 감사합니다...)
source: "deploy/deploy.sh"
는 배포 스크립트의 위치,
target: "/home/ec2-user"
는 원격 서버에 배포 스크립트가 위치할 디렉토리를 뜻합니다.
strip_components: 1
는 숫자 만큼source
경로의 선행 경로를 제거해줍니다. (strip_components: 1
이므로 deploy/deploy.sh
에서 선행 디렉토리인 deploy
제거 -> /deploy.sh
)
overwrite: true
는 원격 서버에 deploy.sh
가 이미 존재하면 덮어쓰기를 해줍니다.
- name: Docker Image Pull and Container run # 6
uses: appleboy/ssh-action@master
with:
host: ${{ env.EC2_HOST }}
username: ${{ env.EC2_SSH_USER }}
key: ${{ env.PRIVATE_KEY }}
script: |
chmod +x deploy.sh
./deploy.sh
scp 로 원격 서버로 전송한 배포 스크립트를 실행합니다.
run: |
aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
배포 전 AWS 보안 그룹에 추가했던 Github actions IP를 제거 합니다.
최종 Github Actions 스크립트
name: CICD with github action and EC2 using docker
on:
push:
branches:
- 'main'
env:
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }}
DOCKER_IMAGE_NAME: ${{ secrets.DOCKER_IMAGE }}
EC2_HOST: ${{ secrets.HOST }}
EC2_SSH_USER: ${{ secrets.SSH_USER }}
PRIVATE_KEY: ${{ secrets.SSH }}
permissions:
id-token: write
jobs:
CI:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ env.DOCKER_HUB_USERNAME }}
password: ${{ env.DOCKER_HUB_ACCESS_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
file: ./docker/Dockerfile
push: true
tags: ${{env.DOCKER_HUB_USERNAME}}/${{ env.DOCKER_IMAGE_NAME }}:latest
CD:
needs: [CI]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Get Github Action IP
id: ip
uses: haythem/public-ip@v1.3
- name: Add Github Actions IP to SG
run: |
aws ec2 authorize-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
- name: Send deploy.sh to EC2
uses: appleboy/scp-action@master
with:
host: ${{ env.EC2_HOST }}
username: ${{ env.EC2_SSH_USER }}
key: ${{ env.PRIVATE_KEY }}
source: "deploy/deploy.sh"
target: "/home/ec2-user"
strip_components: 1
overwrite: true
- name: Docker Image Pull and Container run
uses: appleboy/ssh-action@master
with:
host: ${{ env.EC2_HOST }}
username: ${{ env.EC2_SSH_USER }}
key: ${{ env.PRIVATE_KEY }}
script: |
chmod +x deploy.sh
./deploy.sh
- name: Remove Github Action IP from SG
run: |
aws ec2 revoke-security-group-ingress --group-id ${{ secrets.AWS_SG_ID }} --protocol tcp --port 22 --cidr ${{ steps.ip.outputs.ipv4 }}/32
+)
추가적으로 Send deploy.sh to EC2
단계는 굳이 없어도 됩니다.
대신 Docker Image Pull and Container run
에서 script 부분에 배포 스크립트를 작성해주어야 합니다.