Memories in SeoK

기억하고 싶은 것들, 기억해야 하는 것들

개발/자바 Java

[Ubuntu] 따라하기 + 배포 연습 (Spring Boot)

Seo K 2024. 6. 16. 03:42

[ 이동욱 저 - 스프링 부트와 AWS로 혼자 구현하는 웹 서비스 ] 책의 예제를 최신 프레임워크와 라이브러리로 대체하여 따라 하기 중 만들기에 (프로젝트 작성에) 집중한 이전 글에 이어서 배포 관련 부분을 해당 글에 작성합니다

 

따라가며 만들기 + 마이그레이션 연습 (Spring Boot, AWS)

기억을 되새길 겸, 블로그 콘텐츠도 얻을 겸 몇 년 전에 작성된 책에 (이동욱 저 - 스프링 부트와 AWS로 혼자 구현하는 웹 서비스) 있는 예제 프로젝트를 최신 프레임워크와 라이브러리로 대체해

mem-in-seok.tistory.com


 

AWS를 사용할 수 없어서 별도 리눅스 서버를 구해서 배포할 예정인데, 서버에 대해 모르는 것이 많기도 하고, 배포 연습한 서버를 열어둘 수도 없어서 대신 기본적인 내용과 서버 변경에 따라 달라진 내용 등을 글로 가능한 세세하게 남겨두려는 생각에 배포 부분을 나눠서 작성하기로 했다

궁금한 점, 요청 사항, 조언, 지적 등을 댓글에 남겨 주시면 감사하겠습니다

Virtual Machine (Proxmox)
--  Processor 1 core (x86-64-v2-AES)
--  Memory 2GiB
--  Disk 32GiB
Ubuntu 22.04.2-amd64 (Linux 6.x - 2.6 Kernel)
--  Git 2.34.1
--  OpenSSH 3.0.2 (8.9p1 Ubuntu-3ubuntu0.7)
Temurin 17 jre
rdate 1.10.1

Linux 명령어

* 계열 / 배포판 변경으로 명령어가 다른 경우

책 예제에서는 AWS EC2에 레드햇 계열의 Amazon Linux 1을 (이하 AL1, RHEL CentOS 기반) 설치했으나, 내가 빌린 서버에는 데비안 계열인 우분투가 설치되었다

(CentOS는 지원이 끊겼고, Rocky는 구하기 귀찮다는 이유로 레드햇 계열로 설치 바란다는 나의 요청은 가볍게 무시되었다)

계열부터 달라진 만큼 명령어가 많이 다르다
하지만 Amazon Linux를 선택한다 해도 AL1은 2023년 이후로 더 이상 사용할 수 없고, 최신 버전인 AL2023부터는 페도라 계열로 바뀌어서, 늦어도 AL2가 완전히 종료되는 2028년부터는 예시 그대로 사용할 수 없는 명령어가 생길 것이다

더보기
## Repostory (install / remove)
## AL1 (CentOS)
$ sudo yum install -y [package name]
$ sudo /usr/sbin/alternatives --config java
## Ubuntu
$ sudo apt-get install -y [package name]
## remove와 달리 purge는 설정 파일까지 삭제
$ sudo apt-get remove [package name]

## hostname 확인/변경
## AL1 (CentOS)
$ sudo vim /etc/sysconfig/network
## Ubuntu
$ sudo vim /etc/hostname

 

* 알아두면 좋은 우분투/리눅스 명령어

더보기

* 따옴표의 사용

