기존에는 CI만 구현되어 있는 상태였고, PR merge 후 EC2 서버에 수동 배포했다.
매번 배포할 때마다 우분투 서버에서 아래 과정들을 반복했는데, 시간이 오래 걸리고 번거로웠다.
- 깃허브 프로젝트 클론 받기
- gitignore 파일 직접 추가
- 프로젝트 빌드해 jar 파일 생성
- 기존에 실행 중인 프로세스 종료
- jar 파일 백그라운드에서 실행
이에 Github Actions와 Docker를 활용해 배포를 자동화하였다.
이번 글에서는 어떻게 배포를 자동화했는지 정리해보려고 한다.
cd.yml을 작성하기 전 Dockerfile 추가, EC2 패스워드 연결 활성화, Github Secrets 환경 변수 추가가 선행되어야 한다.
Dockerfile 추가
DockerFile은 자바 Docker 이미지를 생성하기 위한 스크립트이다.
폴더 최상단에 Dockerfile을 추가했는데 코드 각 기능은 다음과 같다.
FROM openjdk:17-jdk
ARG JAR_FILE=api/build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
[1] 시작할 자바 이미지 파일을 지정한다.
[2] jar 파일 경로를 지정한다.
[3] JAR_FILE을 app.jar로 복사한다.
[4] 컨테이너 시작 시 실행할 기본 명령을 지정한다.
EC2 패스워드 연결 활성화
깃허브에서 AWS EC2에 접근해야 한다.
이에 pem 키가 아닌 비밀번호로도 접근할 수 있도록 EC2를 설정하였다. (참고 블로그)
아래 명령어를 통해 아이디를 확인하고 비밀번호를 초기화한다.

이후 패스워드 연결 방식을 설정하기 위해 sshd_config 파일에 접속한다.

파일 내 PasswordAuthentication 부분을 no->yes로 수정해서 패스워드 인증을 활성화했다.

sshd를 재실행하여 변경 사항을 반영하였다.

Github Secrets에 환경변수 추가
민감 정보들을 github secrets에 넣고, cd 스크립트를 실행 시 가져오도록 할 것이다.
Dockerhub 계정 정보, AWS 계정정보, 프로젝트 내 민감 파일을 secrets에 추가하였다.
프로젝트 민감 파일은 application.yml, firebase 토큰 파일이 있었고, base64로 인코딩해서 넣었다. (참고 블로그)
cd.yml 작성
gitignore 파일 추가
application.yml, firebase.json은 gitignore에 포함되어 있으므로 추가해 주는 작업이 필요하다.
mkdir로 파일을 넣을 디렉토리를 생성하였다.
echo 명령어로 secret 환경 변수값을 출력하고, base64로 디코딩한 후 '>' 뒤 명시된 파일에 저장했다.
# yml 파일 생성
- name: Generate application-core.yml
run: |
mkdir -p ./core/src/main/resources
echo "${{ secrets.APPLICATION_CORE }}" | base64 -d > ./core/src/main/resources/application-core.yml
# firebase json 파일 생성
- name : Generate firebase_account.json
run: |
mkdir -p ./core/src/main/resources/firebase
echo "${{ secrets.FIREBASE_ACCOUNT }}" | base64 -d > ./core/src/main/resources/firebase/firebase_account.json
Gradle 빌드
이 부분은 이전에 작성했던 ci.yml과 유사해서 이해하는데 어렵지 않았다.
하나의 차이점이라면, 배포용이라 테스트가 불필요하므로 -test 옵션을 추가해 test 경로를 제외하고 빌드하였다.
# gradle 권한 부여
- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash
# 빌드
- name: Build with Gradle
run: ./gradlew build -x test
도커 이미지 빌드 및 푸시
앞에서 작성했던 Dockerfile을 참조하여 jar파일을 이미지로 빌드한다.
이후 내가 생성한 Docker Hub private 레포지토리에 푸시하였다.
# 도커 이미지 빌드 및 푸시
- name: docker image build and push
run: |
docker build -f Dockerfile -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_APP_NAME }} .
docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_APP_NAME }}
서버 백그라운드 실행
이미지를 pull 받고 컨테이너로 실행하는 코드이다.
Github Actions가 EC2에 접근하기 위해 필요한 정보들을 파라미터로 넣어줬다.
- host: EC2의 퍼블릭 IPv4 주소
- username: 앞서 확인한 EC2 아이디 (ubuntu)
- password: 앞서 재설정한 EC2 비밀번호
- port: SSH로 접근할 포트
- name: pull image and run container
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USERNAME }}
password: ${{ secrets.EC2_PASSWORD }}
port: ${{ secrets.EC2_PORT }}
script: |
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_APP_NAME }}
docker stop ${{ secrets.DOCKERHUB_APP_NAME }}
docker rm ${{ secrets.DOCKERHUB_APP_NAME }}
docker run -d \
-p 8080:8080 \
-e TZ=Asia/Seoul \
--name ${{ secrets.DOCKERHUB_APP_NAME }} \
${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_APP_NAME }}
docker system prune -f
script 키워드를 통해 실행할 명령어들을 나열했다.
먼저 도커 이미지를 레포지토리로부터 pull 받는다.
이후 기존에 실행 중이던 docker 컨테이너를 멈춘 후 제거한다.
docker run으로 새로운 컨테이너를 생성하고 실행한다.
-d는 백그라운드 모드이고, -p는 포트 설정 -e는 컨테이너의 환경 변수 지정이다.
--name을 통해 실행할 이미지를 지정하였다.
마지막으로 사용하지 않는 도커 리소스를 정리했다.
실행 결과
PR merge 시 CD work flow가 실행된다.


트러블 슈팅
문제
Unable to connect to Redis server
애플리케이션이 EC2 서버 IP:6379 포트로 연결을 시도했지만 연결이 거부당했다.
해결1: application.yml에서 redis host 변경
도커 컨테이너에서 jar 파일을 실행하는데, 도커는 localhost를 컨테이너라고 인식한다.
이에 따라 redis 호스트에 localhost가 아닌 EC2 IPv4 주소를 적었다.
data:
redis:
host: [EC2 IPv4 주소]
port: 6379
repositories:
enabled: false
해결2: Redis.conf 설정 변경
우분투 서버에서 실행 중인 Redis 서버는 보안으로 인해 localhost 접속만 허용한다.
도커 컨테이너에서 Redis 서버에 접근하기 위해 외부 IP 주소 접속을 허용해야 한다.
redis.conf 설정 파일에 들어가 외부 네트워크 접속을 허용하자.

변경 사항을 반영하기 위해 Redis를 재시작한다.
확인해 보면 0.0.0.0으로 변경되어 Docker에서 redis에 접근 가능하다.

sudo vi /etc/redis/redis.conf #설정 파일 접근
sudo systemctl restart redis #설정 파일 수정 후 Redis 재시작
ps -ef | grep redis # redis 정보 조회
마무리
CI/CD를 구축해 본 경험이 없었기에 프로젝트 내내 큰 벽처럼 느껴졌다.
CI/CD를 구현하는 과정에서 도커, 리눅스 명령어도 공부할 수 있어서 뜻깊었다.
다음 프로젝트에서는 자신 있게 CI/CD 파이프라인을 구축할 수 있을 것 같다.
Reference
https://lucas-owner.tistory.com/49
https://lucas-owner.tistory.com/50
https://0m1n.tistory.com/100
https://supreme-ys.tistory.com/161