이제 AWS 환경에 배포를 자동화해보자.
EC2 인스턴스에서 Docker 이미지를 가져올 수 있도록 Docker Hub에 업로드한다.
docker tag my-spring-boot-app <docker-hub-username>/my-spring-boot-app:latest
docker push <docker-hub-username>/my-spring-boot-app:latest
Bash
복사
시간이 좀 걸리지만, 업로드가 잘 된 것 같다.
다음은 GitHub Actions를 활용한 배포 자동화 파이프라인에 대해 살펴보자.
GitHub Actions 워크플로우 작성
GitHub 레포지토리에서 .github/workflows/deploy.yml 파일을 생성하여 CI/CD 워크플로우를 정의한다. 이 파일은 Docker 이미지를 빌드하고, Docker Hub에 푸시한 다음, CodeDeploy로 배포를 요청하는 역할을 한다.
name: Deploy to EC2
run-name: Running CICD Workflow
on:
push:
branches:
- main
env:
AWS_REGION: ap-northeast-2
AWS_S3_BUCKET: voya9e-s3
AWS_CODE_DEPLOY_APPLICATION: voya9e-cd
AWS_CODE_DEPLOY_GROUP: voya9e-cd-group
jobs:
deploy:
runs-on: ubuntu-22.04
steps:
- name: Checkout to main branch
uses: actions/checkout@v3
with:
ref: main
- name: Install JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Build project
run: ./gradlew clean build -x test
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
tags: <docker-hub-username>/my-spring-boot-app:latest
push: true
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: ${{ env.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Upload to S3
run: |
zip -r release.zip .
aws s3 cp release.zip s3://${{ env.AWS_S3_BUCKET }}/cicdtest/$GITHUB_SHA.zip
- name: Deploy to EC2 using CodeDeploy
run: |
aws deploy create-deployment \
--application-name ${{ env.AWS_CODE_DEPLOY_APPLICATION }} \
--deployment-config-name CodeDeployDefault.AllAtOnce \
--deployment-group-name ${{ env.AWS_CODE_DEPLOY_GROUP }} \
--s3-location bucket=${{ env.AWS_S3_BUCKET }},key=cicdtest/$GITHUB_SHA.zip,bundleType=zip
YAML
복사
각 요소에 대해 설명해보자면,
•
name: 워크플로우의 이름으로, GitHub Actions의 작업 목록에서 식별하기 위한 이름
•
run-name: 워크플로우가 실행될 때 표시되는 이름
•
on: 워크플로우가 실행될 조건을 정의
•
push: 특정 브랜치에 코드가 푸시될 때 워크플로우가 트리거됨
•
branches: main 브랜치에 코드가 푸시될 때 실행
•
jobs: 이 워크플로우에서 수행할 작업 목록
•
deploy: 작업(job)의 이름
•
runs-on: ubuntu-latest: 이 작업이 ubuntu-latest 이미지에서 실행됨을 지정
•
steps: 각 steps는 특정 작업을 수행하며, 순서대로 실행됨
•
name: 이 단계의 이름
위의 과정에서 필요한 secrets key를 설정하려면, 아래와 같이, Actions의 Repository Secrets에 필요한 키 값들을 저장해두고, 워크플로에 불러와서 사용할 수 있다. 정말 간편하고 좋은 것 같다.
AWS IAM Secret Key는 기존에 버킷만들때도 쓰였고 하니, 저장된 부분을 만들어주면 되고, Docker Hub에서 키 발급받는 방법은 아래 링크를 참조하자.
Read & Write로 만들어줬다.
Code Deploy AppSpec 작성
AppSpec 파일을 사용해서 프로젝트의 어떤 파일들을 EC2 의 어떤 경로에 복사할지 설정 가능하고, 배포 프로세스 이후에 수행할 스크립트를 지정하여 자동으로 서버를 띄울 수도 있다.
공식 문서를 참고하면 좋다. - AWS CodeDeployAppSpec '후크' 섹션 - AWS CodeDeploy
아래의 파일을 프로젝트 루트 디렉토리에 작성해준다.
•
appspec.yml
version: 0.0
os: linux
files:
- source: /
destination: /home/ubuntu/app
overwrite: yes
permissions:
- object: /
owner: ubuntu
group: ubuntu
hooks:
BeforeInstall:
- location: scripts/before_install.sh
timeout: 300
runas: ubuntu
- run: chmod +x scripts/*.sh
ApplicationStart:
- location: scripts/start_server.sh
timeout: 300
runas: ubuntu
ValidateService:
- location: scripts/validate_service.sh
timeout: 300
runas: ubuntu
YAML
복사
여기서 중요한 부분은 hooks이다. Code Deploy의 작업은 아래와 같은 Life Cycle을 가지고 있는데, 필요한 부분에 hooks를 작성해주면 된다.
나는 BeforeInstall, ApplicationStart, ValidateService훅에 각각 before_install.sh, start_server.sh, validate_service.sh를 작성해주었다.
before_install.sh
이 스크립트는 기존 컨테이너를 중지하고, Docker 이미지를 업데이트하는 역할을 한다.
#!/bin/bash
# 기존 컨테이너 중지 및 삭제
docker stop my-spring-boot-app || true
docker rm my-spring-boot-app || true
# 최신 이미지 가져오기
docker pull <your-docker-hub-username>/my-spring-boot-app:latest
YAML
복사
start_server.sh
새로운 Docker 컨테이너를 시작한다.
#!/bin/bash
# Docker 컨테이너 실행
docker run -d -p 8080:8080 --name my-spring-boot-app <docker-hub-username>/my-spring-boot-app:latest
YAML
복사
validate_service.sh
애플리케이션이 정상적으로 작동하는지 확인하는 스크립트. HTTP 상태 코드를 확인하여 서비스가 성공적으로 시작되었는지 확인한다.
#!/bin/bash
# 서비스 상태 확인
curl -f http://localhost:8080 || exit 1
YAML
복사
배포
이제 main 브랜치에 코드 push를 발생시키고, 잘 진행되는지 확인한다.
워크플로우는 깔끔하게 완료되었다!
S3에도 압축된 코드 파일이 잘 올라와 있는걸 확인했다. 이제 EC2 빌드만 잘 되면 된다.
실패 1
그러고 codedeploy를 확인해 봤는데..
배포가 실패했고 위와 같은 로그가 올라와있었다.
실패지점을 확인해봤더니, 딱 appspec 들어가는 지점부터 막혔다.
우선 로그 메시지는 CodeDeploy Agent와의 연결이 안된다 == EC2와의 연결이 제대로 안되고 있다는 소리였기에, EC2에 접속해서 CodeDeploy Agent 상태를 확인했다.
너무나 잘 작동하고 있었기에, 일단 restart 한 번 해주고, cat /var/log/aws/codedeploy-agent/codedeploy-agent.log 명령어로 로그를 확인했더니, 아래와 같이 나왔다.
찾아보니 스크립트 실행 권한에 대한 문제로, 위에서 확인한 실패지점과 일치하는 상황이었다.
기존에 BeforeInstall 훅에 run으로 스크립트 권한 부여를 설정했는데, 아무래도 이부분이 먹히지 않은 것 같다. 생각해보면 당연한게 애초에 스크립트 실행자체가 안되는데 어떻게 내부 로직 실행을 기대한건지.. ㅋㅋ
해결방법으로 로컬에서 스크립트 작성을 할 때, push 전 미리 실행권한을 부여하는 방법을 사용했다. 설정 후, 다시 배포해보니 스크립트 실행단계는 잘 수행되었다.
실패 2
이번엔 validate_service에서 오류가 생겼다. curl을 날려도 애플리케이션 실행이 되지 않아 실행 검증이 불가능한 상황이었다. 원래 실행이 좀 느릴때가 있으니까, 대기시간을 늘리면 어떨까 해서 아래 로직을 추가했었다.
#!/bin/bash
for i in {1..10}; do
curl -f http://localhost:8080 && exit 0
echo "Waiting for the application to be ready..."
sleep 5
done
echo "Application failed to start within expected time."
exit 1
YAML
복사
5초 간격으로 최대 10회까지 애플리케이션 상태 확인을 재시도하는 방식이다.
하지만 로그만 늘어날뿐, 결과는 똑같았다.
때문에, 내부적으로 docker 컨테이너가 실행되지 않았는지 확인했고, 금방 꺼지긴 하지만 실행된 흔적이 있었기에, docker logs my-spring-boot-app 으로 로그를 찍어보았다.
위와 같이, datasource를 읽을 수 없다는 오류가 계속 발생했다. application.yaml 파일을 못읽어온다 생각하여, 로컬에서 마운트가 안되나보다 싶어서 start_server.sh를 아래와 같이 수정했다.
#!/bin/bash
# Docker 컨테이너 실행
docker run -d -p 8080:8080 \
-v $(pwd)/src/main/resources/application.yaml:/app/config/application.yaml \
--name my-spring-boot-app geonoo/my-spring-boot-app:latest
YAML
복사
그러나 똑같은 오류가 떴고, 잠깐 실행되는 그 몇초간 docker exec -it my-spring-boot-app ls /app/config/ 명령어로 application.yaml 파일 유무를 확인해본 결과, 제대로 올라가는 것 같았다. 내부 내용을 볼 수 없는게 답답했지만..
mysql driver 런타임 실행부분 implementation으로 변경 후 빌드도 해보고 application.yaml 마운트 방식 수정도 해봤다… 아무것도 이걸 해결할 수 없었다.
바보 1
이유는 당연. gitignore 설정 해놓고, github에 push하는데 당연히 secret 파일이 올라갈 리가 없었다;; 참..
그래서 선택한 방법은 아까 필요한 키값들을 저장해뒀던 Actions의 Repository Secrets에 secret 파일을 통째로 저장한 후, 이를 불러와서 사용하는 방식이다. 물론 application.yaml이 없데이트될때마다 매번 들어가서 수정해줘야하는 번거로움이 있기에, 이 부분도 추후 개선해보려고 한다.
바보 2
docker hub에 이미지 올려놓고,, 굳이굳이 s3에 코드압축파일 올려서 codedeploy로 배포하는 바보짓을 하고 있었다… 배포 인프라에 대한 이해가 부족했다..
name: Deploy to EC2
run-name: Running CICD Workflow
on:
push:
branches:
- main
env:
AWS_REGION: ap-northeast-2
jobs:
deploy:
runs-on: ubuntu-22.04
steps:
- name: Checkout to main branch
uses: actions/checkout@v3
with:
ref: main
- name: Install JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'corretto'
- name: Set up application.yaml from GitHub Secrets
run: |
mkdir -p ./src/main/resources
echo "${{ secrets.APPLICATION }}" > ./src/main/resources/application.yaml
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Build project
run: ./gradlew clean build -x test
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v2
with:
context: .
tags: <docker-hub-username>/my-spring-boot-app:latest
push: true
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: ${{ env.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }}
- name: Deploy to EC2
uses: appleboy/ssh-action@v0.1.7
with:
host: ${{ secrets.EC2_HOST }}
username: ubuntu
key: ${{ secrets.EC2_SSH_KEY }}
script: |
sudo rm -rf /home/ubuntu/app/config/application.yaml
sudo mkdir -p /home/ubuntu/app/config
echo "${{ secrets.APPLICATION }}" | sudo tee /home/ubuntu/app/config/application.yaml > /dev/null
docker pull <docker-hub-username>/my-spring-boot-app:latest
docker stop my-spring-boot-app || true
docker rm my-spring-boot-app || true
docker run -d -p 8080:8080 \
-v /home/ubuntu/app/config/application.yaml:/app/config/application.yaml \
--name my-spring-boot-app <docker-hub-username>/my-spring-boot-app:latest
YAML
복사
code deploy 설정도, 이를 위한 appsepce과 scripts파일도 필요 없었다.. ㅎㅎ 그냥 dockerhub에서 pull 해와서 그대로 컨테이너 돌려주면 되는 것…
돌려보면, 기존 코드에 redis 사용 부분이 있어서 redis 연결 오류가 뜨는데, ec2에 redis를 설치 후, application.yaml의 redis.host를 ec2 퍼블릭 IP로 지정해주면 docker에서 ec2내부의 redis 서버로 연결된다.
성공~