리눅스에서 홑따옴표와 (') 쌍따옴표는 (") 그 쓰임새가 미묘하게 다르다
cf. 반달가면 [B-Side] :: [bash: ",'] 따옴표 선택/사용하기 (tistory.com)

## ssh를 이용할 경우 그 차이를 더 느낄 수 있다
## Windows Powershell 클라이언트에서 ssh를 통해 Ubuntu 서버로 접속하는 상황

## 쌍따옴표 사용 : 문자열 의미를 해석 > 명령어라면 클라이언트에서 실행, 변수라면 값 반환
PS E:\Java> ssh username@server "$(cat ~/app/java_path)/java --version"
cat : 'C:\Users\user\app\java_path' 경로는 존재하지 않으므로 찾을 수 없습니다.
위치 줄:1 문자:56
...
    ...
bash: line 1: /java: No such file or directory

PS E:\Java> ssh username@server "echo '$PATH'"

  ## Powershell에서 사용할 수 있는 "PATH" 변수 또는 그 값이 없는 것 같다

## 홑따옴표 사용 : 문자열 그대로 인식 > 문자열이 전달되어 명령어가 서버에서 실행됨
PS E:\Java> ssh username@server '$(cat ~/app/java_path)/java --version'
  ## 참고: SSH를 통해 접속할 경우 ~/.bashrc를 통한 설정이 안 되어 java 명령어 사용 시 전체 경로 지정 필요함
openjdk 17.0.11 2024-04-16
OpenJDK Runtime Environment Temurin-17.0.11+9 (build 17.0.11+9)
OpenJDK 64-Bit Server VM Temurin-17.0.11+9 (build 17.0.11+9, mixed mode, sharing)

PS E:\Java> ssh username@server 'echo "$PATH"'
/usr/local/ ... /bin
  ## 서버에 있는 "PATH" 변수의 값

 

* echo > file, echo >> file 차이

하나만 있으면 덮어쓰기, 두 개를 쓰면 추가하기

## file에 "Hi"라는 값이 있을 경우
$ echo ' world' >> file
$ cat file
Hi world

$ echo ' world' > file
$ cat file
 world

 

* 현재 위치 절대 경로 확인 :: pwd

Print Working Directory

 

* 디스크 용량 확인 :: df -Th, du -hs
* 메모리 용량 확인 :: free -h
* 리소스 모니터링 :: top

cf. [Linux/Ubuntu] 메모리 / 디스크 / 디렉토리 용량 확인

 

* 권한 변경 :: chmod +x [파일명], chmod 755 [파일명]

[ d 폴더, - 파일 ] [ rwx (4+2+1) ] [  ] [  ]

 

* 링크 생성 :: ln -s [대상] [링크 이름]

 

* readonly 파일의 수정 (:set 입력하면 현재 속성을 볼 수 있다)

(user 권한이 있을 때) :w!  >  sudo 권한으로 시도  >  root 권한으로 시도

 

* nohup과 &

둘 다 명령의 실행 상태를 유지시킨다는 공통점이 있으나 약간의 차이가 있다
nohup은 사용자가 로그아웃하더라도 (세션이 종료되더라도) 실행 상태를 유지하라는 명령어
파일명 끝의 &는 작업을 백그라운드로 실행하라는 명령어 (작업 완료를 기다릴 필요 없이 곧바로 다른 작업을 할 수 있다)
eg. nohup [파일명] > [로그 경로] 2>&1 & 명령을 해석하면 파일을 세션 상태와 관계없이 (세션 종료하더라도) 실행 상태를 유지하고, 지정한 위치에 로그를 남기고, 에러를 (stderr) 표준 출력으로 (stdout) 전달해서 (redirection) 로깅하고, 백그라운드로 실행하여 멀티 태스킹을 한다는 뜻이다

cf. [유닉스/리눅스] 프로세스 관리 명령어 2(j.. : 네이버블로그 (naver.com)

 

* Shell Option :: set

set -e (-o errexit): 에러 발생 시 종료
set -x (-o xtrace): 명령 실행 전 명령어와 인수 출력 (디버깅)
set -u (-o nounset): 값이 설정되지 않은 변수 사용 시 에러 발생

cf. https://ks1171-park.tistory.com/113
cf. set man page (linuxcommand.org) (공식?, 영문)

 

* 문자열에서 숫자 추출 :: sed, grep과 정규표현식

cf. [Linux] SED (tistory.com)

cf. [Linux] 리눅스 명령어 - date : 네이버 블로그 (naver.com)

## 문자열에서 숫자가 아닌 문자들을 지운 후 (sed 치환) 14자리까지만 선택 출력 (grep -o)
$ echo "2024-06-21T06:34:28+09:00" | sed 's/[^0-9]//g' | grep -o '.\{14\}'
20240621063428

Vim 단축키, 명령어

[VIM] vim 유용한 단축키 정리 - Heee's Development Blog (gmlwjd9405.github.io)

 

[VIM] vim 유용한 단축키 정리 - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io

 

Git 명령어

* 저장소로 Push한 Commit 되돌리고 바꿔서 Push하기

IntelliJ Ultimate (유료) 버전은 GUI로 지원하고, Community (무료) 버전은 터미널에서 직접 명령어를 입력해야 한다

cf. git rebase -i 알아보기 (tistory.com)

 

git rebase -i 알아보기

git rebase -i 알아보기 git 을 사용하다 보면 , 이미 커밋한 히스토리를 변경하거나 또는 삭제하거나, 내용을 추가해야하는 상황이 자주 발생합니다. 이때 사용할수 있는 명령이 바로 $git rebase -i 입

beomseok95.tistory.com

더보기

커밋 해시 확인 > rebase interactive > 제거할 commit drop > 변경 commit > push force

# 목록 (log) 확인
$ git log -4
commit [해시4] (HEAD -> master)
...
commit [해시3]
...

# 대화형 rebase
#$ git rebase -i [해시2]
$ git rebase -i HEAD~2
  ## 입력한 commit의 다음 commit부터 불러온다
pick [해시3] [메시지3]
pick [해시4] [메시지4]

# vim 수정 후 저장 (:wq)
pick [해시3] [메시지3]
drop [해시4] [메시지4] ## 제거

# 파일 변경, 신규 커밋

# 강제 push
$ git push -f
info: please complete authentication in your browser...
  ## GitHub 로그인
Enumeratig objects: 5, done. ## 영향을 받는 파일 수
Counting objects: 100% (5/5), done.
...
 + [해시5] master -> master (forced update)

 

* 알아두면 좋은 Git 명령어

더보기

자주 실수하는 git :: 부들잎의 이것저것

 

자주 실수하는 git

소프트웨어에 개발에서 실수를 했을 때, 간단히 돌아오는 방법이 있습니다. GIT을 사용해서 프로젝트를 관리한다면 실수에서 벗어나는데 큰 도움이 됩니다. 1. 추적 된 파일 추적 금지 빈번히 발

forteleaf.tistory.com

# 저장소 update (로컬 반영은 안 함)
git fetch --all

# 삭제 확인
git status

# (Git 2.23 이후) 로컬 파일과 인덱스를 (Stage) 최종 Commit으로 복원 (--source=[commit] 옵션으로 지정 가능)
git restore [파일]
# 모두 HEAD로 복원 (--soft 커밋 포인터만, --mixed 포인터+인덱스(기본값))
git reset --hard HEAD [파일]
# 로컬 파일을 최종 Commit으로 복원
git checkout -- [파일]

TimeZone 설정 및 시간 동기화

AWS는 클라우드 서버이기 때문에 아마도 주기적으로 동기화를 할 것이라 예상되지만, 개인 서버는 동기화를 직접 해야한다

Ubuntu 20.04 리눅스 서버에 Timezone변경/NTP 시간동기화/수동으로 직접 시간 설정하기

 

Ubuntu 20.04 리눅스 서버에 Timezone변경/NTP 시간동기화/수동으로 직접 시간 설정하기

리눅스 서버에 어떤 서비스를 동작시킬 경우 종종 정확한 시간 설정이 필요한 경우가 많다. 예를 들면, 데...

blog.naver.com

더보기
## 현재 시간대 설정 확인 (UTC)
$ date
Sat Jun 15 11:46:29 AM UTC 2024

## 현재 UTC 설정을 (/usr/share/zoneinfo/Etc/UTC) KST 설정으로 변경
$ sudo ln -sb /usr/share/zoneinfo/Asia/Seoul /etc/localtime
  ## -s : 심볼릭 생성 옵션
  ## -b : 백업 생성 옵션
  ## 백업 없이 덮어쓰기하려면 -sf 옵션 사용

## 시간 동기화 도구 설치
$ sudo apt-get install -y rdate
## 동기화 서버 조회 (U+ 서버 time.bora.net / NIST 서버)
$ rdate -p time.nist.gov
Sun Jun 16 03:03:35 KST 2024
## 시간 동기화
$ sudo rdate -s time.nist.gov

## 변경 확인
$ date
Sun Jun 16 03:09:11 AM KST 2024

저장소를 통해 Temurin Java 설치 안 되는 문제

Temurin 홈페이지에 나온 대로 입력해도 설치가 되지 않는다
꽤 오래된 문제인 것 같은데 2024년 6월 현재 아직까지도 해결이 안 된 듯하다
파일을 직접 다운로드하고, 압축을 풀어서 해결했다

$ sudo apt install temurin-17-jdk
E: Unable to locate package temurin-17-jdk
    ## jre 및 temurin-21-jdk도 마찬가지
더보기

문제 제기: GitHub Issue

 

Debian Bookworm repository for Temurin 21 not working · Issue #766 · adoptium/installer

What are you trying to do? Install temurin-21-jdk on Debian Bookworm Observed behaviour: root@bennu:~# cat /etc/apt/sources.list.d/adoptium.list deb [signed-by=/etc/apt/keyrings/adoptium.asc] https...

github.com

 

해결: OpenJDK – 어떤 버전, 어떤 배포판을 사용해야 할까? (tistory.com)

## JDK 직접 다운로드
$ cd /tmp
$ sudo wget [압축파일 다운로드 URL]
## 압축 해제 (=설치)
$ sudo tar xzfv [압축파일 이름]
$ sudo mv [압축 해제 후 폴더 이름] /opt/

## 환경 변수에 구문 추가
$ vim ~/.bashrc
export PATH=$PATH:/opt/[압축 해제 후 폴더 이름]/bin
## 환경 변수 적용
$ source ~/.bashrc

## 정상 설치 확인
$ java --version

Travis CI 유료화

Travis-CI 서비스가 2021년부터 더 이상 무료로 제공되지 않는다 (위키피디아 (영문))
두 가지 플랜을 월 구독, 연 구독 형태로 구매할 수 있게 하여 총 네 가지 요금이 있고, 신규 회원은 1회 한정으로 30일 동안 사용할 수 있는 크레딧을 제공받아 체험해볼 수 있다 (요금 정책 공식 문서 (영문))

GitHub 무료 저장소를 대상으로는 무료로 사용 가능하다는 글이 보이는데 (꽤 최신 글임에도!) 완전 유료화가 되기 전에는 그랬다는 것인지 (유료화 전 정책까지 찾아보긴 귀찮다), 아니면 위키피디아 한글 문서를 잘못 읽은 건지 잘 모르겠다 (위키 한글 문서는 "오픈 소스에는 무료 플랜을 제공한다"라고 되어 있는데, 오픈 소스 프로젝트가 없어졌으므로 무료 플랜도 없어졌다로 이해하는 게 맞겠다)

 

GitHub Actions라는 게 있다고 해서 (그리고 마침 참고할 수 있는 글도 있고 해서) 사용할 예정이다

Actions 사용하면서 달라지거나 바꾼 내용들은 작업 스크립트 Step 2에 작성

 

스프링 부트와 AWS로 혼자 구현하는 웹서비스 따라하기 - 9. Github Actions를 활용한 배포 자동화

이 포스팅은 이동욱 저자님의 '스프링 부트와 AWS로 혼자 구현하는 웹서비스 따라하기'를 바...

blog.naver.com

SSH, SCP

예제에서는 S3, CodeDeploy, CodeDeploy Agent 서비스를 모두 사용하는데, 각 역할을 구분해서 사용할 때 어떤 이득을 얻을 수 있는지 잘 모르겠어서 배포 서비스에서 테스트, 프로젝트 빌드 후 압축 파일을 (jar) 곧바로 서버로 전송하고, 서버에 있는 배포 스크립트를 실행시키는 방법을 구상했다

이를 구현하기 위해 GitHub Actions에서 SSH (Secure SHell), SCP를 (Secure CoPy) 이용한 서버 연결이 필요했다

GitHub Actions에서 ssh, scp 연결이 안 돼서 며칠째 삽질 중이다 (얼른 해결돼야 다른 일도 할 텐데..ㅠㅠ)

꽤 긴 시간 고통 받았으나 검색과 갖은 변형 / 적용 시도를 통해 결국 해결했다
리눅스 명령어와 ssh에 대해 굉장히 많이 배웠다

해당 문단에는 호스트와 사용자의 SSH 연결 및 관련 설정에 집중해서 적도록 하고, GitHub Actions 관련한 내용은 작업 스크립트 Step 2에 남긴다)

 

* 공개 키 암호화 방식

서로 한 쌍을 이루는 공개 키와 (Public Key /

.pub

파일) 개인 키를 (Private Key /

.pem

또는 확장자 없는 파일) 각자 생성해서, 공개 키는 서로 주고 받아 상대를 식별하는 데 (인증) 사용하고, 개인 키는 본인을 확인하는 데 (인가) 사용하는 방식

 

* SSH 키

SSH 연결을 통해 원격 서버에 접속할 때 사용되는 암호화 키

OpenSSH 설치 시 자신의 키가 기본 생성되는 것 같다 (

/etc/ssh/ssh_host_[암호화 알고리즘]_key

)
하지만 호스트의 (서버) 개인 키는 알아야 할 일이 없으니 대체로 ssh-keyscan 명령어로 공개 키만 확인해서 사용하고, 사용자는 (클라이언트) 대체로 ssh-keygen 명령어로 자신의 키를 새로 생성해서 사용한다
호스트가 사용자의 공개 키를 저장하는 기본 위치는

~/.ssh/authorized_keys

이고, 사용자가 호스트의 공개 키를 저장하는 기본 위치는

~/.ssh/known_hosts

이다

cf. SSH 공개키 인증을 사용하여 접속하기 (velog.io)

 

SSH 공개키 인증을 사용하여 접속하기

공개 키 암호 방식은 암호 방식의 한 종류로 사전에 비밀 키를 나눠가지지 않은 사용자들이 안전하게 통신할 수 있도록 한다.공개 키 암호 방식에서는 공개 키와 비밀 키가 존재하며, 공개 키는

velog.io

 

* SSH 설정 및 자격(?) 권한(?) SSH 키 생성과 사용 예제

cf. 시리즈 | Ubuntu - gogh.log (velog.io) 글들이 전반적으로 큰 도움이 되었다

 

시리즈 | Ubuntu - gogh.log

데스크탑에 Ubuntu OS를 설치해 현재 진행중인 프로젝트의 백엔드 서버를 배포할 예정이다.SSH 원격 접속, 웹 서버 구축, 도메인 연결, 서버 포트포워딩, 방화벽 설정, 보안 설정등 서버 구축에 필요

velog.io

더보기

- 1. 서버의 SSH 설정 확인

cf. [ubuntu] SSH Port 22 외 다른 port로 다중 설정하기 (tistory.com)

## 서버의 (Ubuntu) ip 확인
$ ip address
  ## ip a 동일

## SSH 상태 확인 (+포트 확인)
$ systemctl status ssh

## SSH 설정 확인 (Port, PasswordAuthentication, PubkeyAuthentication)
$ sudo vim /etc/ssh/sshd_config
  ## PasswordAuthentication yes : 암호 인증 사용 (SSH 키 접속 완료 후 지우거나 no로 변경 예정)
  ## PubkeyAuthentication yes : SSH 키 인증 사용

## (설정 변경 시) SSH 변경 적용
$ sudo systemctl restart sshd

 

- 2. (선택 사항) 클라이언트에 호스트 공개 키 저장 (StrictHostKeyChecking 옵션 입력 없이 SSH 접속)

Actions에서 SSH 접속을 막았던 가장 큰 장애물로, 원인을 못 찾아서 해결하는 데 시간을 가장 오래 쓰게 한 원흉이다
(SSH 키가 문제인가, SSH 설정이 문제인가, SSH와 관련된 거의 모든 부분을 다시 보고, 고쳐 보고..)
특히 actions/checkout 기능의 옵션 중 ssh-known-hosts의 존재 때문에 머릿 속이 더 엉켰던 것도 있다 (아직도 이 옵션의 정체는 잘 모르겠다)

사용자가 처음 SSH 연결을 시도하는 호스트일 경우 대화형 쉘이라면 호스트 키를 등록할 것인지 묻는 메시지가 나오지만, (나만 반자동으로 "yes"를 입력하고 넘기는 건 아닐 것이라 확신한다)

The authenticity of host '[서버 ip/도메인]:[포트] ([서버 ip]:[포트])' can't be established. ECDSA key fingerprint is SHA256:.... Are you sure you want to continue connecting (yes/no/[fingerprint])?
호스트 '...'의 신뢰성을 설정할 수 없습니다. ECDSA 키 지문은 SHA256:...입니다. 계속 연결하시겠습니까 (예/아니요/[지문])?
  ## yes 입력
Warning: Permanently added '[서버 ip/도메인]:[포트]' (ECDSA) to the list of known hosts.
경고: 알려진 호스트 목록에 '...'(ECDSA)가 영구적으로 추가되었습니다.

Actions 스크립트는 대화형 쉘이 아니라서 그런지 그냥

Host key verification failed.

메시지 한 줄과 함께 접속 실패해버리니 원인을 파악하기가 쉽지 않았다

 

ssh 명령 사용 시 -o StrictHostKeyChecking=accept-new 옵션을 추가하는 것이 가장 간편한 해결책이었지만, 보안 문제는 없을까 하는 막연한 걱정이 있었고 무엇보다 known_hosts 파일에 호스트 키를 수동 등록하는 방법을 더 먼저 찾은 이후에 알게 된 해결책이다.

서버에서 직접 locahost, 127.0.0.1 또는 자신의 ip 등을 입력하는 방법으로도 확인해 봤는데, 키 부분은 동일하게 나오나 전체가 일치한 것은 아니어서인지 Actions에서 사용했을 때는 인증에 실패했다 (Actions 쪽에서 뭔가를 잘못해서 그랬을 가능성도 있긴 하다)

## 클라이언트에서 (Windows Powershell) 호스트 공개 키 복사
## > GitHub Actions Secrets에 추가
PS E:> ssh-keyscan -p [포트] -H [호스트 ip/도메인]
# [호스트 ip/도메인]:[포트] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.7 ## 전체 복사 (여러 줄)
|1|... ssh-rsa ...
# [호스트 ip/도메인]:[포트] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.7
|1|... ecdsa-sha2-nistp256 ...
# [호스트 ip/도메인]:[포트] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.7
|1|... ssh-ed25519 ...

## 호스트의 공개 키 저장
## 키를 등록한 경우와 하지 않았을 경우 ssh 접속할 때 메시지 발생에 차이가 있을 것이라 예상함
PS E:> mkdir -force ~/.ssh
  ## 리눅스는 mkdir -p ~/.ssh
PS E:> echo "[복사한 호스트 공개 키]" >> ~/.ssh/known_hosts

 

- 3. 사용자의 SSH 키 생성

## 사용자의 (Windows Powershell) SSH 키 생성
PS E:> cd [키 저장할 경로]
PS E:> ssh-keygen -t rsa -b 2048 -f [키 파일명]
  ## -f 기본 값인 "~/.ssh/id_rsa"는 옵션 생략 가능
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
  ## 값을 입력해도 되고 그냥 Enter로 넘겨도 됨 > 키 파일 덮어쓰기/삭제(?) 시 사용
Enter same passphrase again:
Your identification has been saved in [키 파일명]
  ## 개인 키
Your public key has been saved in [키 파일명].pub
  ## 공개 키
The key fingerprint is:
...
  ## 키 지문

## 개인 키 변환 (리눅스일 경우 변환 없이 그대로 사용 가능)
PS E:> cp [키 파일명] [사용자 키].pem

## 개인 키를 GitHub Actions Secrets에 등록
PS E:> cat [사용자 키].pem
-----BEGIN OPENSSH PRIVATE KEY----- ## 전체 복사 (여러 줄)
...
-----END OPENSSH PRIVATE KEY-----

 

- 4. 서버에 사용자의 공개 키 저장

## 호스트로 키를 바로 전송할 수 있는 기능이 있는 것 같다 (사용해보진 않았다)
##PS E:> ssh-copy-id -i [사용자 키].pub -p [포트] [서버 사용자명]@[서버 ip/도메인]

## 사용자 공개 키 복사
PS E:> cat [사용자 키].pub
ssh-rsa ... ## 전체 복사 (한 줄)

## 호스트로 (Ubuntu) SSH 접속 (암호)
## ssh 사용하지 않고 서버에서 직접 작업해도 됨
PS E:> ssh -p [포트] [서버 사용자명]@[서버 ip/도메인]
  ## -p 기본 값인 "22"는 옵션 생략 가능
  ## 호스트 공개 키를 등록해두지 않았다면 신뢰할 수 있는 호스트로 저장할지 묻는다
  ## > GitHub Actions에서 접속 실패한 제 1 원인
... 접속 로그 및 서버 정보 ...

## 사용자 공개 키 저장
[호스트 이름]:~ $ mkdir -p ~/.ssh
[호스트 이름]:~ $ echo "[복사한 사용자 공개 키]" >> ~/.ssh/authorized_keys
  ## 파일 생성 후 vim 등으로 입력해도 됨
[호스트 이름]:~ $ chmod 600 ~/.ssh/authorized_keys
[호스트 이름]:~ $ exit

## 호스트로 SSH 접속 (키)
PS E:> ssh -i .\[사용자 키].pem -p [포트] [서버 사용자명]@[서버 ip/도메인]
... 접속 로그 및 서버 정보 ...
[호스트 이름]:~ $ exit

 

- 4.1. 키를 이용한 SSH 접속에서 오류 발생 :: 개인 키의 파일 권한 변경

@ WARNING: UNPROTECTED PRIVATE KEY FILE! @
Permissions 0644 for '[사용자 키].pem' are too open.
It is required that your private key files are NOT accessible by others. This private key will be ignored. Load key ".\[사용자 키].pem": bad permissions

cf. Windows에서 pem 파일 생성 :: icacls.exe

 

[EC2]SSH 접속 UNPROTECTED PRIVATE KEY FILE 에러 해결

발급받은 .pem(키 페어)로 EC2에 SSH 접속 시도했으나 아래 사진과 같이 막힘.키 페어 파일에 권한이 잘못 설정되었기 때문아래 명령어를 통해 권한 재설정chmod : “change mode”의 약어로, 파일이나

velog.io

## 파일 권한 변경 (Ubuntu)
$ chmod 400 [사용자 키]

## 파일 권한 변경 (Windows PowerShell)
PS E:> icacls.exe .\[사용자 키].pem /reset
처리된 파일: .\[사용자 키].pem
1 파일을 처리했으며 0 파일은 처리하지 못했습니다.

PS E:> icacls.exe [사용자 키].pem /grant:r %username%:(R,W)
  ## PowerShell에서는 에러가 발생한다 (해결책 못 찾음)
위치 줄:1 문자:51
+ icacls.exe .\[사용자 키].pem /grant:r %username%:(R,W)
+                                                   ~
매개 변수 목록에 인수가 없습니다.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : MissingArgument

  ## command에서는 정상 동작
처리된 파일: .\[사용자 키].pem
1 파일을 처리했으며 0 파일은 처리하지 못했습니다.

PS E:> icacls.exe [사용자 키].pem /inheritance:r
처리된 파일: .\[사용자 키].pem
1 파일을 처리했으며 0 파일은 처리하지 못했습니다.

 

- 5. 서버의 SSH 설정 변경

## SSH 설정 변경 (Port, PasswordAuthentication, PubkeyAuthentication)
$ sudo vim /etc/ssh/sshd_config
  ## PasswordAuthentication no # 항목을 지워도 동일 동작

## SSH 변경 적용
$ sudo systemctl restart sshd

* SSH 접속 시

.bashrc

호출이 안 됨 (java 명령어 전역 호출 불가능)

서버에 있는 deploy.sh을 실행하는 것임에도 java 명령어를 찾을 수 없다는 오류 메시지가 발생한다
(물론 서버에서 직접 실행하면 정상 동작하는 bash 파일이다)

$ ssh user@remote "app/step2/deploy.sh"
    # nohup java -jar $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

nohup: failed to run command 'java': No such file or directory
ssh 실행은 정상적으로 완료됐어도 nohup.out 열어보면 오류인 경우도 있다

 

깔끔한 해결책은 못 찾았고 그냥 java bin 경로를 특정 위치에 저장하고 bash에서 그 경로를 참조해 java 명령을 실행하도록 했다

nohup "$( cat app/java_path )"/java -jar $JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

 

* SCP

SSH 프로토콜을 사용한 파일 전송 프로토콜로, 파일이나 폴더를 보내거나 가져올 수 있다

더보기

-p: 원본 파일의 수정 시간, 접근 시간 및 모드를 보존
-P: 원격 서버의 포트 번호를 지정 (기본 값인 22번 포트는 생략 가능)
-r: 폴더와 그 내용을 재귀적으로 복사
-c: 사용할 암호화 알고리즘을 지정 (기본 값인 aes128-ctr 알고리즘은 생략 가능)
-C: 압축 전송 (전송 속도를 높인다?)
-l: 대역폭 제한 (-l 100k 옵션은 초당 100KB로 대역폭 제한)
-i: SSH 키 파일을 지정 (기본 값인 ~/.ssh/id_rsa 또는 ~/.ssh/id_dsa 파일은 생략 가능)
-o: SSH 옵션 지정 (기본 값인 StrictHostKeyChecking=yes 옵션은 생략 가능)

$ scp -p -P 2222 -i /path/to/my_key.pem -c aes256-ctr -o StrictHostKeyChecking=no user@remote_host:"/path/to/remote/file /path/to/remote/file2" /local/destination/path
  ## 원격의 /path/to/remote/file, file2 파일을 로컬의 /local/destination/path 경로로 복사
  ## -p 옵션으로 원본 파일의 수정 시간, 접근 시간 및 파일 권한 등 메타 정보를 보존
  ## -P 옵션으로 원격 접속 시 2222 포트 사용
  ## -i 옵션으로 /path/to/my_key.pem 키 파일을 SSH 인증에 사용
  ## -c 옵션으로 데이터 전송에 aes256-ctr 암호화 알고리즘을 사용
  ## -o StrictHostKeyChecking=no 옵션으로 원격 호스트에 대한 키 확인을 하지 않음

 

예제와 실제 배포 사이의 차이 + 작성한 스크립트 및 소스

* Step 1 (Git 연결 및 빌드, 수동 배포)

- 외부 설정 파일 (oauth.properties, db.properties)

처음에는 예제와 마찬가지로 Git으로 관리하지 않았기 때문에 git clone 이후 서버에서 직접 생성 및 수정 (모든 properties 파일은 /src/main/resources/에만 존재)
이후 Step 1 진행 끄트머리 즈음에 각각의 sample.properties 만들어 Git으로 관리하고, 차후 서버에서는 Git에서 sample을 받아서 생성하는 방향으로 진행 예정

 

- 생성한 소스 (기본적으로는 GitHub 저장소 참조)

더보기

* [서버] ~/app/step1/deploy.sh

#!/bin/bash

REPOSITORY=/home/[사용자명]/app/step1
PROJECT_NAME=[프로젝트명] # rootProject.name in /settings.gradle

cd $REPOSITORY/$PROJECT_NAME/

echo ">> Git Pull"
git pull

echo ">> Build"
./gradlew build

## GitHub 저장소명과 프로젝트명이 달라서 Jar 이름을 먼저 가져옴
## 시간 역순 정렬 목록에서 jar 파일만 검색, 마지막 1개
#JAR_NAME=$(ls -tr ./build/libs/ | grep .jar | tail -n 1)
## 시간순 정렬 후 jar 파일만 검색, 처음 1개
JAR_NAME=$(ls -t ./build/libs/ | grep .jar | head -n 1)
echo ">> Copy jar file to Root :: $JAR_NAME"
cp ./build/libs/$JAR_NAME $REPOSITORY/

echo ">> Find Running PID"
CURRENT_PID=$(pgrep -f ${JAR_NAME})
if [ -z "$CURRENT_PID" ]; then
    echo ">> Nothing found PID to Stop"
else
    echo ">> Stop PID :: $CURRENT_PID"
    kill -15 $CURRENT_PID
    sleep 10
fi

echo ">> Start Application"
## 로그 저장할 위치로 이동
cd $REPOSITORY
## 설정 파일 지정 불필요 (모두 classpath: 밑에 있음)
#nohup java -jar \
#  ## 역슬래시로 ('\') 줄바꿈
#    -Dspring.config.location=\
#        classpath:/application.properties,\
#          ## 구동 환경 (profile) 설정 시 공통으로 관리할 설정이 있는 게 아니라면 불필요
#        [추가 property 절대 경로] \
#    -Dspring.profiles.active=real \
#      ## 구동 환경 구성: application-{profile}.properties을 자동으로 찾음
#    $REPOSITORY/$JAR_NAME 2>&1 &
nohup java -jar $REPOSITORY/$JAR_NAME 2>&1 &

- 예시 내용 정리 (기본적으로는 예시 프로젝트 GitHub 참조)

더보기

* AWS 가입 > 결제 방법 등록 > 기본 플랜 (무료) 요금제 선택

 

* deploy.sh 파일은 생성한 파일에 주석으로 함께 정리

 

* Step 2 (필요한 파일만 자동 배포 :: GitHub Actions)

- 테스트용 설정 파일 추가

GitHub Actions에서 gradlew test 실행 시 /src/main/resources/에 있는 properties 파일들을 불러오지 못하고 GitHub에는 불러올 수 있는 properties 파일이 없는 상태이기 (sample만 존재) 때문에 에러를 발생시킨다
테스트에 필요한 설정들만 추려서

/src/test/resources/

에 properties 파일을 새로 만들어줘야했다

- Actions 스크립트에 분기를 추가하려는 뻘짓에서 배운 점

test 결과에 따라 build로 넘어갈지, 에러 발생 메일을 전송하고 작업을 중지할지 분기를 만들려고 했으나 결론적으로 삽질만 잔뜩한 셈이었다

더보기

* gradlew build 중 테스트 동작과 gradlew test의 동작이 동일

현재 작업 중인 프로젝트는 멀티 모듈이 아니라서 gradlew build 진행 중 실행되는 테스트는 gradlew test와 결국 같은 동작인 것으로 보인다 (멀티 모듈이더라도 build 시 실행되는 테스트나 gradlew test나 같은 동작일 것 같은데 거짓말 잘하는 AI는 일단 다르댄다)

* Workflow 실행 중 실패가 발생하면 저장소 소유자에게 메일을 전송하는 시스템 기능이 이미 존재한다

* Step 실행 조건의 기본 값

각 step은 if: success() 조건이 기본 값으로 설정되어 있으므로 재정의하지 않는 한 앞선 작업 중 하나라도 실패하면 실행되지 않는다
cf. [GitHub Actions] if 사용해 Step이 Fail되었을 때 다음 Step 제어하기 — 조세영의 Kotlin World

- Actions 스크립트에서 SSH 연결에 성공하기까지, 그리고 호스트의 bash 파일을 정상 실행하기까지 고생을 꽤나 했다

자세한 내용은 SSH 연결 문단으로..

- GitHub Actions Context

더보기

*

github.repository

>

github.event.repository.namegithub.repository

값이 "소유자/저장소"라서 (eg. "Seo-Kim/example-springboot") 파일명으로 사용하기에 부적절 (슬래시가 ( '/' ) 문제됨)
저장소 이름만 (eg. "example-springboot") 값으로 갖고 있는 github.event.repository.name 사용
(24.06.30. 추가) 또는 basename 명령어 사용 (https://terms.naver.com/entry.naver?cid=59321&docId=4125562&categoryId=59321)

*

github.event.commits[0].timestamp

일시 (날짜와 시간) 값을 가진 유일한 Context
출력 형태는

[YYYY]-[MM]-[DD]T[HH24]:[mm]:[ss]+09:00

* toJson() 함수

Context의 값이 문자열일 때 toJson([Context 이름]) 함수를 사용하면 객체가 아닌 문자열로 반환된다고 함 (무슨 차이인 건지..;;)
eg. ls -l build/libs/${{ toJson( github.event.repository.name ) }}

 

- 생성한 소스 (기본적으로는 GitHub 저장소 참조)

더보기

*

/src/test/resources/application.properties
# gradle test 실행 시 필요한 최소 설정 정의
spring.security.oauth2.client.registration.google.client-id=test
spring.security.oauth2.client.registration.naver.client-id=test
spring.security.oauth2.client.registration.naver.authorization-grant-type=test
spring.security.oauth2.client.provider.naver.user-info-uri=
  ## 다른 것들과 다르게 값이 없어도 동작했다

* 서버에서 Git에 있는 sample properties 다운로드 받아 수정

$ mkdir -p ~/app/step2/props && cd ~/app/step2/props

# application-db.properties, application-oauth.properties 생성
#$ curl -O https://raw.githubusercontent.com/[Git 소유자]/[Git 저장소]/[브랜치]/src/main/resources/application-db-sample.properties
  ## -O (--remote-name): 원격의 파일 이름을 그대로 사용해서 기록
  ## 서버에 "application-db-sample.properties" 이름으로 다운로드되며, 이름과 내용 모두 수정 필요

$ curl -o application-db.properties https://raw.githubusercontent.com/[Git 소유자]/[Git 저장소]/[브랜치]/src/main/resources/application-db-sample.properties
  ## -o (--output) <파일명>: stdout (표준 출력) 대신 지정한 이름인 파일에 기록

* /.github/workflows/gradle.yml (GitHub Actions 스크립트)

run: |
  ## 접속 허용 목록에 수동 등록
  mkdir -p ~/.ssh
  echo "${{ secrets.[접속 키 변수명] }}" > ~/.ssh/known_hosts
  ## 클라이언트 개인 키 파일 생성
  echo "${{ secrets.[개인 키 (pem) 변수명] }}" > ./[개인 키 파일명]
  chmod 400 ./[개인 키 파일명]
  ## build jar 파일을 서버로 전송
  scp -i ./[개인 키 파일명] -P [포트] ./build/libs/[jar 파일명] [사용자명]@[서버 ip/도메인]:[jar 저장할 경로]
  ## 서버의 배포 스크립트 실행
  #ssh -i ./[개인 키 파일명] -o StrictHostKeyChecking=accept-new -p [포트] [사용자명]@[서버 ip/도메인] '[실행할 스크립트 파일 경로]'
    ## known_hosts 등록 없이 옵션 추가만으로도 해결 가능 > 보안 문제?
  ssh -i ./[개인 키 파일명] -p [포트] [사용자명]@[서버 ip/도메인] '[실행할 스크립트 파일 경로]'

- 예시 내용 정리 (기본적으로는 예시 프로젝트 GitHub 참조)

더보기

* 배포 작업의 순서

GitHub push > Travis에서 catch, build > AWS S3 upload, AWS CodeDeploy 호출(?) > (설치된 CodeDeploy Agent 통해) AWS EC2에 배포 및 서비스 재시작

* AWS IAM (Identity and Access Management) 외부 서비스용 권한 생성 (Travis - S3, CodeDeploy)
사용자 추가 > 접근 유형 "프로그래밍 방식" > 권한 설정 "기존 정책 연결" (S3 Full, CodeDeploy Full) > Access Key, Secret Key 발행 (공개 키, 개인 키 느낌) > .travis.yml에서 S3 버킷 접근 시 활용 (버킷 보안 설정은 "모든 공개 접근 차단")

* AWS IAM 내부 서비스용 권한 생성 (CodeDeploy - EC2)

역할 만들기 > 역할 사용 서비스 "EC2" > 정책 "EC2 Role for CodeDeploy" > EC2 인스턴스 설정 > 역할 연결 (인스턴스 재부팅)
역할 만들기 > 역할 사용 서비스 "CodeDeploy" > 정책 "AWS CodeDeploy Role" > CodeDeploy 배포 그룹 생성 시 서비스 역할에 적용

* EC2에 CodeDeploy Agent 설치

$ aws s3 cp s3://aws-codedeploy-ap-northeast-2/latest/install . --region ap-northeast-2
download: ...

$ chmod +x ./install
$ sudo ./install auto

## 설치 중 루비 환경 변수 관련 에러 발생 시
$ sudo yum install ruby

$ sudo service codedeploy-agent status
The AWS CodeDeploy agent is running as PID ***

*

/.travis.yml

(Travis CI 스크립트)

...
before_deploy:
  ## 압축으로 묶을 파일들을 한 곳으로 모음
  - cp scripts/*.sh before-deploy/
  - cp appspec.yml before-deploy/
  - cp build/libs/*.jar before-deploy/
  ## S3는 파일 특정해서 업로드 불가 (디렉토리만 선택 가능)
  - zip -r deploy/freelec-springboot2-webservice.zip before-deploy/*

deploy:
  - provider: s3
    access_key_id: $AWS_ACCESS_KEY ## Travis repo settings에 설정된 값
    secret_access_key: $AWS_SECRET_KEY ## Travis repo settings에 설정된 값
    acl: private ## zip 파일 접근을 private으로
    local_dir: deploy ## 압축 파일 위치
    ...

  - provider: codedeploy
    access_key_id: $AWS_ACCESS_KEY
    secret_access_key: $AWS_SECRET_KEY
    bucket: freelec-springboot-build ## S3 버킷 이름
    key: freelec-springboot2-webservice.zip ## 압축 파일명
    application: [웹 콘솔에서 설정한 CodeDeploy 어플리케이션 이름]
    deployment_group: [웹 콘솔에서 설정한 CodeDeploy 배포 그룹 이름]
    ...
...

*

/appspec.yml

(CodeDeploy 스크립트)
https://github.com/jojoldu/freelec-springboot2-webservice/blob/fd9214134f941acc1e5fbd05184c6522fbab3a34/appspec.yml

version: 0.0 ## CodeDeploy 버전
  ## 프로젝트 버전이 아니므로 0.0으로 입력
...
files:
  - source:  / ## 전체 파일
      ## EC2 임시 다운로드 위치: /opt/codedeploy-agent/deployment-root/[User ID]/
    destination: ~/app/step2/zip/
    ...
permissions: ## 파일에 부여할 권한 설정
    ...
hooks: ## 배포 시 실행할 내용
  ApplicationStart:
    - location: deploy.sh
      ...

*

/script/deploy.sh
...
$ CURRENT_PID=$( pgrep -fl freelec-springboot2-webservice | grep jar | awk '{print $1}' )
...
$ nohup java -jar \
    ...
    $JAR_NAME > $REPOSITORY/$nohup.out 2>&1 &
      ## 대화형 쉘이 아닌 배포 서비스 스크립트로 실행될 파일이므로 로깅 위치를 지정해줘야 함
      ## 지정하지 않을 경우 서버가 아닌 배포 서비스에서 실행되어 무한 대기하며 로깅할 것
      ## CodeDeploy 로그: /opt/codedeploy-agent/deployment-root/deployment-logs/codedeploy-agent-deployments.log

 

* Step 3 (무중단 배포 :: NginX)

- 무중단 배포 방식

서버를 여러 개 두거나 (AWS Blue-Green)
컨테이너를 사용하거나 (Docker)
스위칭 장비를 사용하거나 (L4 스위치)
리버스 프록시, 로드 밸런싱 기능이 있는 웹 서버를 사용 (NginX)

 

- 생성한 소스 (기본적으로는 GitHub 저장소 참조)

- 예시 내용 정리 (기본적으로는 예시 프로젝트 GitHub 참조)

더보기

* EC2 보안 그룹 설정

EC2 > 보안 그룹 > 인바운드 편집 > TCP 80포트 ip 0.0.0.0/0, ::/0 추가

(이때 브라우저에서 80 포트로 접속해보면 nginx 초기 페이지가 출력됨)

 

* 프로그램 URL 입력된 곳들의 접속 포트 변경

OAuth 제공자의 Redirection URI, Callback URL 등에서 포트를 80으로 변경 (기본 값이므로 제거)

 

* NginX

# 설치
$ sudo yum install nginx

# 실행
$ sudo service nginx start
Starting nginx: [ OK ]

# 재실행 (설정 변경 후 적용, 잠시 끊김)
$ sudo service nginx restart

# 설정 다시 불러오기 (외부 설정 다시 불러와 적용, 거의 끊김 없음)
$ sudo service nginx reload

 

* 프록시 설정

/etc/nginx/conf.d/service-url.inc

set $service_url http://127.0.0.1:8080;
  ## port 변경 시에도 nginx 재시작할 필요 없도록 include 파일 만들어 따로 정의


/etc/nginx/nginx.conf

include /etc/nginx/conf.d/service-url.inc;

server {
  ...
  location / {
    #proxy_pass http://localhost:8080;
    proxy_pass $service_url;
      ## 요청을 전달할 URI
    ...
  }
}

 

* profile 별로 properties 추가

/src/main/resources/application-real1.properties, application-real2.properties

server.port=8081 //real2: 8082
...

 

* Active Profile 확인하는 API 생성 및 Security 인증 예외 설정
/src/main/java/.../web/ProfileController.java,  /src/test/java/.../web/ProfileControllerUnitTest.java, /src/main/java/.../web/ProfileController.java

private final Environment env;
  // 생성자 주입의 장점: Test 시 Spring이 제공하는 구현체 MockEnvironment를 사용하면 되므로 @SpringBootTest 불필요

List<String> profiles = Arrays.asList(env.getActiveProfiles());
  // 현재 실행 중인 (active) profile을 모두 가져온다
  // 이후 stream filter를 통해 필요한 profile만 추려서 반환할 것이다

 

* 배포 스크립트

/scripts/start.sh (배포 후 중지했던 프로그램 시작), stop.sh (배포 대상 (=NginX에 연결되지 않은) 프로그램 중지)

# 현재 실행 파일의 위치
ABSPATH=$(readlink -f $0)
  ## $0:
ABSDIR=$(dirname $ABSPATH)

# import / include
source ${ABSDIR}/profile.sh
...
IDLE_PID=$(lsof -ti tcp:${IDLE_PORT})
  ## lsof:
  ## -ti:

 

/scripts/switch.sh (NginX 프록시 설정 변경)

# service-url.inc 파일 수정
echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" | sudo tee /etc/nginx/conf.d/service-url.inc
  ## set:
  ## tee: 덮어쓰기?

 

/scripts/health.sh (프로그램의 정상 실행 확인)

for RETRY_COUNT in {1..10}
do
  # 배포 대상 포트로 프로그램이 정상 실행되었는지 확인
  RESPONSE=$(curl -s http://localhost:${IDLE_PORT}/profile)
  UP_COUNT=$(echo ${RESPONSE} | grep 'real' | wc -l)
    ## wc:
  ... ## 성공 / 실패 시 "break" / "exit 1"
done

 

/scripts/profile.sh (NginX에 연결되지 않은 profile, 포트 확인)

RESPONSE_CODE=$( curl -s -o /dev/null -w "%{http_code}" http://localhost/profile )
  ## 생성한 Profile API 호출
  ## -s: 
  ## -o: 
  ## -w: 
...
# bash는 return value 없어서 echo로 출력해서 활용 (!!다른 출력이 없도록 한다!!)
echo "${IDLE_PROFILE}"

 

/appspec.yml

...
hooks:
  AfterInstall:
    - location: stop.sh ## 배포 대상 (NginX에 연결되지 않은) 프로그램 중지
      timeout: 60
      runas: ec2-user
  ApplicationStart:
    - location: start.sh ## 배포 후 중지했던 프로그램 시작
      ...
  ValidateService:
    - location: health.sh ## 프로그램의 정상 실행 확인
      ...
...

 

* Build마다 버전 변경

/build.gradle

version '1.0.1-SNAPSHOT-'+new Date().format("yyyyMMddHHmmss")
  ## Groovy 문법으로 빌드 일시를 추가