CD편이 필요하다는 친구의 요청으로 작성해보는 계속되는 구글링 + 나홀로 CI/CD 도전기..
CD(지속적 제공/배포)에 앞서, CI(지속적 통합)편을 읽어보는 것을 권장합니다.
CD(Continuous Delivery/Deployment): 지속적 배포
배포 과정
GitHub workflow에서
- zip으로 압축된 빌드 파일을 AWS S3에 업로드
- CodeDeploy가 S3에 있는 압축 파일을 해제하여 EC2 서버에 배포
언뜻 보면 단순한 이 과정을 위하여 수많은 관문을 건너야 한다...🤯
1. S3 버킷 생성
2. IAM 사용자 생성 (뒤늦게 블로그를 작성하면서 생각해보니 이 단계는 빠져도 되는 것 같다..)
3. EC2 설정
4. CodeDeploy 생성
5. IAM 사용자 생성 - GitHub Actions용
6. AppSpec.yml 작성
7. 배포 스크립트 작성
8. plain.jar 제외 (참고사항 먼저 볼 것)
9. GitHub Actions에 배포 workflow 생성
렛츠고
1. S3 버킷 생성
1) 이름 지정
2) 객체 소유권 - ACL 활성화
3) 모든 퍼블릭 액세스 차단 해제
사실 아직도 이렇게 모든 액세스 차단을 해제하는 것이 괜찮은 건지 잘 모르겠다. 나중에 AWS 퍼블릭 액세스 차단 관련 설정에 대해서도 다시 알아봐야 할 것 같다..
현재, 2. IAM 사용자 생성을 접은 글에 숨겨둔 상태인데, 만약 여기서 퍼블릭 액세스를 차단한 경우 IAM 사용자 또는 역할을 통해 접근에 대한 권한을 주는 것이 아닌가 하는 추측을 해보는 중이다. 하지만, 2번 과정을 생략해도 아래에서 IAM 사용자 계정과 역할을 통해 충분히 접근 권한을 주고 있는 것 같다.. 어렵다 🤯 이 부분은 추후 확인 후 수정하겠습니다..!
4) 버킷 정책
버킷 > 권한 > 버킷 정책에서 편집/삭제 가능
버킷에 저장된 객체에 대한 액세스 권한을 제공하는 정책으로, 아래와 같이 설정하는 경우 객체를 가져오고 업로드 하는 것에 대한 권한을 열어주게 된다.
{
"Version": "2012-10-17",
"Id": "<임의의 ID>",
"Statement": [
{
"Sid": "<임의의 Sid>",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::<버킷 이름>/*"
}
]
}
2. IAM 생성
블로그를 작성하면서 생각해보니 이 단계는 빠져도 되는 것 같아서 접은 글 안에 작성해두도록 하겠습니다. 해당 과정 생략한 후 진행해 보시고 안 되면 적용해보세요!! 댓글로도 알려주시면 더 감사할 것 같습니다🥹 나도 나중에 직접 확인해봐야지..
AWS > IAM > 사용자 > 사용자 생성
1. 사용자 세부 정보 지정
임의의 사용자 이름 지정
2. 권한 설정
직접 정책 연결 → AmazonS3FullAccess
3. IAM AccessKey
AWS > IAM > 사용자 > 보안 자격 증명 > 액세스 키 > 액세스 키 만들기
1. ‘액세스 키 모범 사례 및 대안’은 사례를 보여주는 파트로 아무거나 선택해도 됨
2. 액세스 키, 시크릿 키 - ⭐️ 따로 꼭 저장해두기
3. EC2 설정
1) EC2 태그 추가
임의의 키와 값 지정
2) IAM 역할 생성
AWS > IAM > 역할 > 역할 생성
1. 신뢰할 수 있는 엔티티 선택
- 신뢰할 수 있는 엔티티 유형: AWS 서비스
- 사용 사례: EC2
2. 권한 추가
- AmazonS3FullAccess
3. 이름 지정, 검토 및 생성
2) AWS EC2에 IAM 역할 등록
ec2 > 작업 > 보안 > IAM 역할 수정
3) EC2 서버에 Code Deploy Agent 설치
sudo apt update
sudo apt install ruby-full
sudo apt install wget
cd /home/ubuntu
wget https://aws-codedeploy-ap-northeast-2.s3.ap-northeast-2.amazonaws.com/latest/install
chmod +x ./install
sudo ./install auto > /tmp/logfile
sudo service codedeploy-agent status
#하다가 꼬인 경우 아래 코드로 codedeploy-agent 삭제 후 처음부터 진행
sudo dpkg --remove --force-all codedeploy-agent
4. Code Deploy 생성
1) Code Deploy 전용 IAM 역할 생성
AWS > IAM > 역할 > 역할 생성
위에서 했던 것과 같지만, 사용 사례
에서 EC2가 아닌 CodeDeploy
선택
2) CodeDeploy 애플리케이션 생성
1. 애플리케이션 생성
AWS > CodeDeploy > 애플리케이션 > 애플리케이션 생성
2. 배포 그룹 생성
AWS > CodeDeploy > 애플리케이션 > 애플리케이션 > 배포 그룹 > 배포 그룹 생성
- 서비스 역할: CodeDeploy용으로 추가했던 IAM 역할 선택
- 환경 구성: EC2 인스턴트에 추가했던 태그 선택
5. GitHub Actions에서 사용할 IAM 사용자 생성
1) IAM 사용자 생성
1. AWS Management Console에 대한 사용자 액세스 권한 제공
2. 권한 설정
- 직접 정책 연결
- AmazonS3FullAccess
- AWSCodeDeployFullAccess
3. 액세스 키 발급
2) GitHub Actions에 secrets 등록
6. AppSpec.yml 파일 작성
CodeDeploy에서 배포 관리할 때 사용하는 YAML 형식 파일 프로젝트 최상단에 작성 |
1. 운영체제
배포가 진행될 운영체제: Linux
2. files
source 경로의 소스 파일을 destination에 덮어쓰기(overwrite)
3. permissions
object 경로의 모든 파일과 디렉토리(**)에 대하여 권한을 설정
소유자(owner)와 그룹(group)을 ubuntu로 지정
4. hooks
설치 후 실행할 스크립트 지정(AfterInstall)
애플리케이션 시작 시 실행할 스크립트 지정(ApplicationStart)
timeout: 스크립트 최대 실행 시간, 60초
runas: 스크립트를 실행할 사용자, ubuntu
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/project
overwrite: yes
permissions:
- object: /
pattern: "**"
owner: ubuntu
group: ubuntu
hooks:
AfterInstall:
- location: scripts/stop.sh
timeout: 60
runas: ubuntu
ApplicationStart:
- location: scripts/start.sh
timeout: 60
runas: ubuntu
7. 배포 스크립트 작성
❗️두 스크립트의 PROJECT_ROOT
가 AppSpec.yml의 files.detination
과 일치해야 함❗️
stop.sh
JAR_FILE
명은 build.gradle에서 설정한 {group}-{version}.jar
이다.
group = 'project', version = '0.0.1'이라면 아래와 같이 작성해야 한다.
#!/usr/bin/env bash
PROJECT_ROOT="/home/ubuntu/project"
JAR_FILE="$PROJECT_ROOT/build/libs/project-0.0.1.jar"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
# 현재 구동 중인 애플리케이션 pid 확인
CURRENT_PID=$(pgrep -f $JAR_FILE)
# 프로세스가 켜져 있으면 종료
if [ -z $CURRENT_PID ]; then
echo "$TIME_NOW > 현재 실행중인 애플리케이션이 없습니다" >> $DEPLOY_LOG
else
echo "$TIME_NOW > 실행중인 $CURRENT_PID 애플리케이션 종료 " >> $DEPLOY_LOG
kill -9 $CURRENT_PID
fi
start.sh
#!/usr/bin/env bash
PROJECT_ROOT="/home/ubuntu/project"
JAR_FILE="$PROJECT_ROOT/build/libs/project-0.0.1.jar"
APP_LOG="$PROJECT_ROOT/application.log"
ERROR_LOG="$PROJECT_ROOT/error.log"
DEPLOY_LOG="$PROJECT_ROOT/deploy.log"
TIME_NOW=$(date +%c)
# build 파일 복사
echo "$TIME_NOW > $JAR_FILE 파일 복사" >> $DEPLOY_LOG
cp $PROJECT_ROOT/build/libs/*.jar $JAR_FILE
# jar 파일 실행
echo "$TIME_NOW > $JAR_FILE 파일 실행" >> $DEPLOY_LOG
nohup java -jar $JAR_FILE > $APP_LOG 2> $ERROR_LOG &
CURRENT_PID=$(pgrep -f $JAR_FILE)
echo "$TIME_NOW > 실행된 프로세스 아이디 $CURRENT_PID 입니다." >> $DEPLOY_LOG
이렇게 하면 정상 배포되었을 때 서버에서
application.log로 프로젝트 로깅 확인
deploy.log에서 배포 과정의 로깅(실행 중인 프로세스 id) 확인
error.log에서 에러 로깅 가능
8. plain.jar 제외 (참고사항 먼저 보기)
Spring Boot 2.5 버전부터 빌드시 일반 jar
파일과 -plain.jar
파일이 동시에 생성된다고 한다.
plain jar은 어플리케이션 실행에 필요한 모든 의존성을 포함하지 않고, 작성된 소스코드의 클래스 파일과 리소스 파일만 포함한다.
따라서, java -jar
로 실행시 에러가 발생하게 된다.
build.gradle에 아래 코드 추가
jar {
enabled = false
}
(참고사항) 사실
stop.sh와 start.sh에서 jar 파일의 이름을 직접 명시해줬기 때문에 위 과정이 필요한 것 같지는 않다.
많은 자동 배포 블로그를 살펴본 결과, 스크립트 파일의 JAR_FILE에 *.jar
로 작성하는 사람들이 있었는데, 그들에게 필요한 건 아닌가 혼자 생각했다.
9. GitHub Actions에 deploy workflow 생성
pr closed시 deploy
CI편에서 main 또는 dev 브랜치에 pull request 했을 때 CI workflow를 실행하도록 설정했다.
CD(deploy)는 CI 과정이 정상적으로 마무리된 후에, pr이 종료되었을 때 실행하기 위하여 workflow를 분리하여 만들었다.
name: CD
on:
pull_request:
types: [ closed ]
branches: [ "main" ]
AWS 환경 변수
우리는 AWS의 S3에 압축 파일을 올리고 CodeDeploy를 이용하여 EC2 서버에 배포할 것이다. 이때, GitHub Actions는 S3와 CodeDeploy에 대하여 외부 접근을 하는 것이기 때문에 인증하고 접근하기 위하여 몇몇 환경 변수 정보가 필요하다. 이를 상단에 미리 적어줄 것이다.
AWS_REGION
: AWS 지역S3_BUCKET_NAME
: S3 버킷 이름CODE_DEPLOY_APPLICATION_NAME
: CodeDeploy 애플리케이션 이름CODE_DEPLOY_DEPLOYMENT_GROUP_NAME
: CodeDeploy 배포 그룹 이름
지금 생각해보니 이 정보들도 GitHub secrets로 가려주는 게 좋을 것 같다..!😮
env:
AWS_REGION: ap-northeast-2
S3_BUCKET_NAME: projectname-bucket
CODE_DEPLOY_APPLICATION_NAME: projectname
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: projectname-deployment-group
merge + closed인 경우에만 deploy
종료된 main에 대한 모든 pr에 대하여 해당 작업을 실행하면 한 가지 문제가 발생한다. pr은 성공적으로 merge하지 않더라도 종료(close)시킬 수 있다. 이 때에는 배포 작업이 이루어지면 안 된다. 이를 방지하기 위한 코드가 jobs의 deploy에 github.event.pull_request.merged == true
이다. pr에서 merge가 발생한 경우에만 배포를 진행한다.
jobs:
deploy:
if: github.event.pull_request.merged == true
runs-on, checkout, Set up JDK
runs-on은 말 그대로 어디에서 실행하는가?에 해당한다. ubuntu로 EC2 서버를 생성하여 사용하고 있으므로, ubuntu-latest를 적어준다.
그 다음에는 steps에 적힌 모든 과정이 실행된다. 본격적으로 빌드, 압축, 배포 과정이 시작되는 것이다.
actions/checkout@v4
는 현재 레파지토리의 내용을 로컬 디렉토리에 클론(clone)하여 워크플로우에서 사용할 수 있도록 한다. 이는 후속 스텝에서 해당 레파지토리의 파일들에 접근하고 조작할 수 있도록 한다.
아래 블로그의 내용이 되움될 것 같다.
프로젝트에서 사용한 JDK 버전을 명시한다.
name: Deploy
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
[⚠️주의 사항]: YAML은 중복된 키를 허용하지 않기 때문에 각 steps은 고유한 name과 uses를 가져야 한다.
환경/운영 정보 설정
졸업 프로젝트에서 FCM을 이용한 푸시 알림 기능을 구현해서 firebase service key 파일을 등록해주어야 했다. 이는 노출되어서는 안되는 정보이므로 레파지토리에 업로드하지 않고, GitHub Secrets에 등록하여 사용하였다. 해당 파일이 존재해야 하는 프로젝트의 특정 경로에 파일을 생성하는 과정은 아래 코드와 같다. resources 안의 firebase 폴더가 존재하지 않는 경우, 오류가 발생하므로 우선 파일을 만들고 파일을 생성한다.
#firebase 키 데이터 파일 정보
- name: make firebase dir
run: |
cd ./src/main/resources
mkdir firebase
- name: Set Firebase Service Key JSON File
id: create-json
uses: jsdaniell/create-json@v1.2.2
with:
name: "firebase_service_key.json"
json: ${{ secrets.FIREBASE_SERVICE_KEY }}
dir: './src/main/resources/firebase'
[⚠️이슈]: 파이어베이스 서비스 키 파일은 json 형식인데 이를 secrets를 통해 사용함에 있어 작은 이슈가 있었다. 이 문제와 해결 방법은 아래 포스트에 적어두었다.
${{ secrets.?? }}과 같이 사용되는 정보들은 모두 GitHub의 Secrets에 등록해둬야 한다.
서버에서 직접 배포했을 때에는 실행 명령어에 프로필을 포함한 모든 환경 정보를 직접 작성하였다. 하지만, secrets에 배포에 사용되는 운영 환경 정보에 대한 application.yml 파일을 등록하고, workflow 과정 중에 이를 프로젝트에 적재하여 사용할 수 있다.
#운영 환경에 대한 설정 파일 정보
- name: Set Release YML File
run: |
cd ./src/main/resources
touch ./application.yml
echo "${{ secrets.RELEASE_YML }}" > ./application.yml
Gradle 설정 및 build
1. Gradle을 설정
2. 실행 권한을 줌
3. 프로젝트 빌드
- name: Setup Gradle
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
- name: Grant Execute Permission for Gradlew
run: |
chmod +x ./gradlew
shell: bash
- name: Build with Gradle Wrapper
run: ./gradlew clean build --exclude-task test
AWS 인증, S3 버킷에 업로드 및 EC2에 배포
1. AWS의 서비스를 사용하기 위한 인증 과정
- 위에서 GitHub Actions 용으로 생성한 IAM 사용자 계정 정보 이용
2. AWS S3에 압축 파일 업로드
3. CodeDeploy 실행 → 프로젝트 배포
#AWS 인증
- 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: ${{ env.AWS_REGION }}
#빌드 결과물 S3 버킷에 업로드
- name: Upload to AWS S3
run: |
aws deploy push \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--ignore-hidden-files \
--s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
--source .
#S3 버킷에 있는 파일 CodeDeploy 실행
- name: Deploy to AWS EC2 from S3
run: |
aws deploy create-deployment \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip
deploy.yml 전체 코드
#./.github/workflows/deploy.yml
name: CD
on:
pull_request:
types: [ closed ]
branches: [ "main" ]
env:
AWS_REGION: ap-northeast-2
S3_BUCKET_NAME: projectname-bucket
CODE_DEPLOY_APPLICATION_NAME: projectname
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: projectname-deployment-group
permissions:
contents: read
jobs:
deploy:
if: github.event.pull_request.merged == true
name: Deploy
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
#firebase 키 데이터 파일 정보
- name: make firebase dir
run: |
cd ./src/main/resources
mkdir firebase
- name: Set Firebase Service Key JSON File
id: create-json
uses: jsdaniell/create-json@v1.2.2
with:
name: "firebase_service_key.json"
json: ${{ secrets.FIREBASE_SERVICE_KEY }}
dir: './src/main/resources/firebase'
#운영 환경에 대한 설정 파일 정보
- name: Set Release YML File
run: |
cd ./src/main/resources
touch ./application.yml
echo "${{ secrets.RELEASE_YML }}" > ./application.yml
- name: Setup Gradle
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
- name: Grant Execute Permission for Gradlew
run: |
chmod +x ./gradlew
shell: bash
- name: Build with Gradle Wrapper
run: ./gradlew clean build --exclude-task test
#AWS 인증
- 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: ${{ env.AWS_REGION }}
#빌드 결과물 S3 버킷에 업로드
- name: Upload to AWS S3
run: |
aws deploy push \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--ignore-hidden-files \
--s3-location s3://$S3_BUCKET_NAME/$GITHUB_SHA.zip \
--source .
#S3 버킷에 있는 파일 CodeDeploy 실행
- name: Deploy to AWS EC2 from S3
run: |
aws deploy create-deployment \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--s3-location bucket=$S3_BUCKET_NAME,key=$GITHUB_SHA.zip,bundleType=zip
자 이제 해보자
pull request 생성 및 build workflow 완료시
merge pull request 클릭 후 deploy workflow 진행
deploy 성공시
Actions에서도 확인 가능
❗️AWS CodeDeploy에서도 확인이 필요
여기까지 완료한 후에 이제 됐다는 행복함이 찾아오는데, deploy workflow 실행 성공 이후에 CodeDeploy에서도 확인이 필요하다. stop.sh, start.sh 내 경로 설정에서 시행 착오를 많이 겪었다..ㅜㅜ
GtiHub Actions에서 성공을 했어도 배포를 하는 과정에서 실패할 수 있기 때문에 확인해야 한다.
CodeDeploy에서 실패시
AWS > CodeDeploy > 배포 > 실패한 배포 > 하단 배포 수명주기 이벤트 > View events
에서 원인을 살펴볼 수 있다.
개발 중 오류를 마중할 때와 같이 에러 메시지 잘 읽어보고 해결하면 된다.🥲