Github Actions(CI/CD 구축) - Spring Boot로 만든 개인 프로젝트의 CI/CD
Spring Boot로 만든 개인 프로젝트에서의 CI/CD의 구축
이번에 해보고자 하는 CI/CD의 흐름은 먼저 로컬에서 코드를 작성한 후에 그 내용을 Github에 Push하게 된다.
그러면 Github Actions가 이벤트를 확인해서 workflow를 실행해서 러너의 내부에서 AWS의 EC2로 원격 접속을 한 후에 Git을 통해서 Pull을 해서 해당 소스를 AWS의 EC2로 통합한다.
이러면 전체 프로그램 자체를 다 교체하는 방식이 아니라 지금 Push한 내용에 대해서만 교체하기 때문에 속도와 비용 면에서 이점을 가지게 된다.
그러나 빌드작업 자체는 EC2자체에서 직접 진행하는데 빌드가 컴퓨터의 성능을 많이 먹는 작업이기 때문에 운영하고 있는 서버의 성능에 영향을 줄 수 있다.
또한 Github 계정의 정보가 EC2에 저장되어야 Pull을 할 수 있기 때문에 개인 프로젝트의 경우에만 사용이 가능하거나 신뢰가능한 사람들이랑 만 사용이 가능하다는 단점이 존재한다.
Spring Boot 프로젝트의 생성 및 실행
위 홈페이지로 들어가서 아래와 같이 Spring Web과 Spring Boot DevTools만 디펜던시로 받아 준 다음에 Generate해주자
설치된 압축파일을 풀어 IDE를 통해서 열어주자.
이제 간단하게 컨트롤러를 하나 만들어주고
RestController로 어노테이션을 추가해주고 루트로의 Get요청에 대해서 문자열을 반환하는 메서드를 하나 추가해주도록 하자
이러고 일단 프로그램을 실행시켜서 정상적으로 구동 되는지를 확인해보자.
DemoApplication에 마우스 우클릭 > Run 'DemoApplicaton.main()'을 선택해주던가
보다 싶이 DemoApplication을 마우스로 클릭 한 다음에 Ctrl + Shift + F10으로 프로그램을 실행해주고
이렇게 8080포트에서 정상적으로 실행했음을 확인했다면 URL에 localhost:8080으로 접속해보자
그러면 바로 /(루트) 경로로 들어가면서 위에서 return 했던 문자열을 확인할 수 있으면 서버가 정상적으로 실행되고 구동되는 것이다.
Github Repository 생성
이제 Github로 가서 우측 New를 눌러서 Repository를 하나 생성해주도록 하자.
생성할때는 따로 설정할건 없고 이름을 설정해주고 공개 범위를 private으로 설정하는 것만 하고 Repository를 만들어주자.
그리고 이제 로컬 IDE로 다시 돌아와서 먼저 git init 명령어를 통해서 로컬 프로젝트 공간을 Git 레포로 설정해주고
그리고 최초의 상태를 git에 add 및 commit 해서 저장해주자.
그리고 이제 Github의 repo와 연결해주는 명령어인 remote를 사용해서 원격 주소를 등록해주자
git remote add origin [URL]
주소는 Github의 Repo의 메인 화면에 존재한다
복사 버튼을 눌러서 복사한 다음에 실행해주고
이제 push를 통해서 소스를 Github에 올려주자.
그리고 Github에 정상적으로 올라갔는지 확인해주자.
이제 EC2를 설정해주자.
EC2 t3.micro의 swap
프리티어인 micro는 메모리가 1GB인데 여기에 springboot를 올리면 메모리 부족으로 에러가 난다고 한다.
그럴땐 Swap이란것을 사용해줘야지만 서버를 띄울 수 있다고 한다.
1) Swap이란
swap은 RAM이 모자랄 때 임시로 디스크(EBS)를 메모리처럼 쓰는 영역을 의미한다.
SpringBoot를 쓰면 JVM + 앱 메모리만으로 메모리 부족으로 꺼질 수 있기에 힙 공간을 작게 설정해서 JVM이 너무 메모리를 많이 먹어서 메모리부족이 발생하지 않도록 설정하고 스왑파일(하드디스크에 일반 파일을 생성해서 스왑 공간을 구현한다, 이는 하드디스크 내부에서 가짜 RAM의 역할을 해줄 그릇이라고 보면 된다.)을 생성해 잠깐 모자랄 때 임시 메모리처럼 잠깐 쓰는 개념이다.
이건 결국에 너무 느리기 때문에 지금 처럼 테스트나 교육을 위해서만 사용하는게 좋지 않을까 싶다.
2) Swap의 설정
먼저 EC2에 연결해서 top 명령어를 통해서 현재 EC2가 Swap을 사용하는지 확인해보자
보면 Swap을 사용하지 않고 있는걸 볼 수 있다.
이제 Swap 메모리를 할당해보자.
sudo su를 통해서 root로 이동하고
아래 명령어를 사용해주자
sudo dd if=/dev/zero of=/swapfile bs=128M count=16
이 명령어는
- dd : 블록 단위로 데이터를 복사하는 유틸리티
- if=/dev/zero : if는 input file의 약자이고 /dev/zero는 무한하게 0만 채워 넣는 가상의 장치이다. 이는 파일 하나를 0으로 가득 채워 초기화해서 생성하겠다 라는 의미로 만들어진 파일을 이용해서 어딘가에 값을 복사하겠다
- of=/swapfile : of는 output file의 약자이고 /swapfile이라는 이름의 파일을 하드디스크(EBS)에 생성해서 복사한 내용의 Destination으로 사용한다
- bs=128M : 한번에 복사할 데이터의 크기는 128 메가바이트(MB)이고
- count=16 : 16회 복사하겠다
정리해보자면 관리자 권한으로(sudo) 블록단위로 데이터를 복사하는 도구를 사용해서(dd) 0을 무한하게 출력하는 /dev아래의 zero라는 파일의 내용을 복사해서(if=/dev/zero) 128M씩 블록으로 묶어서(bs=128M) 16번을(count=16) /swapfile에 붙여 넣겠다(of=/swapfile).
이렇게 해서 0으로 가득 찬 2GB의 swapfile이 하드디스크(EBS)에 생성된다.
이제 이렇게 swapfile을 만들었고 이걸 root만 쓸 수 있도록 권한을 변경해주고(스왑 파일에는 메모리에서 밀려난 비밀번호, 토큰, 키 등등의 민감한 데이터가 담길 수 있으니 root만 접근할 수 있도록 설정해둬야 한다)
sudo chmod 600 /swapfile
이제 mkswap 명령어를 사용해서 swapfile을 실제 스왑 공간으로 설정하도록 해주자
(mkswap 명령어는 일반 파일을 운영체제로 하여금 실제 스왑 공간으로 사용할 수 있게 초기화하고 포멧하는 명령어이다.)
sudo mkswap /swapfile
이제 swapon 명령어를 사용해서 위에서 만든 스왑공간을 시스템에 적용해주자.
(mkswap은 단순하게 공간을 준비하는 과정이고 이걸 실제 CPU와 OS의 메모리 관리 시스템에 연결해주는 것이 swapon이다.)
sudo swapon /swapfile
이제 sudo swapon -s를 사용해서 잘 만들어졌는지 확인해보자.
잘 만들어 졌으면 이 설정이 재부팅 시점에도 계속 활성화될 수 있도록 /etc/fstab파일에 들어가서
가장 아래에 아래 명령어를 추가해주자.
/swapfile none swap sw 0 0
이제 top을 다시 한번 쳐보면
swap공간이 잡혀 있는걸 볼 수 있다.
스프링부트 환경 및 배포 테스트
스프링 부트 프로젝트를 돌리기 위해서 먼저 EC2환경에 JDK를 설치해줘야 한다
그걸 위해서 먼저 apt를 업데이트 해주고
그리고 jdk 17 버전을 설치해주자.
sudo apt install openjdk-17-jdk -y
그리고 jdk가 잘 설치되었는지를 확인하기 위해서 버전 확인 명령어를 한번 확인해주자.
java -version
이제 깃을 통해서 우리가 올린 스프링 부트를 내려받아주자.
git clone "깃허브클론URL"
=> username을 치라고 하면 계정의 닉네임을 치면 됨 나의 경우는 WoodWoodE
=> password를 치라고 하면 Personal Access Token을 넣어주면 됨
이제 해당 디렉터리로 들어가서 그래들 명령어를 통해서 빌드를 해보자.
./gradlew clean build
- ./gradlew : 그래들의 빌드도구를 사용해서
- clean : 기존에 빌드된 build 디렉터리를 지우고
- build : 빌드한다
혹시 gradlew가 실행에 아무것도 권한이 없다면

