Docker + Github Action + Spring Boot 자동배포환경
본 게시물 내용은 docker와 docker-compose 그리고 github action에 대한 이해용으로 참고해주시고 혹시 실습으로 🚨hub에 이미지를 push하셨다면 실습 후에 올리신 이미지는 삭제🚨 해주시기 바랍니다.(자세한 이유는 댓글을 봐주세요😃)
최근에 github action을 이용해서 spring boot서버의 자동배포환경을 구축했습니다. docker-compose와 nginx도 함께사용하니 비슷한 설정을 원하시는 분들께 도움이 될 것같네요👍 어렵고 성가신(?) 과정이지만 초반에 잘 세팅해놓으면 서버개발이 진짜 편해집니다..(어려운만큼 만들어 놓으면 뿌듯👻)
배포환경자동화에는 여러가지 방법이 있습니다. 그중에서 저는 Docker Hub를 활용해서 코드를 옮겨야하는 불편함을 제거하려고 했습니다. 그럼 한 단계씩 자세히 설명을 해보겠습니다~☀️
먼저 spring boot 프로젝트의 코드는 간단합니다 테스트용이기 때문에 확인만할 수 있게 작성했습니다. /sample이라는 url로 접속시에 "sample!!"이 뜨게한 RestController를 하나 작성해줬습니다. 작성하시고 로컬에서 잘 돌아가는지 한번 프로젝트 실행해보세요!! 그럼 완료입니다
Sample.class
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class Sample {
@RequestMapping("/sample")
public String greeting(){
return "sample!!";
}
}
그리고 도커파일을 작성해줍니다. 도커파일은 이미지를 빌드하기위한 파일이고 이 이미지를 기반으로 컨테이너가 실행됩니다. 내용은 base이미지를 openjdk:11로 JAR_FILE변수에 파일경로를 등록하고 해당경로의 파일을 컨테이너의 app.jar로 옮기고 컨테이너에서 java -jar /app.jar을 실행한다는 뜻입니다. 여기서 파일경로를 보시면 알겠지만 저기에는 빌드파일이 들어가겠네요 즉 프로젝트 빌드파일을 실행하겠다는 명령어입니다.
FROM openjdk:11
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
즉 코드가 수정될때 마다 빌드파일만 새로 구워주면 컨테이너는 실행하며 빌드파일을 실행해주겠다는 것이죠 여기서 openjdk:11은 제가 작성한 프로젝트가 jdk 11버전이기 때문입니다. 따라서 여러분이 작성하는 프로젝트가 다른버전이라면 그에 맞게 base image 버전을 바꿔주셔야합니다.
저는 nginx도 같이 컨테이너화해서 spring boot 앞단에 붙여줄겁니다. 따라서 docker-compose를 사용할겁니다. 먼저 nginx설정을 해줍시다~ 프로젝트에서 /nginx/conf.d/nginx.conf
경로에 맞게 nginx.conf파일을 작성해줍니다.
80포트로 받아서 8080으로 보내준다는거고 설정들은 구글링하면서 필요한 것들만 가져와봤습니다. ec2DNS:80포트로 접속할거라 server_name을 설정해줬고 proxy_pass 에서는 web이 springboot 컨테이너의 이름을 web으로 할 것이라서 그렇습니다. 지금은 좀 이해가 안갈 수 있지만 docker-compose에 가서 다 쓰입니다!!
server {
listen 80;
server_name *.compute.amazonaws.com
access_log off;
location / {
proxy_pass http://web:8080;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
이제 제가 해준 위의 설정을 갖게 된 nginx를 이미지로 구워봅시다. 아래와 같이 nginx를 위한 dockerfile을 작성해줍니다.
dockerfile-nginx
FROM nginx
COPY ./nginx/conf.d/nginx.conf /etc/nginx/conf.d
기본 base image는 nginx로 그리고 /etc/nginx/conf.d에 아까 그 파일을 옮겨주려고합니다. (이 경로가 nginx에서 지정한 설정파일경로입니다)
이제 이미지를 구워서 docker hub에 올립니다.
> docker build -f dockerfile-nginx -t rmswjdtn/second-eyes-nginx .
> docker push rmswjdtn/second-eyes-nginx
이제 docker-compose.yaml을 작성해봅시다. 컨테이너는 web, nginx 두개를 만들거고 이름도 잘적어줍니다(위에서 web이 이걸말한 겁니다~) 이제 각각 어떤 이미로 만들 것인지 이미지 이름을 넣어줍니다. 두 이미지 다 제가 구워서 docker-hub에 올려놓은 것을 사용할것이라 그 이름으로 넣어줬습니다. 포트도 각각 작성합니다. 이제 이 docker-compose파일을 ec2에 접속해서 넣어줍니다.
docker-compose.yaml
version: '3'
services:
web:
container_name: web
image: rmswjdtn/second-eyes-web
expose:
- 8080
ports:
- 8080:8080
nginx:
container_name: nginx
image: rmswjdtn/second-eyes-nginx
ports:
- 80:80
depends_on:
- web
이제 github action의 workflow를 작성합시다. github action은 특정레퍼지토리의 동작을 트레킹해서 개발자가 작성한 workflow대로 행동하는 CICD툴입니다.
먼저 프로젝트를 올린 깃헙에 Actios에 들어가 새로운 workflow를 생성합니다.
그럼 이렇게 기본workflow가 나옵니다. 이름은 원하시는 것으로 바구고 commit을 누릅니다. 그럼 레포지토리에 .github이라는 폴더아래 workflow가 추가되어있을 것입니다. 로컬로 pull받아서 작업을 이어합시다!
저는 아래와 같이 workflow를 작성했습니다. 아래에서 하나씩 뜯어서 설명을 해보겠습니다.
name: Java CI with Gradle
on:
push:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
## create application-database.yaml
- name: make application-database.yaml
run: |
## create application-database.yaml
cd ./src/main/resources
# application-database.yaml 파일 생성
touch ./application-database.yaml
# GitHub-Actions 에서 설정한 값을 application-database.yaml 파일에 쓰기
echo "${{ secrets.DATABASE }}" >> ./application-database.yaml
shell: bash
## gradle build
- name: Build with Gradle
run: ./gradlew bootJar
## 웹 이미지 빌드 및 도커허브에 push
- name: web docker build and push
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t ${{ secrets.DOCKER_REPO }}/second-eyes-web .
docker push ${{ secrets.DOCKER_REPO }}/second-eyes-web
## docker compose up
- name: executing remote ssh commands using password
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ubuntu
key: ${{ secrets.KEY }}
script: |
sudo docker rm -f $(docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_REPO }}/second-eyes-web
sudo docker pull ${{ secrets.DOCKER_REPO }}/second-eyes-nginx
docker-compose up -d
docker image prune -f
기본적인 부분들입니다. 이름은 바꿔주셔도됩니다. 그리고 on은 어떤 조건에서 workflow를 실행할지 컨디션을 적어주는 것입니다. 저는 main브랜치에 push될때마다 재배포하는 것으로 했습니다.
name: Java CI with Gradle
on:
push:
branches: [ "main" ]
permissions:
contents: read
이제 jobs밑으로 어떤일들을 실행할지 하나씩 적어주면 됩니다. runs-on은 github action의 CI서버 runner가 어떤 환경을 갖출지 고르는 것입니다. 저는 ubuntu-latest로 설정해줬습니다. 이제 steps밑으로 하나씩 처리할 일들을 순서대로 적어줍니다.
형식은 name(옵션)이름을 적어주고 uses나 run으로 어떤일을 할지 적어줍니다. 여기서 uses는 이미 만들어져있는 것을 가져다쓰는 것이고 run은 직접 명령어를 적어준다는 의미입니다.
1번은 checkout인데 github action과 연결된 레포지토리의 코드를 runner로 옮기는 것입니다. 즉 매번 새로 푸쉬된 코드로 일을 처리하는 것이죠
2번은 set up을 해주는 것입니다. jdk 11버전으로 작성된 프로젝트이기 때문에 이에 맞춰 setup을 해줍니다. 여기까지는 다 uses를 이용해서 이미 작성된 것들을 가져다가 쓰게됩니다 (정말편하죠👍)
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout # 1
uses: actions/checkout@v3
- name: Set up JDK 11 # 2
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
환경파일이라고 하기는 했지만 github에 올리진 못했던 정보가든 설정파일을 runner에 만들어주겠다는 것입니다. 저는 DB정보가 담긴 application-database.yaml파일을 github에 올리지 않고 레포지토리 settings의 secret에 DATABASE라는 이름으로 올려놨습니다. 따라서 거기의 내용을 옮겨서 파일을 runner에 만들어줄 수 있습니다.
## create application-database.yaml
- name: make application-database.yaml
run: |
# create application-database.yaml
cd ./src/main/resources
# application-database.yaml 파일 생성
touch ./application-database.yaml
# GitHub-Actions 에서 설정한 값을 application-database.yaml 파일에 쓰기
echo "${{ secrets.DATABASE }}" >> ./application-database.yaml
shell: bash
참고로 application.yaml은 아래와 같이 올라가있습니다. 프로젝트를 돌리면 application-database.yaml을 찾아서 설정정보를 참고하게됩니다.
spring:
profiles:
include: database
application-database.yaml은 아래와 같이 민감한 데이터베이스 접속정보 및 설정을 담고있습니다. 전 JPA, RDB, MySQL을 사용했고 데이터베이스를 연결하는 분들은 프로젝트에 맞는 정보를 넣어주시면 됩니다.
spring:
jpa:
show-sql: true
generate-ddl: true
hibernate:
ddl-auto: update
datasource:
url: 각자적기
username: 각자적기
password: 각자적기
hikari:
maximum-pool-size: 10
이제 이 코드를 기반으로 빌드파일을 만들어줍니다. 그럼 build폴더가생기고 그 안에 .jar가 생성됩니다. 어디에? 바로 Runner에 생깁니다!
## gradle build
- name: Build with Gradle
run: ./gradlew bootJar
자 이제 새로 빌드한 파일을 넣은 이미지를 빌드해줘야겠죠? 그리고 그 이미지를 docker hub에 push해서 ec2서버에서 쉽게 가져다가 쓸 수 있게합시다!
## 웹 이미지 빌드 및 도커허브에 push
- name: web docker build and push
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -t ${{ secrets.DOCKER_REPO }}/second-eyes-web .
docker push ${{ secrets.DOCKER_REPO }}/second-eyes-web
도커계정정보도 다 secret에 넣어놨습니다. 따라서 레포이름/second-eyes-web이라는 이름의 이미지가 우리가 작성해준 dockerfile을 기반으로 생성되겠네요
dockerfile을 다시 보자면 방금빌드한 따끈따끈한 jar파일이 app.jar로 복사되겠네요 그리고 그것을 이미지로 구워서 push한 것입니다.
FROM openjdk:11
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
드디어 마지막과정입니다. 저는 nginx컨테이너도 만들어줄거라 docker-compose를 사용하려고합니다. 이제 드디어 우리의 서버 ec2에서 작업을 해봅시다. 그럼 ssh접속을 해야겠죠?
appleboy/ssh-action@master
를 통해서 ssh접속이 가능합니다. 접속시 필요한 정보는 모두 secret에 등록해줬습니다. HOST는 ec2 퍼블릭 DNS를 KEY는 pem키를 복사해서 넣어줬어요. script부분은 이제 ec2에 접속해서 실행할 명령어입니다. 먼저 필요없는 컨테이너들을 지워주고 web, nginx이미지를 docker hub에서 pull받고 docker-compose up -d(background실행)로 실행하게됩니다.
## docker compose up
- name: executing remote ssh commands using password
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST }}
username: ubuntu
key: ${{ secrets.KEY }}
script: |
sudo docker rm -f $(docker ps -qa)
sudo docker pull ${{ secrets.DOCKER_REPO }}/second-eyes-web
sudo docker pull ${{ secrets.DOCKER_REPO }}/second-eyes-nginx
docker-compose up -d
docker image prune -f
🥲근데 pem key어떻게 확인해요?
mac기준 터미널에서 cat <pem키경로>를 하면 (그냥 pem키 터미널로 drag and drop하시면 경로작성됩니다) --------BEGIN---------이러면서 내용이 나와요 처음 ------ 부분과 마지막 ------- 부분까지 싹다 복사해줍니다 마지막에 붙은 %는 제외입니다!!!
🥲secret들은 어디에 등록해요?
repository settings에 Secrets>Actions에 등록해주면 됩니다.
이제까지 작성한 코드를 메인에 push하면 repo에 Actions에 아까 작성한 일들이 하나씩 실행됩니다. 각각 step별로 로그가 찍히는데 오류가 난다면 로그를 읽으면서 확인해보시면 됩니다.
마지막으로 ec2DNS/sample로 접속하면 sample!!이 뜨는지 확인해보면 됩니다~
ssh: handshake failed: ssh: unable to authenticate, attempted methods [none publickey], no supported methods remain
이 과정에서 대부분 자잘한 오류를 겪게 될 것입니다. 거의 오타나 간단한 오류들이 많은 것입니다. 그런데!!! 우분투 최신버전으로 ec2서버를 만드신 분들은 ssh접속하는 부분
에서 오류가 납니다. 이미 해결법이 나온 이슈이기 때문에 올려놓겠습니다~✨✨
배포환경자동화에 도움이 되었기를 바라면서 추가로 도움이 될만한 링크들 남겨놓겠습니다⭐️