
해당 포스팅은 유투버 Bucky의 『Pro Django Tutorial』 9강의 내용을 바탕으로 작성 했습니다.
동영상 바로가기
동영상 속 깃헙 바로가기
해당 강의에서는 main branch에서 push 이벤트 발생시 배포 워크플로우를 실행하는 가장 기본적인 구조의 CI/CD 파이프 라인을 만든다.
초기 세팅으로 프로젝트 최상위 디렉토리에서
./github/worflows 디렉토리를 생성한 후 시작한다.
깃헙이 개발컴과 서버컴에 통신할 수 있도록 하기 위해
개발컴에서 SSH 키 생성 후
비밀키를 깃헙에 저장한다 (?)
다음 명령어를 통해 SSH키를 생성해 주면 공개키(.pub)와 개인키 한쌍이 생성 된다;
ssh-keygen -t rsa -b 4096 -f ~/.ssh/cicd
자동화를 위해 password는 설정하면 안된다! (엔터 두번)
깃헙에서 프로젝트 레포 > settings > Secrets and variables > Actions > New repository secret 버튼 클릭
1️⃣ SSH_PRIVATE_KEY
SSH_PRIVATE_KEY를 이름으로,
cat (위에서 생성한 ssh 비밀키 이름) 의 결과를 시크릿으로 등록해 준다.
(이때, ----BEGIN ... 부터 ----END ... 까지 모두 카피해 주도록 한다.)
2️⃣ SSH_HOST
SSH_HOST 이름으로,
우분투 서버컴 공인 IP 주소를 시크릿으로 등록해 준다.
3️⃣ SSH_USER
SSH_USER 이름으로,
우분투 서버컴 유저명을 시크릿으로 등록해 준다.
2, 3번 작업은 추후 워크플로우에서 자동배포를 위해
서버컴에 ssh 접속을 위한 환경변수로 사용하기 위함이다.
ssh (사용자명)@(공인아이피 주소) 를 통해 서버컴 터미널에 접속한다.
이후 sudo nano ~/.ssh/authorized_keys 명령어를 통해 편집기를 켜준 후
개발컴에서 cat (위에서 생성한 ssh 공개키 이름) 을 했을 때 출력되는 공개키를 복붙해 넣어준다.
(참고로 'authorized_keys'는 서버컴에서 /home/(사용자명)/.ssh/authorized_keys 위치에 있다.)
+) 이걸 한 목적?
ssh (유저명)@(공인 IP 주소) 했을 때 서버컴이 인증요구 -> 맥북에서 비밀키로 응답 하면 서버컴의 /home/(사용자명)/.ssh/authorized_keys에 있는 공개키와 짝이 맞는지 확인 -> 비밀번호 없이 접속 허용됨./github/workflows/master.yml 파일을 생성한 뒤 다음과 같이 작성해 준다;
name: Continuous Integration
# 트리거 설정 - main 브랜치에 push 이벤트 발생시
on:
push:
branches:
- main
# 동시성 방지 - push 중에 새로운 push 이벤트가 발생하면 현재 작업을 멈추고 새로운 작업을 시작하도록
concurrency:
group: main
cancel-in-progress: true
# 수행할 작업
jobs:
deploy:
name: 배포
runs-on: ubuntu-latest
steps:
- name: 서버컴 SSH 접속
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
run: |
mkdir -p ~/.ssh/
echo "$SSH_PRIVATE_KEY" > ~/.ssh/cicd
chmod 600 ~/.ssh/cicd
cat >>~/.ssh/config <<END
HOST target
HostName $SSH_HOST
USER $SSH_USER
IdentityFile ~/.ssh/cicd
LogLevel ERROR
StrictHostKeyChecking no
END
- name: 배포 작업
run: |
ssh target "cd /home/saemi/src/project_2/Core && \
docker-compose down && \
git pull && \
docker-compose build && \
docker-compose -d --force-recreate"
상세 설명;
깃헙 Secrets를 명령어로 사용하기 위해 환경변수로 가져오기 위한 작업;
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
이후 SSH 접속을 위해 실제로 실행할 명령어들;
run: |
mkdir -p ~/.ssh/
echo "$SSH_PRIVATE_KEY" > ~/.ssh/cicd
chmod 600 ~/.ssh/cicd
cat >>~/.ssh/config <<END
HOST target
HostName $SSH_HOST
USER $SSH_USER
IdentityFile ~/.ssh/cicd
LogLevel ERROR
StrictHostKeyChecking no
END
runs-on: ubuntu-latest는 초기화 상태의 우분투를 뜻한다.
+) 이건 깃헙 액션이 제공하는 가상머신에서 돌아가는건가?
또한 run: | 에서 | 는 여러 줄짜리 명령어 블록을 작성할 때 사용한다. (| 없이는 한줄만 인식된다.)
따라서 mkdir -p ~/.ssh/ 명령어를 통해 ssh를 담아둘 디렉토리를 생성해 주고
echo "$SSH_PRIVATE_KEY" > ~/.ssh/cicd 비밀키를 복사해 준다.
chmod 600 ~/.ssh/cicd 로 권한을 주고
+) root에게 읽기 + 쓰기 권한을 주는건가? 왜 6임?
cat >> ~/.ssh/config 에서,
cat 은 본디 파일 내용을 보여주는 명령어지만, >> 와 함께 쓰면 파일 끝에 추가(append) 한다는 의미이다. 즉, ~/.ssh/config 파일에 다음에 올 내용을 덧붙이겠다는 뜻.
<<END 는 END 가 나올 때까지의 텍스트를 cat 명령어의 입력으로 사용한다는 의미이다. 즉, ~/.ssh/config 파일에 다음 내용을 그대로 추가한다는 뜻;
HOST target
HostName $SSH_HOST
USER $SSH_USER
IdentityFile ~/.ssh/cicd
LogLevel ERROR
StrictHostKeyChecking no
이때 StrictHostKeyChecking no 는 아묻따 진행하라는 의미.
ssh target ... 명령어는 SSH로 서버컴 접속시 기본 디렉토리(/home/saemi)를 기준으로 실행된다.
추가적인 에러가 싫어 그냥 절대경로로 지정해 주었다.
--force-recreate
기존 컨테이너가 변경되지 않았더라도 항상 새로 생성하라는 의미이다.
코드나 이미지가 바뀌지 않았더라도 컨테이너를 내려서 새로 띄우는 작업을 한다.
하지만 볼륨(volume)이나 영속 데이터를 제거하지는 않는다.
(docker-compose에서 DB 볼륨을 명시적으로 분리한 경우에)데이터 유실은 없다.
GitHub Actions가 CI/CD 워크플로우를 실행(main 브랜치에서 push 이벤트 감지시 master.yml 파일 실행)
해당 워크플로우에서 GitHub Secrets에 등록된 비공개 SSH 키를 환경변수로 가져옴
22번 포트(또는 지정된 포트)로 서버에 SSH 접속 시도
서버컴에서는 /home/사용자명/.ssh/authorized_keys에서 해당 SSH 비밀키와 일치하는 공개키를 찾음
공개키와 비공개키가 일치한다면 로그인 허용
로그인 후 배포 스크립트 실행
▶️ GitHub Actions 측 (클라이언트 역할)
GitHub 서버에서 ssh 명령을 실행함 (실제는 ubuntu-latest 라는 가상의 Linux 머신에서 실행됨)
~/.ssh/config에 Host, HostName, User, Port, IdentityFile 설정이 포함됨
IdentityFile로 지정된 비밀키(private key) 를 사용해서 서버에 접속 시도
▶️ 서버 측 (서버 역할)
서버에서 SSH 데몬(sshd)이 포트 22 또는 지정된 포트에서 대기
SSH 접속 요청이 들어오면:
클라이언트가 비밀키를 기반으로 서명한 인증 요청을 보냄
서버는 /home/사용자명/.ssh/authorized_keys 안의 공개키들 중에
이 요청을 복호화할 수 있는 키가 있는지 확인
있으면 → 로그인 허용
없으면 → Permission denied (publickey) 발생
이후 git push를 진행해 주었더니 에러가 났다. ㅎㅎ..