그러면 추가적으로
chmod +x ./gradlew
해당 명령어를 통해서 실행 권한을 줘야 하고 이렇게 준다면 ls 명령어로

이렇게 보이던 리스트가

이런식으로 실행 가능한 상태로 변경된다(초록색)
이렇게 처리를 해줘야만 한다.
그러면 이렇게 빌드가 된다.
이렇게 빌드를 하면
이렇게 build 디렉터리가 생성되고 내부를 보면
libs라는 디렉터리가 있는데 이쪽 으로 들어가보면
이렇게 빌드된 파일 2개가 떨어지게 되어 있다.
이 두 파일중에 plain이 안붙어 있는 jar파일을 실행시켜주면 된다.
실행시켜주는 방법은 아래와 같다.
nohub java -jar [jar이름]&
여기서 nohub은 지금 터미널이 끊어져도 실행하는 프로세스가 꺼지지 않도록 해주게 해주는 명령어로 java를 이용해서 jar을 실행해라 라는 의미이고 끝에 &이 붙는건 백그라운드에서 실행해라 라는 의미이다.
이 상태에서 엔터를 눌러주고 이제 배포가 잘됐는지 확인하기 위해서 아래 명령어를 통해서 포트를 점유 중인지 확인해보자.
sudo lsof -i:8080
자바가 이렇게 리슨중인걸 알 수 있다.
그리고 웹에서 host:port로 접근했을때
이렇게 웹에서 확인 가능한것을 볼 수 있다.
만약에 8080포트로 접근이 되지 않는다면 인바운드 규칙에서 사용자 TCP로 8080포트를 열어줘야만 한다.

