[도커-4] CI/CD 자동화: GitHub Actions를 통한 도커 이미지 빌드와 자동 배포
아니 도커 얘기중인데 갑자기 깃허브 관련한게 왜 나옴 ?
도커로 애플리케이션을 깔끔하게 컨테이너화하는 것까진 성공했다. 그런데 문득 궁금증이 생겼다.
“내가 만든 이 이미지를 코드가 바뀔 때마다 자동으로 빌드하고, 서버에 올리는 과정은 어떻게 해야 할까?”
결국 도커의 궁극적인 목적, 즉 ‘어디서든 쉽게 실행되는 애플리케이션’을 완성하려면 자동화 파이프라인 구축이 필수적이었다.
Jenkins, GitLab CI/CD, CircleCI, GitHub Actions등 수 많은 CI/CD 도구가 있었는데,
여기서는 GitHub Actions를 사용하여 자동화 파이프라인을 구축해보도록 하겠다.
도커가 결과물(이미지)을 담당한다면, 액션은 과정(빌드, 테스트, 배포)을 책임지는 셈이다.
최종적인 워크 플로우는 아래와 같이 될 것이다.
1. 개발자가 로컬에서 코드 수정
↓
2. 수정 내역 Git Push
↓
3. 자동 테스트 및 빌드
↓
4. 성공시 자동 도커 허브 업로드
깃허브
레포지토리 생성
우선 코드를 올릴 수 있는 레포지토리가 필요하다.
docker_github-actions 라는 이름으로 생성을 하고, 도커와 연동하기 위해
도커 허브 엑세스 토큰을 발급받아 유저 이름과 함께 해당 레포지토리 secrets에 등록을 했다.
깃허브 액션
깃허브 액션의 워크플로우를 정의하고, 사용하기 위해서 반드시 지켜야 하는 규칙은 두 가지이다.
- 레포지토리 최상단에 필수 디렉토리명 지정하여 생성: .github/workflows
- 해당 디렉토리 안의 파일은 yaml, yml 포맷으로 작성
다음은 .github/workflows/docker-build.yml의 내용이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# ============================================
# Workflow 이름 (GitHub Actions 탭에 표시됨)
# ============================================
name: Docker Build and Push
# ============================================
# 트리거: 언제 이 workflow를 실행할지 정의
# ============================================
on:
push:
branches: [ main ] # main 브랜치에 push 시 실행
tags:
- 'v*' # v로 시작하는 태그 push 시 실행 (예: v1.0, v2.1)
# ============================================
# Jobs: 실행할 작업들
# ============================================
jobs:
build:
# Ubuntu 최신 버전 가상 머신에서 실행
runs-on: ubuntu-latest
# ============================================
# Steps: 순차적으로 실행할 단계들
# ============================================
steps:
# ----------------------------------------
# Step 1: GitHub 저장소에서 코드 가져오기
# ----------------------------------------
- name: Checkout code
uses: actions/checkout@v3 # GitHub 공식 액션 사용
# ----------------------------------------
# Step 2: 버전 정보 추출
# 태그 push면 태그 이름(v2.1), 아니면 'latest'
# ----------------------------------------
- name: Get version
id: version # 다른 step에서 참조할 ID
run: |
# GITHUB_REF가 refs/tags/로 시작하는지 확인
if [[ $GITHUB_REF == refs/tags/* ]]; then
# refs/tags/v2.1 → v2.1 추출
VERSION=${GITHUB_REF#refs/tags/}
# 출력 변수 설정 (다른 step에서 사용)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
else
# 태그가 아니면 latest
echo "version=latest" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
fi
# ----------------------------------------
# Step 3: Docker Hub 로그인
# GitHub Secrets에 저장된 인증 정보 사용
# ----------------------------------------
- name: Login to Docker Hub
uses: docker/login-action@v2 # Docker 공식 로그인 액션
with:
# Settings → Secrets에 저장된 값 사용
username: $
password: $
# ----------------------------------------
# Step 4: Docker 이미지 빌드
# 항상 latest 빌드, 릴리스면 버전 태그도 빌드
# ----------------------------------------
- name: Build Docker image
run: |
# latest 태그는 항상 빌드
docker build -t nayatrei3/nayatrei-app:latest ./app
# 릴리스(태그 push)인 경우에만 버전 태그 추가 빌드
if [[ "$" == "true" ]]; then
# 예: nayatrei3/nayatrei-app:v2.1
docker build -t nayatrei3/nayatrei-app:$ ./app
fi
# ----------------------------------------
# Step 5: Docker Hub에 이미지 업로드
# 빌드한 이미지들을 Docker Hub에 push
# ----------------------------------------
- name: Push to Docker Hub
run: |
# latest 태그는 항상 push
docker push nayatrei3/nayatrei-app:latest
# 릴리스인 경우에만 버전 태그도 push
if [[ "$" == "true" ]]; then
# 예: nayatrei3/nayatrei-app:v2.1
docker push nayatrei3/nayatrei-app:$
fi
# ----------------------------------------
# Step 6: 결과 출력 (로그에 표시)
# 어떤 이미지가 업로드되었는지 확인용
# ----------------------------------------
- name: Image info
run: |
# latest는 항상 출력
echo "Pushed: nayatrei3/nayatrei-app:latest"
# 릴리스인 경우 버전 태그도 출력
if [[ "$" == "true" ]]; then
echo "Pushed: nayatrei3/nayatrei-app:$"
fi
# ============================================
# 전체 흐름 요약:
# 1. main push → latest만 빌드/업로드
# 2. v2.1 tag push → latest + v2.1 빌드/업로드
# ============================================
파일은 기본적으로 yaml형식으로 작성하며, 내용은 깃허브 액션에서 정의한 고유의 문법을 따른다.
정확히 무슨 기능인지는 모르지만 키워드를 보고 유추하기에는 충분하다.
모르면 그냥 느껴
CI/CD 자동화
태그 버전관리 자동화
개발자가 개발을 하게 되면 하루에도 몇 번씩 깃 저장소에 올릴 일이 생기는데,
그에 따라 생기는 모든 이미지를 도커 허브에 태그를 지정하여 올릴 필요는 없을 것이다.
특정 시점의 버전만 태그를 따서 올리면 충분하기에 매번 덮어씌워지는 마지막 이미지를 알리는 latest 태그와,
v2.1처럼 따로 넘버링해서 올리고 관리하는 태그 두 가지 방식으로 사용할 수 있게 워크 플로우 파일을 작성했었다.
준비는 되었으니 이제 테스트를 해 보겠다.
daily update에 비해, weekly update는 태그를 생성하는 절차가 더 존재한다.
매일 하는 업데이트는 따로 버전 관리 없이 마지막 이미지만 저장하고,
주에 1번 하는 업데이트는 버전 명을 지정해서 따로 이미지를 저장하는 방식이다.
각 구문별 해석
git push origin main- main 브랜치의 최신 상태를 GitHub에 업로드한다.
git tag v2.1- 현재 커밋에 v2.1이라는 태그를 붙인다.
git push origin v2.1- v2.1 태그를 GitHub에 업로드한다.
빌드 테스트 자동화
현재 상태에서는 깃에 푸쉬하면 자동으로 도커 허브 저장소까지 반영이 되는데,
빌드가 실패하는 잘못된 코드가 올라간다면 문제가 생길 것 이다.
물론 개발자가 애초에 깃에 빌드가 실패하는 코드를 올릴 경우는 거의 없겠지만,
로컬 환경과 운영 환경의 차이에서 오는 문제로 생길 가능성은 충분히 예측할 수 있다.
그러므로 혹시 모를 상황에 대비하여, 깃에 푸쉬가 되면 자동으로 빌드 테스트를 진행하고 성공했을때만 도커 허브에 배포하는 기능을 추가해보겠다.
예상 실행 흐름
선 빌드 테스트를 수행하여 성공했을 경우에만 빌드하여 도커 허브에 업로드를 한다.
- 핵심: 버그 있는 코드는 배포 하지 않는다.
테스트 파일 작성
성공/실패 케이스 파일을 따로 만드는게 테스트하기에 편하기에 테스트용 파일을 준비했다.
- 파이썬 앱(/docker_github-actions/app/test/test_app.py)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# test/test_app.py
import pytest
import sys
import os
# app.py를 import할 수 있도록 경로 추가
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from app import app
@pytest.fixture
def client():
"""Flask 테스트 클라이언트 생성"""
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_home(client):
"""메인 페이지 테스트"""
response = client.get('/')
assert response.status_code == 200
data = response.get_json()
assert 'message' in data
assert 'status' in data
assert data['status'] == 'running'
def test_home_message_content(client):
"""메시지 내용 확인"""
response = client.get('/')
data = response.get_json()
assert 'Hello' in data['message'] or 'CI/CD' in data['message']
def test_response_format(client):
"""응답 형식 검증"""
response = client.get('/')
assert response.content_type == 'application/json'
- requirements.txt(/docker_github-actions/app/requirements.txt)
기존 파일에 파이썬 테스트 프레임워크 pytest==7.4.3 추가
- __init__.py(/docker_github-actions/app/test/__init__.py)
test 디렉토리를 파이썬 패키지로 인식시키기 위한 빈 init 파일 생성
빌드 테스트
pip install pytest
cd /docker_github-actions/app
pytest test/ -v
실행 전 pip install -r requirements.txt 명령어를 실행하여 패키지를 설치했다.
워크플로우 파일에 빌드 테스트 추가
워크플로우(/docker_github-actions/.github/workflows/docker-build.yml)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# ============================================
# Workflow 이름 (GitHub Actions 탭에 표시됨)
# ============================================
name: Docker Build and Push
# ============================================
# 트리거: 언제 이 workflow를 실행할지 정의
# ============================================
on:
push:
branches: [ main ] # main 브랜치에 push 시 실행
tags:
- 'v*' # v로 시작하는 태그 push 시 실행 (예: v1.0, v2.1)
# ============================================
# Jobs: 실행할 작업들
# ============================================
jobs:
# ============================================
# Job 1: 테스트 실행 (새로 추가)
# ============================================
test:
# Ubuntu 최신 버전 가상 머신에서 실행
runs-on: ubuntu-latest
steps:
# 코드 가져오기
- name: Checkout code
uses: actions/checkout@v3
# Python 환경 설정 (새로 추가)
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
# 의존성 설치 (새로 추가)
- name: Install dependencies
run: |
cd app
pip install -r requirements.txt
# 테스트 실행 (새로 추가)
- name: Run tests
run: |
cd app
pytest tests/ -v --tb=short
# 테스트 결과 요약 (새로 추가)
- name: Test summary
if: always()
run: |
if [ $? -eq 0 ]; then
echo "All tests passed!"
else
echo "Tests failed!"
exit 1
fi
# ============================================
# Job 2: Docker 빌드 & 배포
# ============================================
build:
needs: test # test job이 성공해야 실행 (추가됨)
# Ubuntu 최신 버전 가상 머신에서 실행
runs-on: ubuntu-latest
# ============================================
# Steps: 순차적으로 실행할 단계들
# ============================================
steps:
# ----------------------------------------
# Step 1: GitHub 저장소에서 코드 가져오기
# ----------------------------------------
- name: Checkout code
uses: actions/checkout@v3 # GitHub 공식 액션 사용
# ----------------------------------------
# Step 2: 버전 정보 추출
# 태그 push면 태그 이름(v2.1), 아니면 'latest'
# ----------------------------------------
- name: Get version
id: version # 다른 step에서 참조할 ID
run: |
# GITHUB_REF가 refs/tags/로 시작하는지 확인
if [[ $GITHUB_REF == refs/tags/* ]]; then
# refs/tags/v2.1 → v2.1 추출
VERSION=${GITHUB_REF#refs/tags/}
# 출력 변수 설정 (다른 step에서 사용)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "is_release=true" >> $GITHUB_OUTPUT
else
# 태그가 아니면 latest
echo "version=latest" >> $GITHUB_OUTPUT
echo "is_release=false" >> $GITHUB_OUTPUT
fi
# ----------------------------------------
# Step 3: Docker Hub 로그인
# GitHub Secrets에 저장된 인증 정보 사용
# ----------------------------------------
- name: Login to Docker Hub
uses: docker/login-action@v2 # Docker 공식 로그인 액션
with:
# Settings → Secrets에 저장된 값 사용
username: $
password: $
# ----------------------------------------
# Step 4: Docker 이미지 빌드
# 항상 latest 빌드, 릴리스면 버전 태그도 빌드
# ----------------------------------------
- name: Build Docker image
run: |
# latest 태그는 항상 빌드
docker build -t nayatrei3/nayatrei-app:latest ./app
# 릴리스(태그 push)인 경우에만 버전 태그 추가 빌드
if [[ "$" == "true" ]]; then
# 예: nayatrei3/nayatrei-app:v2.1
docker build -t nayatrei3/nayatrei-app:$ ./app
fi
# ----------------------------------------
# Step 5: Docker Hub에 이미지 업로드
# 빌드한 이미지들을 Docker Hub에 push
# ----------------------------------------
- name: Push to Docker Hub
run: |
# latest 태그는 항상 push
docker push nayatrei3/nayatrei-app:latest
# 릴리스인 경우에만 버전 태그도 push
if [[ "$" == "true" ]]; then
# 예: nayatrei3/nayatrei-app:v2.1
docker push nayatrei3/nayatrei-app:$
fi
# ----------------------------------------
# Step 6: 결과 출력 (로그에 표시)
# 어떤 이미지가 업로드되었는지 확인용
# ----------------------------------------
- name: Image info
run: |
# latest는 항상 출력
echo "Pushed: nayatrei3/nayatrei-app:latest"
# 릴리스인 경우 버전 태그도 출력
if [[ "$" == "true" ]]; then
echo "Pushed: nayatrei3/nayatrei-app:$"
fi
# ============================================
# 전체 흐름 요약:
# 1. main push → test 실행 → (성공) build 실행 (새로 추가)
# 2. v2.1 tag push → test 실행 → (성공) build 실행 (새로 추가)
# 3. 테스트 실패 시 → build 실행 안 됨 (새로 추가)
# ============================================
빌드 테스트 관련하여 새로 추가된 부분을 표시했다.
push 테스트
수정 내역이 깃에 올라가면 먼저 빌드 테스트를 수행하여 성공시에만 도커 허브에 업로드하는 로직을 추가하였으니 정상 작동되는지 테스트를 해보겠다.
깃 토큰에 workflow 권한을 추가해줘야 정상적으로 push가 가능해진다.
깃허브 액션에서 먼저 테스트, 빌드를 성공적으로 수행 후 도커 허브에 이미지 업로드가 되었다.
빌드 실패 테스트
테스트용 파이썬 앱 파일(/docker_github-actions/app/test/test_app.py)의 맨 끝에 실패하는 테스트용 로직을 추가했다.
1
2
3
def test_should_fail():
"""일부러 실패하는 테스트"""
assert 1 == 2 # 무조건 실패!
깃허브 액션에서 빌드를 실패하니, 도커 허브에는 이미지 업로드가 되지 않았다.
이제 빌드 성공 유무에 따른 도커 허브 업로드 자동화까지 적용하였다.
처음 목표는 여기까지였지만 현재 상태에서는 빌드 및 업로드 이후 서버 배포는 수동으로 해야 하는 상황이기 때문에,
좀 더 자동화의 의미에 맞으려면 서버 배포 자동화까지 구축하는게 맞다고 생각하여 해당 항목까지 진행해보도록 하겠다.
서버 배포 자동화
도커 허브 업로드 이후 서버 배포까지 자동화한다면 구성된 최종 CI/CD 파이프라인은 아래와 같이 될 것이다.
1. 개발자가 로컬에서 코드 수정
↓
2. 수정 내역 Git Push
↓
3. 자동 테스트 및 빌드
↓
4. 성공시 자동 도커 허브 업로드
↓
5. 서버 자동 배포
서버 자동 배포 과정은 간단하게 요약하면 서버 접속 -> 도커 허브 이미지 pull -> 도커 서버 restart가 된다.
SSH 키 생성
깃허브 액션이 서버에 접속하려면 키 또는 비밀번호가 필요하다.
사용자가 직접 입력하는게 아니기 때문에 비밀번호는 사용할 수 없다.
그런 이유로 자동화에 용이하며, 보안성도 더 높은 SSH키를 이용하도록 하겠다.
각 구문별 해석
ssh-keygen -t ed25519 -C "github-actions" -f ~/.ssh/github_actions -N ""- SSH키를 생성한다.
- 현대적인 암호화 알고리즘 ed25519를 선택한다.
- 키 파일 끝에 붙는 식별용 라벨을 github-actions로 지정한다.
- 키를 저장할 위치와 파일 이름을 ~/.ssh/github_actions로 지정한다.
- 키를 사용할때 입력할 비밀번호를 ““(없음)으로 설정한다.
- SSH키를 생성한다.
github_actions- 깃허브 액션이 소유할 개인키.
github_actions.pub- 서버에 등록할 공개키.
cat ~/.ssh/github_actions.pub >> ~/.ssh/authorized_keys- 공개키를 authorized_keys에 추가한다.
- 추후 서버에 접근시 해당 파일을 확인하여 접속이 허용된 공개키인지 확인하는 용도이다.
- 리스트에 있는 공개키라면 개인키를 검사하여 일치하면 통과시킨다.
- 공개키를 authorized_keys에 추가한다.
chmod 600 ~/.ssh/authorized_keys- authorized_keys 파일에 권한을 설정한다.
- 소유자(본인)만이 읽기+쓰기가 가능하다.
- authorized_keys 파일에 권한을 설정한다.
chmod 700 ~/.ssh- .ssh 디렉토리에 권한을 설정한다.
- 소유자(본인)만이 읽기+쓰기+실행이 가능하다.
- .ssh 디렉토리에 권한을 설정한다.
cat ~/.ssh/github_actions- 등록한 개인키를 출력한다.(복사하여 깃에 저장할 용도)
깃허브에 키 등록
깃허브 설정의 secrets and variables>Actions 에서 키를 등록했다.
- SERVER_HOST: 접속할 도커 서버의 ip주소
- SERVER_SSH_KEY: 복사한 개인키
- SERVER_USER: SSH 사용자명
서버에 배포 디렉토리 준비
- /app 디렉토리를 생성한다.
- /nayatrei-docker-compose/docker-compose.yml을 /app에 복사한다.
- 복사한 파일을 열어 수정한다.
1 2 3 4
services: app: # build: ./app ← 주석 처리 또는 삭제 image: nayatrei3/nayatrei-app:latest # ← 추가
Workflow에 배포 추가
/docker_github-actions/.github/workflows/docker-build.yml 파일을 열어 맨 끝에 deploy job을 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# ============================================
# Job 3: 서버 자동 배포
# ============================================
deploy:
needs: build # build 성공 후 실행
runs-on: ubuntu-latest
steps:
# ----------------------------------------
# Step 1: 서버에 SSH 접속 및 배포
# ----------------------------------------
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: $
username: $
key: $
script: |
echo "🚀 Starting deployment..."
# 배포 디렉토리로 이동
cd ~/app
# 최신 이미지 받기
echo "Pulling latest image..."
docker pull nayatrei3/nayatrei-app:latest
# 기존 컨테이너 중지 및 제거
echo "Stopping containers..."
docker-compose down
# 새 이미지로 시작
echo "Starting new containers..."
docker-compose up -d
# 상태 확인
echo "Checking status..."
docker-compose ps
echo "Deployment completed!"
# ----------------------------------------
# Step 2: 배포 결과 출력
# ----------------------------------------
- name: Deployment summary
run: |
echo "Deployed to: $"
echo "User: $"
echo "Image: nayatrei3/nayatrei-app:latest"
빌드 테스트 및 서버 배포 자동화 최종 결과
코드 수정한 부분이 반영되어 처음과 마지막의 curl 호출 결과 값이 다르다.
영상 내용을 요약하면 다음과 같다.
- 코드를 수정하고 깃에 푸시
- 깃허브 액션 자동 빌드 테스트 확인
- 도커 허브에 이미지 업로드 확인
- ps, curl 명령어로 서버 재배포 확인
다음 목표
이로써 도커의 기본부터 컴포즈, 도커 파일 최적화, 도커 허브, CI/CD 자동화(테스트 + 배포)까지 학습했다.
서버 배포나 도커 시스템 고도화는 아직 이른 느낌이고 학습 체감도 덜 될 것 같아, 다음번에는 지루하지만 이론 관련 학습을 해볼까한다.
실무에서 서버 자체를 구성하는것도 중요하겠지만 네트워크 설정, 보안, 성능 최적화도 중요할 거라고 생각해서 주제는 도커 네트워크 심화로 정했다.
도커에서 자주 쓰는 네트워크들의 차이점, 컨테이너 간 통신 원리 등 이론쪽으로 파고 들어 볼 예정이다.