env로 넘겨준 SSH_PRIVATE_KEY, HOST, USER을 적용하기도 전에 접속 자체가 안됨. 서버가 SSH 접속을 아예 거부하고 있음.
그럼에도 불구하고 동일한 에러 반복.
sudo ufw allow 22
그럼에도 불구하고 동일한 에러 반복.
sudo systemctl status ssh SSH 서버가 돌고 있는지 확인 결과 정상.
ssh -i ~/.ssh/cicd saemi@[공인IP 또는 도메인]
깃헙 액션 실행 환경(개발컴)에서 서버 포트 22가 열려있는지 확인 결과
--> 개발컴에서 서버컴으로 (비밀번호 묻지 않고) ssh 접속 잘 됨
+) 근데 포트포워딩 설정 하느라 집에 돌아와서 시도한거라 (개발컴과 서버컴이 동일한 공인 IP 주소를 사용하고 있어서) 내일 나가서 다시 확인해 봐야함!
telnet [공인IP] 22
외부 네트워크에서 SSH 접속시 포트 22가 열려있는지 확인
+) 이것도 내일 다시 확인해 봐야함!
+) telnet 이란?
서버컴에서 ssh 데몬 작동 중인거 재확인 완료
방화벽 22번 포트 열린거 재확인 완료
secrets.SSH_HOST에 정확한 공인 IP 주소 재입력 완료
공유기 포트포워딩 설정도 재확인 완료
sudo ss -tlnp | grep :22 명령어를 통해 현재 이 서버가 포트 22에서 TCP 요청을 받고 있는지 확인
결과 아무런 출력도 나오지 않았다. 즉, 서버컴이 현재 22번 포트를 열고있지 않다는 의미. 🤨
해서 기존의 SSH키를 찾아보았다;
ssh -v saemi@서버_공인_IP 명령어는 해당 서버 접속시 사용된 로그가 출력된다.
출력된 로그들 중 다음과 같은 부분을 찾을 수 있다;
Offering public key: /Users/saemi/.ssh/id_rsa RSA SHA256:XXXXXXXXXXXXXXXXXXXXX
여기서 'XXXXXXXXXXXXXXXXXXXXX' 부분은 공개키의 fingerprint(공개키를 해싱한 값 = 공개키의 요약본)이다.
이제 fingerprint를 갖고 공개키를 찾을 수 있다.
(물론 이름만 봐도 나오지만 더 정확하게 찾겠다)
ssh-keygen -lf id_rsa.pub 를 해보면 3072 SHA256:XXXXXXXXXXXXXXXXXXXXX 사용자@이메일.com (RSA) 이 나오는데, 이를 위에서 출력된 값과 비교해 보면 쉽게 찾는다.
같은 실수를 반복하지 않기 위해... (ㅠㅠ) 이번에는 config 파일에 야무지게 저장 해줬다. 다음과 같은 방식으로 저장해 준다;
Host [별칭]
HostName [서버_IP 또는 도메인]
User [사용자명]
IdentityFile [비밀키 경로]
Port [포트번호] # 기본값은 22, 다르면 명시
해당 키는 이미 포트포워딩 설정이 안정적으로 된 상태이기 때문에
자동배포용 SSH키로 함께 쓸까 했다.
하지만 다음과 같은 이유로 별도의 키 생성이 바람직하다고 한다;