스프링 부트 프로젝트의 CI/CD
우리는 이렇게 배포된 프로젝트가 로컬에서 수정이 진행 된 이후에 자동으로 반영이 되고 배포가 되었으면 하는 것이다.
이를 테스트 하기 위해서 로컬에서 수정을 한번 해보고
commit& push 해주자
그리고 이렇게 push가 되었다면 EC2에서 git pull로 새로운 코드를 받아오자.
git pull origin master
이렇게 pull 되었다면 지금 현재 실행되고 있는 프로세스의 경우는 우리의 수정사항이 적용된 상태가 아니기 때문에 8080포트로 실행되는 프로세스를 종료시켜주는 아래 명령어를 사용해서 프로세스를 죽이고
sudo fuser -k -n tcp 8080
- fuser : 어떤 프로세스가 특정 파일/소켓(포트)를 사용하는지 알려주는 도구
- -n tcp : 네임스페이스를 tcp로 지정하여 “TCP 소켓”을 대상으로 보겠다는 뜻
- 8080 : 8080포트를
- -k : 찾은 PID에 종료 신호를 보낸다. 기본 신호는 SIGKILL(9) 이다.
그리고 이제 소스가 바뀐거니까 이걸 기반으로 다시 빌드를 진행하고
다시 libs으로 들어가서 실행해주자.
이 과정을 이제 자동화 하려는 것이다.
1) git 설정 자동화
먼저 자동화 이전에 git에 pull 할때 계속 계정과 토큰을 입력해야 하는 부분부터 스킵할 수 있도록 해보면
git config --global credential.helper store
- git : git의 cli 실행명령어
- config : git 설정을 읽고 쓰는 서브 커멘드(설정 범위에 따라서 .git/config와 ~/.gitconfig, 시스템 등이 있다)
- --global : 현재 사용자 계정 전역 설정으로 기록하라는 의미(전역이기에 ~/.gitconfig에 저장)
- credential.helper : 자격 증명 헬퍼를 어떤 것으로 쓸지 지정하는 설명 키
(git이 HTTPS로 원격 저장소에 접근할 때 사용자/비밀번호(또는 토큰)를 어떻게 보관 및 재사용할지를 결정)
- store : 단순한 평문 텍스트 파일에 영구 저장하는 단순 헬퍼로 한번 인증 하면 ~/.git-credentials에
값을 저장하여 같은 호스트/url로 푸시, 풀 할때 자동으로 재사용한다.
이건 git의 인증정보를 평문파일에 저장해서 같은 호스트에서 push/pull을 할 때 아이티/토큰을 다시 묻지 않도록 하는 설정으로 평문 저장 활성화하는 명령어이다.
이렇게 활성화되었다면 pull을 한번해보면
다음 pull 할때는
안물어보고 바로 pull이 된다.
이 설정은 사용자 홈으로가서
이 파일 내부에 작성되어 있다.
이렇게 내부적으로 파일에 작성되어 있길래 공동작업을 할때는 사용될 수 없는 방식이니 개인 프로젝트에서만 활용하도록 하자.
이제 github action yaml파일을 생성해서 자동화 작업을 진행해보자.
2) workflow 생성
workflow는 .github 디렉터리 내부의 workflows라는 디렉터리 내부에 존재해야 인식을 한다
그래서 디렉터리를 구조대로 만들어주고
내부에 파일을 하나 만들어주자.
이제 내부에 작성을 해줄것인데 가장 먼저 workflow의 이름을 지정하고
이 워크플로우를 활성화 시킬 이벤트는 push 이벤트로 지정한다.
이때 master 브랜치에서만 이 push 이벤트가 발생했을때 workflow를 실행해주도록 설정할 수 도 있다.
이제 push가 되었을때 실행될 작업을 지정해야하는데 먼저 jobs로 작업 영역을 만들어주고
실행할 jobs의 이름을 지정해주자.
그리고 해당 워크플로우의 잡을 러너가 어떤 환경에서 실행할지를 지정해주고
이제 작업의 step을 지정해주기 위해 steps로 영열을 열어주고 첫번째 step으로 EC2에 SSH로 연결을 하기 위한 step을 만들어 주기 위해서 먼저 name을 지정해준다.
이제 ssh로 연결하기 위헤서 Github Action중 하나를 쓸건데 이 라이브러리는 깃허브 액션의 마켓 플레이스에서 확인이 가능하다.
https://github.com/marketplace
이 홈페이지에서 ssh로 검색을 해주면
아래와 같이 ssh로 작업할 수 있게 만들어둔 수많은 라이브러리들이 존재한다.
그 중에서 star가 가장 많은 action을 찾아서
들어가보면 https://github.com/marketplace/actions/ssh-remote-commands
SSH Remote Commands - GitHub Marketplace
Executing remote ssh commands
github.com
어떻게 사용해야하는지도 확인이 가능하다.
보면 uses를 통해서 appleboy/ssh-action@v1을 사용하면 사용이 가능한 것으로 보인다
거기에 추가적으로 with로 ssh의 정보를 확인할 수 있는 것으로 보인다.
여기서 SSH의 정보는 우리의 github의 repo에서 Settings를 들어간 후에 Secrets and variables로 들어가서 actions를 선택해주자
아래에 New repository secret을 눌러서
아래와 같이 secret을 추가하는데
각각 HOST, KEY, USERNAME, PORT을 추가해주자.
해당 정보들은 각각 연결할때 퍼블릭 IPv4주소는 HOST, 사용자 이름을 USERNAME으로 만들어주고
key는 이전에 우리가 키페어를 만들었던 걸 아래와 같이 읽어서 나오는 문자열을 넣어주자
이렇게 만들어진 secrets은 workflows 내부에서 secrets라는 객체의 멤버로써 사용이 가능한데 이는 아래와 같이 ${{}}의 내부에 작성해주면 출력이 되어 사용이 가능하다.
이렇게 작성하면 이제 우리의 EC2의 SSH로 workflow의 러너가 붙게 된다.
이제 이 러너가 작업할 내용을 script의 내용에 넣어주면 되는데 우리가 위에서 했던 작업의 순서를 보면
- 프로젝트의 위치로 이동
- git에서 새로운 소스를 pull
- gradle을 통해서 clean하면서 build
- 기존에 실행중이던 서버를 종료
- build 디렉터리의 libs 디렉터리로 이동
- nohup을 사용해서 빌드된 jar파일을 실행
의 과정을 진행했었다.
이 과정을 이제 스크립트로 한줄 한줄 추가해주면 된다.
1. 프로젝트의 위치로의 이동
러너가 이동할 수 있도록 cd 명령을 첫번째로 추가해준다.
2. git에서 새로운 소스를 pull
3. gradle을 통해서 clean하면서 build
4. 기존에 실행중이던 서버를 종료
이 때 주의해야할 점은 위처럼 kill할때 프로세스가 떠있지 않다면 그냥 에러를 벹으면서 workflow가 비정상 종료를 하기 때문에 만약 없어서 에러가 나오면 ||를 통해서 만약 앞에 명령어가 에러라면 || 뒤를 실행하고 그걸 true로 설정해서 무조건 성공할 수 있게 끔 설정해 에어를 방지해주자.
5. build 디렉터리의 libs 디렉터리로 이동
6. nohup을 사용해서 빌드된 jar파일을 실행
nohub을 사용해서 빌드된 jar을 실행할건데 파일이 생성될때 마다 명칭이 조금씩 달라질 수 있는데
위 처럼 -SNAPSHOT이 붙은걸로 파일을 실행시킬라면 와일드카드 *과 함께 SNAPSHOT.jar을 붙여주면 된다.
근데 여기서 원래는 &를 쓰고 끝이 였는데 이렇게 실행했을때의 로그들을 파일 하나에 저장할 수 있도록
이렇게 추가해주자.
이건 nohup java -jar ./*SNAPSHOT.jar를 실행했을때의 표준출력을(Console의)(>) output.log에 넣고 표준에러출력을(2>, 여기서 2는 FD2, 즉 표준에러를 의미함) 표준출력(&1, 여기서 &는 FD를 의미하는 것으로 FD 1은 표준 입출력을 가리킴)이 가리키는 곳으로 복제해라라는 의미로 표준 에러와 표준 출력을 다 output.log에 넣어줘라 라는 의미이다.
(이걸 꼭 추가해줘야 하는 이유는 이 라이브러리의 이슈로 인해서라고 한다. 위 처럼 표준에러, 표준 출력을 파일로 보내지 않으면 에러를 발생시킨다는듯)
이제 이 내용을 commit 하고 push 해주고
깃허브 액션에 보면
이렇게 배포가 되는걸 볼 수 있다.
눌러보면 정장적으로 접근해서 빌드까지 되는걸 볼 수 있다.
그러면
이렇게 내용이 변경된걸 볼 수 있다.