
- 실습1 - Hangman 서비스
- CI/CD
- Github Actions
이번 실습에서는 웹서비스를 Doker로 구동해보는 전체 과정을 다시한번 익히는 연습을 해보자.
hangman_web이라는 repo의 main에 코드가 머지될 때마다 다음을 수행하도록 구성
- 테스트 수행
- Docker Image 빌드
- 이를 위해 Dockerfile 부터 만들어볼 예정
- Docker Image를 Docker Hub로 push
위 과정을 Github repo에 Github Actions으로 구현해보자

시작하기 앞서 우리가 올릴 프로그램은 hangman 웹서비스로, https://github.com/learndataeng/hangman_web 링크에서 clone을 하던, fork를 하던해서 자신의 repository에 복사해두자.
이 hangman 프로그램은 flask를 사용하여 웹으로 노출하고 있다. 포트번호는 어디든 바인딩 가능하며 실행할 때 지정할 것이고 추가로 flask 관련 모듈 설치가 필요하다 (requirements.txt)
python3 -m flask run --host=0.0.0.0 --port=4000 명령어로 실행할 수 있고, 이 경우 app.py를 기본으로 사용한다.
다음은 Hangman repository의 구조이다.

일단 이 프로그램을 play-with-docker에서 실행해보자. 먼저 깃 저장소를 clone하고, pip3 install -r requirements.txt를 통해 관련 라이브러리를 설치한 다음, python3 -m flask run 명령어로 실행해보자.

그 후, OPEN PORT버튼을 누른 뒤 우리가 설정한 4000을 입력하면 Hangman 웹의 페이지를 볼 수 있다.


한가지 짚고 넘어가야 하는 부분이 있다. 만약 Docker 컨테이너로 포트 4000에 실행된 Flask app이 있다고 가정할 때, 이 app을 호스트 운영체제에서 접근할 수 있을까? 답은 '접근할 수 없다' 이다.

이러한 이유는 컨테이너는 완전히 별개의 공간이기 때문에 그 안에서 포트번호 4000을 열었다고 해서, 컨테이너 바깥에서 보이지는 않기 때문이다. 이를 해결하기 위해 우리는 Docker 컨테이너 내부 프로세스가 오픈한 포트번호를 외부로 노출해주는 작업을 해주어야 한다. 이를 Port-Mapping 이라고 한다.
방법은 docker run 수행시 -p 옵션을 사용하는 것이다. (EX : docker run -p 4000:4000 이미지이름)

이제 Dockefile을 만든 뒤 Docker Image로 빌드를 해보자. 작성한 Dockerfile은 다음과 같다. Dockerfile은 clone 한 Hangman_web Repository안에 만들어 주자. (같은 디렉토리 안에 app.py와 같은 파일이 있어야함)

Dockerfile이 작성이 완료되었다면 docker build 명령어를 통해시 이미지를 만들어주자.


docker run -p 4000:4000 hjjwa1234/hangman 명령어를 사용해서 잘 작동하는지 확인해보자
-d 옵션을 사용하면 백그라운드에서 실행하도록 명령할 수도 있다. 
docker push 명령어를 사용해서 만든 이미지를 Docker Hub에서 공유하고 웹브라우저에서 확인해보자.


play-with-docker 페이지에서 우리가 Docker Hub에 올린 이미지를 컨테이너로 실행시켜 보자. run 명령어를 실행 시 local에서 이미지가 없으면 자동으로 Docker Hub에서 이미지 파일을 찾기 때문에 자동적으로 pull 작업이 완료된다.

이후, OPEN PORT 버튼 누른 뒤 4000번 입력해주면 Hangman 페이지를 확인할 수 있다.


먼저 소프트웨어 빌드란 자신(혹은 팀)이 개발한 소프트웨어를 최종적으로 출시하기 위한 형태로 만드는 것을 말한다. 테스트가 빌드의 중요한 일부로 포함되고, 참여 개발자들이 많을수록 이는 더 중요하다.
개발이 끝나기 전부터 빌드를 하면 소프트웨어의 안정성이 증대한다는 장점이 있다. 이 장점을 살려서 코드를 고칠때마다 테스트를 진행하고, 기대한대로 코드가 잘 돌아가는지 확인하는 작업을 Continuous Integration이라고 한다
이런 방식으로 진행할 때 빌드가 실패했다는 것은 무슨 의미일까? 보통 새 코드의 커밋으로 인해 테스트가 실패하는 경우가 있다.
Git이나 Github와 같은 버전관리 툴을 사용하여 협업을 하는 경우 Push나 Merge 하는 시점이 CI/CD를 실행하기 위한 절호의 순간이다. 코드가 메인/마스터나 브랜치에 추가되는 순간 CI/CD를 트리거하는 방식으로 적용할 수 있다.
그래서 Github에서는 이를 Actions라는 기능을 통해 Workflow라는 이름으로 구현 가능하다.