다시 최초의 오류로 돌아가서.. sshd_config 파일을 살펴보기로 했다.
그 안에는 다음과 같은 내용이 있었고;
Include /etc/ssh/sshd_config.d/*.conf
이 경우 /etc/ssh/sshd_config.d 디렉토리 안에 있는 .conf 파일들이 후속 적용 되면서 포트 설정을 다시 덮어쓸 수도 있다고 한다.
하지만 확인해보니 sshd_config.d 디렉토리는 빈 폴더였다.
리눅스 시스템에서 1024번 이하 포트는 root만 열 수 있다고 한다.
따라서 SSH가 root가 아닌 다른 사용자로 실행되고 있다면, 22번 포트를 열지 못하고 조용히 무시될 수 있다고 한다.
+) 이게 뭐람?
이에 대해 더 파보기 전에.. 그냥 master.yml 파일에서 포트를 지정해 주니 해당 오류는 넘어갔다.
+) SSH 접속시 따로 지정한 포트 외에 22번 기본포트도 열도록 왜 못하는거지?
Step 16/16 : ENTRYPOINT ["/entrypoint.sh"]
---> Using cache
---> 49ab87fbb49f
Successfully built 49ab87fbb49f
Successfully tagged core_app:latest
...
Error: Process completed with exit code 1.
이미지 빌드는 성공 했으나 컨테이너를 띄우는데 실패했다.
서버컴에 접속해 확인해보니 ./shared/gunicorn/ 위치에 구니콘 소켓파일도 만들어지지 않았다.
수동으로 서버컴에서 띄웠을 때에는 분명 됐는데?
이상해서 master.yml 파일을 확인해보니 docker-compose -d --force-recreate 명령어에 오타가 있었다. up 을 빠트린 것..
수정 이후 다시 push 하니 자동 배포에 성공했다!!!!

아오.. 이걸 하루 종일 붙들고 있었다니...
17분짜리 강의였는데..
그래도 조타...
오늘도 맘편히 잘 수 있게따.. ⭐️
끝!