해당 포스팅은 AWS EC2 인스턴스가 존재한다는 가정하에 작성되었습니다.
GitHub Actions로 CI/CD 구축 (1)에서는 소프트웨어 개발 라이프 사이클 중 개발부터 테스팅 단계까지의 지속적 통합(CI)을 구축해 보았습니다.
이번 글에서는 배포단계에 해당하는 지속적 배포(CD)를 구축해 보겠습니다.
AWS EC2 인스턴스에 배포를 위해 다음과 같은 단계를 거칩니다:
문제는 1번에 있습니다. SSH를 통한 EC2 인스턴스에 접근하는 가장 간단한 방법은, 보안 그룹의 인바운드 규칙에서 SSH 포트(22번)를 0.0.0.0/0 으로 설정하여 모든 IP 주소에서의 접근을 허용하는 것입니다.
물론 PEM 키가 노출되지 않으면 문제는 없지만 배포 환경에서는 보안을 고려하는 것은 나쁘지 않기에 필요한 IP만 접근할 수 있도록 제한해 주는 것이 좋아보입니다.
저희는 GitHub Actions에서 SSH를 이용하여 EC2 인스턴스에 접근을 해야 하는데요.
문제는 이 가상의 컴퓨터가 유동적으로 IP가 바뀐다는 점입니다.
따라서 아래와 같은 단계를 거쳐 이를 해결해야 합니다.
IP 주소를 확인하는 방법은 haythem/public-ip
액션을 이용하면 됩니다.
- name: Get Public IP
id: ip
uses: haythem/public-ip@v1.3
- name: Print Public IP
run: |
echo ${{ steps.ip.outputs.ipv4 }}
간단하죠?
인바운드 규칙에 IP 주소를 추가하는 방법은 aws-cli
와 authorize-security-group-ingress
명령어를 이용하면 가능합니다.
aws ec2 authorize-security-group-ingress \
--group-id [보안_그룹_ID] \
--protocol tcp \
--port 22 \
--cidr [IP_주소]
보안_그룹_ID는 EC2 인스턴스의 보안그룹
sg-0cc34214adf4373rf
형태의 ID를 의미합니다.
workflows에 위 명령어를 작성해 주면 되는데요. 먼저 로컬에서 잘 작동하는지 테스트 해보겠습니다.
You must specify a region. You can also configure your region by running "aws configure".
저는 aws configure에 아무런 세팅을 하지 않았기 때문에 오류가 발생하였습니다.
aws configure를 등록하기 위해서는
액세스 키와, 시크릿 키가 필요한데요. 이를 위해서는 사용자를 생성해 주어야 합니다.
IAM 대시보드 → 사용자 → 사용자 생성
저는 minkh
로 생성하였습니다.
이 후, 액세스 키 만들기 버튼을 클릭하면 액세스 키와 시크릿 키를 발급받을 수 있습니다.
이제 발급받은 액세스 키와 시크릿 키를 이용하여 configure를 등록합니다.
$ aws configure
AWS Access Key ID [None]: [발급받은 액세스 키]
AWS Secret Access Key [None]: [발급받은 시크릿 키]
Default region name [None]: ap-northeast-2
Default output format [None]:
동일한 명령어를 다시 실행하면? 사용자에 EC2에 접근할 권한이 없기 때문에 오류가 나타납니다.
User: arn:aws:iam::XXXXXXXXX:user/User is not authorized to perform: ec2:AuthorizeSecurityGroupIngress on resource:
EC2에 접근할 수 있는 권한을 부여해 보겠습니다.
IAM 대시보드 → 생성한 사용자 → 권한 추가
직접 정책 연결 → EC2Full 검색
권한 부여 완료
명령어를 다시 실행하면, 정상적으로 작동하는 것을 볼 수 있습니다.
$ aws ec2 authorize-security-group-ingress \
--group-id sg-1cc34444444444444 \
--protocol tcp \
--port 22 \
--cidr 20.172.5.93/32
이제 이 과정을 workflow로 옮겨보겠습니다.
- name: Get Public IP
id: ip
uses: haythem/public-ip@v1.3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: 'ap-northeast-2'
- name: Add GitHub Actions IP
run: |
aws ec2 authorize-security-group-ingress \
--group-id ${{ secrets.SECURITY_GROUP_ID }} \
--protocol tcp \
--port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32
먼저 haythem/public-ip
액션을 이용하여 IP 주소를 받아옵니다.
aws-actions/configure-aws-credentials
은 위에서 aws configure
를 등록한 것과 동일한 역할을 합니다.
즉, 액세스 키와 시크릿 키를 등록하기 때문에 이에 해당하는 시크릿을 생성해주어야 합니다.
마지막으로 aws-cli를 이용하여 IP 주소를 인바운드 규칙에 추가합니다.
여기에서도 보안 그룹 ID가 필요하므로 이를 시크릿에 등록을 해주어야 합니다.
IP 주소를 추가하는 것 까지 완료되었습니다.
EC2 인스턴스에 접속 후 아래의 단계를 거쳐 배포를 진행합니다.
따라서, 이에 해당하는 쉘 스크립트를 작성해 보겠습니다.
touch bash.sh
#!/bin/bash
IMAGE_NAME="ghcr.io/[OWNER]/[IMAGE_NAME]:latest"
# 이미지 풀
docker pull $IMAGE_NAME
# 기존에 실행중인 컨테이너 중지
docker stop $(docker ps -aq)
# 이미지 실행
docker run -d -p 80:8080 $IMAGE_NAME
# 사용하지 않는 컨테이너, 이미지 제거
docker container prune -f
docker image prune -a -f
작성한 쉘 스크립트를 EC2 인스턴스에 접근하여 저장해 놓습니다.
이제, Actions에서 해당 스크립트를 실행만 시켜주면 됩니다.
해당 workflows는 아래와 같습니다.
# EC2에 접근하여 배포
- name: Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_SSH_HOST_NAME }}
username: ${{ secrets.EC2_SSH_USER }}
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
script: ./deploy.sh
적절한 host, username, key를 시크릿으로 등록한 뒤 쉘 스크립트를 실행하도록 하였습니다.
마지막으로 인바운드 규칙에 추가했던 IP를 제거해 보겠습니다.
인바운드 규칙에 IP 주소를 제거하는 방법은 aws-cli
와 revoke-security-group-ingress
명령어를 이용하면 가능합니다.
aws ec2 revoke-security-group-ingress \
--group-id [보안_그룹_ID] \
--protocol tcp \
--port 22 \
--cidr [IP_주소]
따라서, workflows를 작성하면 아래와 같습니다.
- name: Remove GitHub Actions IP
run: |
aws ec2 revoke-security-group-ingress \
--group-id ${{ secrets.SECURITY_GROUP_ID }} \
--protocol tcp \
--port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32
SECURITY_GROUP_ID
는 이전에 IP를 인바운드 규칙에 추가할 때 사용하던 그룹과 동일하고, 사용자와 권한도 이전에 미리 만들어 놨기 때문에 추가로 더 설정해 줄 것이 없습니다.
최종적으로 생성한 workflows는 아래와 같습니다.
name: Java CI with Gradle
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
# 소스코드 체크아웃
- uses: actions/checkout@v3
# GitHub Actions VM 환경의 IP를 받아온다.
- name: Get Public IP
id: ip
uses: haythem/public-ip@v1.3
# JDK 17 세팅
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
# 빌드
- name: Build with Gradle
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
with:
arguments: build
# 이미지 생성
- name: Docker image Build
run: docker build -t ghcr.io/[OWNER]/[IMAGE_NAME]:latest .
# ghcr 로그인
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: [OWNER]
password: ${{ secrets.CR_TOKEN }}
# 이미지 푸시
- name: Docker image Push
run: docker push ghcr.io/[OWNER]/[IMAGE_NAME]:latest
# AWS 인증 관련 옵션을 추가한다.
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: 'ap-northeast-2'
# GitHub Actions VM 환경의 IP를 인바운드 규칙에 추가한다.
- name: Add GitHub Actions IP
run: |
aws ec2 authorize-security-group-ingress \
--group-id ${{ secrets.SECURITY_GROUP_ID }} \
--protocol tcp \
--port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32
# EC2에 접근하여 배포
- name: Deploy
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_SSH_HOST_NAME }}
username: ${{ secrets.EC2_SSH_USER }}
key: ${{ secrets.EC2_SSH_PRIVATE_KEY }}
script: ./deploy.sh
# GitHub Actions VM 환경의 IP를 인바운드 규칙에서 제거한다.
- name: Remove GitHub Actions IP
run: |
aws ec2 revoke-security-group-ingress \
--group-id ${{ secrets.SECURITY_GROUP_ID }} \
--protocol tcp \
--port 22 \
--cidr ${{ steps.ip.outputs.ipv4 }}/32
OWNER : 본인의 GitHub ID 혹은 Organization ID
IMAGE_NAME : 생성할 이미지 이름
이로써 GitHub Actions를 이용한 지속적 배포(CD)의 구축도 완료되었습니다.
오랜만에 보는 AWS 명령어와 각종 GitHub Actions 문법들 등이 낯설게 느껴져서 처음에는 막막했지만, 막상 끝내놓고 보니 그렇게 어려운 작업은 아니였던 것 같습니다.
똑같은 환경을 구축하고 계신 분들이 있으시다면 좋은 참고가 되셨으면 좋겠습니다.
감사합니다.