Github Actions란 CI/CD를 Github 위에서 구현하기 위한 서비스로, 코드 테스트, 빌드, 배포 자동화 기능을 제공한다. 이를 Workflow라 부르며 아래 컴포넌트로 구성된다.
- Events
- Jobs
- Actions
- Runner
- Github hosted runners
- Self hosted runners
Workflow는 트리거 이벤트가 발생하면 시작되는 일련의 동작들을 지칭한다. 트리거 이벤트의 예들을 보자면 다음과 같은 것이 있다.
Github의 Repository를 보면 Actions 메뉴가 있는데, 여기서 workflow를 생성할 수 있다. yml 파일을 직접 생성하던가, 혹은 템플릿(CI Templates)을 선택 후 수정하는 방법이 있다. (Python Application 혹은 Docker Image)
앞서 우리가 작성했던 hangman 코드에 Github Actions 기능을 이용해서 테스트를 추가해보자.
push나 PR이 있는 경우 test.py를 실행할 예정
우리가 이번 실습에서 사용해볼 CI Templat은 Python Application이라는 템플릿이다.
기본으로 pytest를 테스트 프레임워크로 설치하여 (우리는 unittest로 작성되어 있음) 테스트 코드를 실행하고, 이외에도 Python code linting tool으로 falke8을 설치하여 문법 에러와 코딩 스타일을 체크할 예정이다.
flake8 : 파이썬 코드에서 에러나 코딩 스타일 등에서 이슈를 체크해주는 툴. (이런 툴을 Linting tool이라 부름)
테스트 코드는 다음과 같다. (test.py)
"""Unit test cases for hangman game."""
import unittest
import app as hangman
class HangmanTestCase(unittest.TestCase):
# def setUp(self):
#
# checkCorrectAnswer(correctLetters, secretWord)
def test_checkCorrectAnswer(self):
answer = hangman.checkCorrectAnswer("baon", "baboon")
self.assertTrue(answer)
def test_checkWrongAnswer(self):
answer = hangman.checkWrongAnswer("zebrio", "zebra")
self.assertTrue(answer)
def test_1(self):
answer = hangman.checkCorrectAnswer("bazn", "baboon")
self.assertFalse(answer)
def test_2(self):
answer = hangman.checkCorrectAnswer("", " ")
self.assertFalse(answer)
def test_3(self):
answer = hangman.checkCorrectAnswer("ZEBRA", "zebra")
self.assertFalse(answer)
if __name__ == "__main__":
unittest.main()
이제 자신의 Github의 hangman_web 레포지토리로 이동해서 Actions 탭에 들어가보자.

Configure 버튼을 누르게 되면 .github/workflows 아래의 python-app.yml 파일을 수정할 수 있는 에디터를 볼 수 있다.

기본 내용에서 살짝 다르게 변형해주자. 변경한 내용을 좀 해석해보면 다음과 같다.
# workflow 이름
name: Python application
# 트리거 이벤트 (main 브랜치를 대상으로 push와 pull_request 이벤트 발생 시 workflow 작동)
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
# workflow 내용
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.10
uses: actions/setup-python@v3
with:
python-version: "3.10"
# 라이브러리 설치
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with unittest
# 현재 디렉토리에서 서브 디렉토리를 다 뒤져서 test로 시작하는 모든 파일을 실행해라
run: |
python -m unittest discover -p 'test*.py'
commit changes 버튼을 누르면 .github/workflows 폴더 안에 만든 python-app.yml 파일이 생성된 것을 볼 수 있다.

이 workflow를 테스트 해보기 위해 readme.md 파일을 수정해서 main branch에 push 해보자. push를 하면 Actions 탭에서 Workflow가 실행된 것을 볼 수 있다.

안에 들어가서 자세한 정보를 보게 되면 flake8을 이용해서 문법 검사를 진행하고 unittst 탭에서 test.py의 테스트 5개가 성공적으로 완료된 것을 확인할 수 있다.

이제는 Hangman Github repo에서 Girhub Actions를 통해 Docker Image를 빌드하고 푸시하는 것을 구현해보자. 템플릿은
Docker Image라는 템플릿을 이용하자.
Docker 관련 스텝들을 살펴보면
docker logindocker builddocker push위의 과정을 .github/workflows/docker-image.yml파일에 기술하면 된다. (steps 밑에 name)
먼저 Docker Hub ID와 PASSWORD를 Github내에 저장해보자. Setting 탭에서 Security 항목의 Secrets and variables를 선택하면 ID와 PASSWORD를 변수로 저장해 놓을 수 있다. YML 파일 안에서는 아래로 접근할 수 있다.
${{secrets.DOCKER_USER}}${{secrets.DOCKER_PASSWORD}}
변수명과 값을 입력한 뒤 생성해주자

이제 Docker Image 템플릿을 이용해서 새로운 docker-image.yml 이라는 Workflow를 만들 차례이다.

Configure을 누른 뒤 파일 내용을 다음과 같이 변경해주자.
name: Docker Image CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: docker login
env:
DOCKER_USER: ${{secrets.DOCKER_USER}}
DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}}
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
- name: Build the Docker image
run: docker build --tag ${{secrets.DOCKER_USER}}/hangman:latest .
- name: docker push
run: docker push ${{secrets.DOCKER_USER}}/hangman:latest
이 Workflow yml파일을 만드는 것도 메인 브랜치에 push를 하는 것이기 때문에 commit changes를 누르면 만든 두가지의 workflow가 작동하는 것을 볼 수 있다.

Docker Hub에서도 정상적으로 push가 이루어졌는 지를 확인할 수 있다.

정말 제대로 완료가 되었는지를 알아보기 위해 이를 터미널에서 다시한번 확인해보자.

