이번 포스팅에서는 엄청난 우여곡절 끝에 성공해낸 Github actions + Docker CI/CD에 대해 포스팅 하려한다.
원래는 AWS S3에 이미지 CRUD를 진행하려했다. 하지만, 해외가능 카드가 존재하지 않아 발급하였다.
그래서, 남는 시간에 CI/CD를 시작했다.
나의 서버 환경은 다음과 같다.
- github actions
- VirtualBox 가상머신 YN01
- VirtualBox 가상머신 YN02
설정한 가상머신 네트워크 환경에 대해 간략하게 설명하자면,
통신사 모뎀 -> 와이파이, 데스크탑 -> 데스크탑 내 가상머신(어댑터에 브릿지)
여기서 가상머신 네트워크 설정을 어댑터에 브릿지로 설정하면 모뎀 입장에서는 해당 가상머신을
하나의 호스트로 인식하여 IP를 할당해준다.(가상머신에 대한 자세한 설명은 생략.)
1. Ngrok 설치
CI/CD를 하려다보니 github actions에서 이벤트가 발생했을 때 Job을 수행할 서버의 공인 IP 주소가 필요했다.
하지만, 가상머신은 따지면 내부망에서의 IP만 존재하며 포트포워딩을 하기엔 추가해야할 설정이 너무 많았다.
그러던 중 Ngrok이라는 것을 발견했다.
그럼 Ngrok이 뭘까?
Ngrok은 로컬 개발 환경을 인터넷을 통해 접근할 수 있도록 해주는 도구이다. Ngrok 사이트에서 계정을 만든 뒤 Authtoken을 활용하면
EC2 대신에 로컬 개발 환경에서 다양한 작업이 가능해진다.
Ngrok을 사용하고 싶다면 아래 사이트로 접속하여 회원가입을 진행한다.
이때, OS를 설정하여 Ngrok을 설치한다.
로그인 한 뒤 Your Authtoken을 클릭하면 사용가능한 Authtoken을 알 수 있다.
그 후, 가상머신 CLI에 위의 Command Line에 있는 내용을 복사하여 환경설정을 해준다.
그런 뒤 ngrok http 8080등으로 ngrok을 실행시키면 CLI 창이 바뀌며 외부에서 접속할 수 있는 도메인이 화면에 표시된다.
하지만, github actions에서 접속하려면 ssh로 접속하여야 한다.
ssh는 TCP 프로토콜을 사용하므로 ngrok tcp 22를 사용하면 된다.
ngrok 일반 계정의 경우 하나의 포트로만 열 수 있다.
필자의 경우 공유기에 VPN 설정을 하였기 때문에 VPN으로 와이파이에 내부망으로 접속가능하다.
즉, ngrok이 하나의 포트만 지원하더래도 내부망으로 8080포트 등에 접속할 수 있기 때문에 CI/CD 테스트가 가능하다.
(공유기 VPN 설정 및 접속과 관련한 내용은 생략한다.)
이렇게 해서 EC2 대신 로컬 가상머신으로 CI/CD를 하기 위한 서버 설정을 완료했다.
2. Github Actions 스크립트 파일 작성
Travis CI, Jenkins 등 다양한 CI/CD 도구가 있지만 Github에 공식적으로 내장된 기능이 있는 Github Actions를 사용했다.
우선, SpringBoot 루트 디렉토리(폴더) 하위에 .github/workflows 순서로 디렉토리를 만든다. 그 후, YAML(yml) 파일을 작성한다.
github-actions.yml
# github repository actions 페이지에 나타날 이름
name: CI/CD using github actions & docker
# event trigger
# main이나 develop 브랜치에 push가 되었을 때 실행
on:
push:
branches: ["main", "develop"]
permissions:
contents: read
jobs:
CI-CD:
runs-on: ubuntu-latest
steps:
# JDK setting - github actions에서 사용할 JDK 설정 (프로젝트나 AWS의 java 버전과 달라도 무방)
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'corretto'
# gradle caching - 빌드 시간 향상
- name: Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
# yml 파일 생성 - application.yml
- name: make application.yml
if: |
contains(github.ref, 'main') ||
contains(github.ref, 'develop')
run: |
cd ./src/main/resources # resources 폴더로 이동
touch application.yml
echo application.yml
echo "${{ secrets.YML }}" > application.yml # github actions에서 설정한 값을 application.yml 파일에 쓰기
shell: bash
# gradle build
- name: Build with Gradle
run: ./gradlew build -x test
# docker build & push to production
- name: Docker build & push to prod
if: contains(github.ref, 'main')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/docker-test-prod .
docker push ${{ secrets.DOCKER_USERNAME }}/docker-test-prod
# docker build & push to develop
- name: Docker build & push to dev
if: contains(github.ref, 'develop')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/docker-test-dev2 .
docker push ${{ secrets.DOCKER_USERNAME }}/docker-test-dev2
- name: send docker-compose.yml to Server
uses: appleboy/scp-action@master
with:
username: ${{ secrets.USERNAME }}
host: ${{ secrets.HOST_PROD }} # EC2 퍼블릭 IPv4 DNS
key: ${{ secrets.PRIVATE_KEY }}
port: ${{ secrets.PORT }}
source: "docker-compose.yml"
target: "/home/${{ secrets.USERNAME }}"
## deploy to production
- name: Deploy to prod
uses: appleboy/ssh-action@master
id: deploy-prod
if: contains(github.ref, 'main')
with:
host: ${{ secrets.HOST_PROD }} # EC2 퍼블릭 IPv4 DNS
username: ${{ secrets.USERNAME }}
key: ${{ secrets.PRIVATE_KEY }}
envs: GITHUB_SHA
script: |
docker ps
docker pull ${{ secrets.DOCKER_USERNAME }}/docker-test-prod
docker-compose pull mariadb
docker-compose up -d mariadb
docker-compose pull redis
docker-compose up -d redis
docker-compose pull prod
docker-compose up -d prod
docker image prune -f
## deploy to develop
- name: Develop to dev
uses: appleboy/ssh-action@master
id: deploy-dev
if: contains(github.ref, 'develop')
with:
host: ${{ secrets.HOST_DEV }} # EC2 퍼블릭 IPv4 DNS
username: ${{ secrets.USERNAME }} # ubuntu
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
key: ${{ secrets.PRIVATE_KEY }}
envs: GITHUB_SHA
script: |
docker ps -f name=docker-test-dev2
if [ $? -eq 0 ]; then
docker rm -f docker-test-dev2
fi
docker pull ${{ secrets.DOCKER_USERNAME }}/docker-test-dev2
docker-compose pull redis
docker-compose up -d redis
docker run -d -p 8081:8080 --name docker-test-dev2 ${{ secrets.DOCKER_USERNAME }}/docker-test-dev2
docker image prune -f
각 코드를 차근차근 알아보자.
# github repository actions 페이지에 나타날 이름
name: CI/CD using github actions & docker
# event trigger
# main이나 develop 브랜치에 push가 되었을 때 실행
on:
push:
branches: ["main", "develop"]
permissions:
contents: read
- name: 워크플로우의 이름을 지정한다. github repository actions에 나타날 이름이다.
- on: push: branches: 워크플로우가 언제 실행될지를 지정할 때 사용한다.
- 여기선, [main, develop] 브랜치에 push가 일어날 때 워크플로우가 실행된다.
- permission: 권한을 부여하는 것으로 컨텐츠에 대한 읽기를 할 수 있다.
jobs:
CI-CD:
runs-on: ubuntu-latest
steps:
# JDK setting - github actions에서 사용할 JDK 설정 (프로젝트나 AWS의 java 버전과 달라도 무방)
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'corretto'
# gradle caching - 빌드 시간 향상
- name: Gradle Caching
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- jobs: 워크플로우에서 수행할 작업들이 모여있다.
- CI-CD: 작업의 전체 이름.
- runs-on: ubuntu-latest: 이 작업이 가장 최신 버전의 Ubuntu 환경에서 실행됨을 의미.
- steps: 워크플로우에서 실행할 각 단계를 나열한다.
- users: actions/checkout@v3:
- Github Actions 입장에서 깃허브의 코드 저장소에 올려둔 코드를 CI 서버로 내려받은 후 특정 브랜치로 전환.
- 즉, 작성한 깃허브를 저장소에 내려받는다고 생각하면 된다.
- name: 단계의 이름 지정
- uses: actions/setup-java@v4: java를 설치하는 것이다. java 17의 경우 v3로 하면 오류가 발생한다.
- with: 여러 옵션을 부여할 수 있다. checkout@v3에도 with 옵션을 사용하여 특정 브랜치 이동이 가능하다.
- uses: actions/cache@v3: Gradle을 캐싱해주는 코드이다. 없어도 상관은 없다만 적용했을 때 빌드 시간 단축이 가능하다.
# yml 파일 생성 - application.yml
- name: make application.yml
if: |
contains(github.ref, 'main') ||
contains(github.ref, 'develop')
run: |
cd ./src/main/resources # resources 폴더로 이동
touch application.yml
echo application.yml
echo "${{ secrets.YML }}" > application.yml # github actions에서 설정한 값을 application.yml 파일에 쓰기
shell: bash
# gradle build
- name: Build with Gradle
run: ./gradlew build -x test
이렇게 작성하였지만 이건 여러 yml 파일에 적용할 수 없는 코드이다.
아래와 같이 사용하도록 하자.
# yml 파일 생성 - application.yml
- name: make application-prod.yml
if: github.ref == 'refs/heads/main'
run: |
cd ./src/main/resources # resources 폴더로 이동
touch application.yml
echo application.yml
echo "${{ secrets.PROD_YML }}" > application.yml # github actions에서 설정한 값을 application.yml 파일에 쓰기
shell: bash
- name: make application-dev.yml
if: github.ref != 'refs/heads/main'
run: |
cd ./src/main/resources # resources 폴더로 이동
touch application.yml
echo application.yml
echo "${{ secrets.DEV_YML }}" > application.yml # github actions에서 설정한 값을 application.yml 파일에 쓰기
shell: bash
# gradle build
- name: Build with Gradle
run: ./gradlew build -x test
- name: application.yml를 push된 branch에 맞게 넣어주는 코드이다.
- ${{ 환경변수 }}: 계속 사용되는 환경변수들의 형식으로 Github Repository Settings -> Secrets and variables에서 설정 가능하다.
- run: 각 run은 독립적으로 수행되며 ./gradlew build -x test는 테스트 없이 gradlew build를 진행한다는 것이다.
# docker build & push to production
- name: Docker build & push to prod
if: contains(github.ref, 'main')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/docker-test-prod .
docker push ${{ secrets.DOCKER_USERNAME }}/docker-test-prod
# docker build & push to develop
- name: Docker build & push to dev
if: contains(github.ref, 'develop')
run: |
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker build -f Dockerfile -t ${{ secrets.DOCKER_USERNAME }}/docker-test-dev2 .
docker push ${{ secrets.DOCKER_USERNAME }}/docker-test-dev2
- Github actions Runner 환경에서 도커 로그인 -> Dockerfile에 의거하여 도커 이미지 생성 -> Docker hub에 푸시하는 과정이다.
- Dockerfile은 다음과 같다.
# open jdk 17 버전의 환경을 구성
FROM amd64/amazoncorretto:17
# build가 되는 시점에 JAR_FILE이라는 변수 명에 build/libs/*.jar 선언
# build/libs - gradle로 빌드했을 때 jar 파일이 생성되는 경로
ARG JAR_FILE=build/libs/*.jar
# JAR_FILE을 app.jar로 복사
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-jar", "/app.jar"]
- FROM: 자바 17 버전의 환경을 담고 있는 이미지를 기반으로 한다.
- ARG: build가 되는 시점에 JAR_FILE 변수에 gradle로 빌드했을 때 jar 파일이 생성되는 경로 할당
- COPY: 변수 JAR_FILE을 app.jar로 변경하여 복사
- ENTRYPOINT: 이미지 빌드시 수행될 작업
- name: send docker-compose.yml to Server
uses: appleboy/scp-action@master
with:
username: ${{ secrets.USERNAME }}
host: ${{ secrets.HOST_PROD }} # EC2 퍼블릭 IPv4 DNS
key: ${{ secrets.PRIVATE_KEY }}
port: ${{ secrets.PORT }}
source: "docker-compose.yml"
target: "/home/${{ secrets.USERNAME }}"
...
## deploy to develop
- name: Develop to dev
uses: appleboy/ssh-action@master
id: deploy-dev
if: contains(github.ref, 'develop')
with:
host: ${{ secrets.HOST_DEV }} # EC2 퍼블릭 IPv4 DNS
username: ${{ secrets.USERNAME }} # ubuntu
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
key: ${{ secrets.PRIVATE_KEY }}
envs: GITHUB_SHA
script: |
docker ps -f name={repository}
if [ $? -eq 0 ]; then
docker rm -f {repository}
fi
docker pull ${{ secrets.DOCKER_USERNAME }}/{repository}
docker-compose pull redis
docker-compose up -d redis
docker run -d -p 8081:8080 --name docker-test-dev2 ${{ secrets.DOCKER_USERNAME }}/docker-test-dev2
docker image prune -f
- uses: appleboy@scp-action@master: 파일을 압축해서 전송하는 것을 도와주는 깃허브 오픈소스이다.
- scp-action도 22번 포트로 작동한다. 즉, Ngrok으로 하나의 세션만 열 수 있다고 하여도 사용이 가능하다.
- with: 가상머신 접속 ID/PASSWORD, Ngrok으로 부터 얻은 도메인, 포트번호 등을 환경변수로 넣어준다.
- sources: 가상머신에서 활용할 docker-compose를 가상머신으로 전송해준다.
- target: 전송할 파일의 목표 경로를 입력한다.
- name: Deploy to dev: 아까 도커 허브로 올린 이미지를 Docker에서 컨테이너화하여 실행하는 단계이다.
- uses: appleboy@ssh-action@master: ssh에 접속하기 위한 깃허브 오픈소스이다.
- envs: GITHUB_SHA: 깃허브 커밋 값까지 확인할 때 사용한다.
- script: {repository}라는 이름으로 이미지를 올렸다.
- 따라서, docker에 해당 프로세스가 실행중이라면 종료한다.
- 그 후, docker-compose에서 필요한 컨테이너를 구동시킨다.
- docker-compose.yml은 다음과 같다.
ssh-action으로 접속시 docker를 sudo 명령어와 함께 사용시 문제 발생
만약, sudo로 도커 명령어를 사용하면 비밀번호를 입력해주어야 한다. 하지만, 그것은 CLI 상황이 아니라면 불가능하다.
(DEBIAN_FRONTEND=noninteractive를 사용하면 넘어갈 수 있긴 하다. 하지만, 영구적이지 않음.)
따라서, 아래 명령어들을 사용해서 해결했다.
sudo usermode -aG docker {username}
sudo -su {username}
docker-compose를 아래와 같이 설치했을 때 문제발생
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
원래 regacy인 docker compose 명령어는 docker-compose와 다르다.(파이썬)
그런데, docker-compose version을 하였을 때 docker-compose의 버전이 아닌 docker compose 버전이 자꾸 나왔다.
따라서, 아래를 수행하여 수정했다.
vim ~/.bashrc
# bashrc 가장 아래에 추가 후 !wq
export PATH=/usr/local/bin:$PATH
source ~/.bashrc
version: '3.8'
services:
redis:
container_name: redis
image: redis:alpine
restart: always
ports:
- 6379:6379
networks:
- my_network
prod:
container_name: prod
image: {username}/{repository}
expose:
- 8080
ports:
- 8081:8080
environment:
- TZ=Asia/Seoul
depends_on:
- redis
networks:
- my_network
dev:
container_name: dev
image: {username}/{repository}
expose:
- 8080
ports:
- 8082:8080
environment:
TZ: Asia/Seoul
depends_on:
- redis
networks:
- my_network
networks:
my_network:
네트워크를 등록하여 같이 묶어줬다.
Docker, Docker-Compose, Dockerfile 문법은 다른 게시물에서 다룰 예정.
{{ secrets.PRIVATE_KEY }}가 계속 사용되어서 설명하자면 가상 머신에 접속하기 위한 개인키라고 생각하면 된다.
관련 내용은 아래를 참고하기 바란다.
https://pyhub.kr/recipe/pdEgV6gekLMnG/
자, 여기까지는 큰 문제가 없었다. 그러나, 가상머신에서 작업을 하려니 지금부터 오류의 시작이었다.
원흉의 원인을 따지면 아래 오류이다.(결론적으로 나의 실수지만 억울하다..)
org.h2.jdbc.JdbcSQLNonTransientConnectionException: Connection is broken: "java.net.ConnectException: Connection refused: localhost" [90067-214]
트러블 슈팅 과정
1. 초기에는 가상머신에 Docker Mariadb 이미지를 다운받아 설정.
- docker-compose로 mariadb 수행.(docker-compse.yml 내용에 mariadb가 들어있었음)
version: '3.8'
services:
mariadb:
container_name: mariadb
image: mariadb:10
restart: always
ports:
- 3306:3306
volumes:
- "./mariadb/conf.d:/etc/mysql/conf.d"
- "./mariadb/data:/var/lib/mysql"
environment:
MARIADB_DATABASE: springtoy
- 이런 방식으로 설정했었다.
- 하지만, 아래 오류 여전히 발생
org.h2.jdbc.JdbcSQLNonTransientConnectionException: Connection is broken: "java.net.ConnectException: Connection refused: localhost" [90067-214]
왜 대체 h2 오류가 계속 뜨는가 싶었다. mariadb도 아닌 h2 오류가..
2. Docker Mysql 이미지를 다운받아 설정.
- 마찬가지 h2 오류 발생.
진짜 미치는 줄 알았다. 그러다가 천천히 코드를 다시 살펴보았는데..
아! local에서 application.yml 수정만 하고 github actions 환경변수에 yml를 안넣어줬구나!
이때까지만 해도 이것만 수정하는 줄 알았는데 실패!
대신, 자바 컴파일 때 아래 오류 발생.
Task :compileJava FAILED
Could not resolve all files for configuration ':compileClasspath'.
Could not find mysql:mysql-connector-java:.
Required by:
project :
알고보니까 스프링이 업데이트 되어가면서 mysql jdbc 의존성 설정이 아래와 같이 바꼈단다.
runtimeOnly 'mysql:mysql-connector-java' <- 이전
runtimeOnly 'com.mysql:mysql-connector-j' <- 현재
3. 두 번째, 가상머신 환경에 mysql-server를 그냥 설치 후 해당 가상머신 주소 application.yml 설정.
- 이랬는데 이번엔 또 db connection 시간초과 발생.
진짜 미치는 줄 알았다 ㅋㅋㅋㅋ... 가상머신에 방화벽을 따로 걸어놓지도 않았고 포트도 다 열어두었는데 왜 안되나 싶어서
ping 명령어로 ping을 보내봤는데 와.. 핑이 안간다. 이럼 뭐지? 그냥 접어야 하나 싶었다.
그러다가 방화벽을 재시작하고 켜본 뒤 ping을 해보니까 된다..? DataGrip으로도 커넥션이 된다.
당연히 방화벽을 안켜놓으니 다막히지 근데 방화벽이 맨처음에 켜져있을 때도 안되는데 재시작하니까 된다.
이전에 3306도 이미 열어두었는데 귀신이 곡할 노릇이다.
4. 대망의 Github actions 재실행.
- 된다.. 트러블 슈팅 내용을 다담지 못했지만.. 거의 모든 경우의 수를 다 해봐서 이것마저 안되면 가망이 없었다.
진짜.. 돼서 다행이다. 나의 몇 시간의 노력....
대성공!!!!!!!!!!!!!!
마무리하며
왜 AWS를 쓰는지 알 것 같다. AWS는 인바운드, 아웃바운드, VPC, 보안 그룹 등 설정이 잘 되어있어서 몰랐는데
역시 직접 하려니 많은 애를 먹었고 또 가상 머신을 꾸역꾸역 Ngrok으로 열어서 하려하니 문제가 많았다.
그래도 이것저것 찾아보면서 좀 많이 알게 된 것 같다. 그리고 후에 docker, docker-compose, dockerfile 문법은 따로 정리하려 한다.
그리고 무중단 배포, Jenkins + Nginx, Jenkin + Docker + Nginx도 해보려 한다.
내일은 S3 이미지 배포에 대한 포스팅을 진행하려 한다.
https://ziszini.tistory.com/110
'프로젝트 > 토이 프로젝트' 카테고리의 다른 글
Spring + Redis + S3 + 이메일 인증 + Docker + CI/CD - 8 (0) | 2024.02.28 |
---|---|
Spring + Redis + S3 + 이메일 인증 + Docker + CI/CD - 7 (1) | 2024.02.26 |
Spring + Redis + S3 + 이메일 인증 + Docker + CI/CD - 5 (0) | 2024.02.22 |
Spring + Redis + S3 + 이메일 인증 + Docker + CI/CD - 4 (0) | 2024.02.21 |
Spring + Redis + S3 + 이메일 인증 + Docker + CI/CD - 3 (0) | 2024.02.19 |