DevOps & MLOps

2023 오픈소스 컨트리뷰션 DevOps & MLOps (1) DevOps편 (지속적 통합, 컨테이너화)

jamie-lee 2023. 8. 5. 21:21

들어가기 전에


운이 좋게도 2023 오픈소스 컨트리뷰션 아카데미에 지원하여 참가 중이다. (자세한 프로그램 소개 ☞ 2023 오픈소스 컨트리뷰션 아카데미 (contribution.ac))
클라우드 서비스와 DevOps에 대해 배우고 싶다는 동기가 있었기 때문에, 김대우 멘토님이 진행하시는 "Git 활용 및 DevOps / MLOps"이라는 프로젝트에 참여하게 되었다.
본 과정에서는 파이썬(웹 서버, 머신러닝), Github Actions과 Azure 서비스를 이용한 CI/CD를 경험할 수 있다.
단계별로 진행되었는데 나는 파이썬과 Git, 머신러닝도 일부 (튜토리얼 정도) 다뤄본 경험이 있어서 바로 DevOps 과정으로 넘어갈 수 있었다.

기본적으로 스스로 도전 과제를 진행하되 모르는 부분이 있으면 팀원과 상의하고 멘토님에게 조언을 구할 수 있다.
벌써 3주 정도 지났는데, 너무나 재미있다. ❤️‍🔥

우선 DevOps가 무엇인지, 그리고 가장 중요한 우리가 왜 DevOps를 배우면 좋은지 짚어보고, 본격적으로 과제를 진행하면서 겪었던 이슈를 공유하려고 한다.

DevOps를 왜 알아야 하는가

"개발팀 vs 운영팀, 속도와 제어의 역설"이라는 화두로 시작한다.

속도와 제어의 역설이란 개발 및 운영 환경에서 종종 발생하는 문제를 설명하는 개념이다.
혁신을 위해서는 빠르게 고객의 니즈를 파악하고 제품에 이를 반영하는 것이 중요하다. 이는 개발팀이 주로 하는 일이다.

그러나 변화는 필연적으로 불안정성을 야기하며, 이는 시스템의 안정성, 보안, 및 성능을 유지하는 것에 중점을 두는 운영팀의 목적과 대치된다. 운영팀은 가능한 한 제어 가능한 상황을 만들기 위해 최선을 다한다.
즉 이 역설은 일반적으로 개발팀이 빠른 속도로 신속하게 변화하고 새로운 기능을 릴리즈하려고 노력하는 반면, 운영팀은 안정성과 제어를 유지하려고 노력하기 때문에 발생한다.

그러다보니 이 두 팀 사이에는 자연스러운 긴장감이 있다.
그러면 이러한 두 팀의 갈등을 최소화하고, 두 팀이 효과적으로 협력할 수 있게 만드는 방법이 없을까? (소프트웨어 개발도 결국은 사람이 제일 중요하다─고 멘토님께서 강조.)

그렇게 등장한 방법 중 하나가 DevOps 방법론이다. DevOps는 개발팀과 운영팀 간의 협력을 강조하며 빠른 출시 속도와 높은 운영 안정성 사이의 균형을 찾으려 한다.

DevOps란 무엇인가

현대적인 애플리케이션 배포
Pasted image 20230804222929.png

DevOps vs. 애자일 차이는?

애자일은 고객과 개발팀간의 빠르고 유연한 개발 방법론이다.
반면 데브옵스는 개발팀과 운영팀간의 개발 및 배포 프로세스를 최적화하는 방법론이다.

DevOps 프로젝트의 목표

  • flask를 이용한 간단한 앱을 하나 만들고,
  • Github Action과 Azure 서비스를 이용해
  • CI/CD를 구축한다

DevOps 구현하기


flask app 만들기

원래대로라면 flask 웹 서버를 만드는 것이 맞는데, 사실 웹 서버 구축 보단 DevOps를 경험하는 것이 더 주된 목표이기 때문에 나는 아래같은 간단한 파이썬 소스 코드를 실행하는 앱을 만들어 바로 CI/CD 구현하는 프로세스로 넘어갔다. (마지막에 배포 자동화 단계에서 flask 웹 서버를 제대로 구축하였다.)

# test deploy actions

def function():
    print("hi")
    return

def average(a, b):
    return (a + b) / 2

def main():
    function()
    print(average(1, 2))


if __name__ == '__main__':
    main()

"hi"와 숫자 "1.5"를 출력하는 간단한 프로그램이다.

2. “main.yaml” workflow 생성하기

참고 ☞ GitHub Actions 이해 - GitHub Docs
이제 해야할 것은 이 코드에 이상이 없는지 검사해야 한다. pytest를 이용해 test 코드를 작성하여 test 케이스를 잘 통과하는지, 그리고 flake8로 linting을 거치도록 한다.
그리고 이 검사를 거친 코드만 PR을 올릴 수 있으며, 리뷰를 받은 후 main 브랜치에 병합될 수 있다.
그리고 이를 Github Action을 이용해 자동화한다. 지금 말한 이 과정이 CI에 해당한다.

이를 수행하는 workflow를 정의한 yml 파일은 아래와 같다.

# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Python application

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

permissions:
  contents: read

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - 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 pytest
        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 pytest
      run: |
        python -m pytest tests


“.github/workflows/main.yaml” 경로로 바로 작성해보았다.
main 브랜치에 PR이 올라오거나 push될 때 이 workflow가 작동할 것이며, workflow는 job > step > action의 계층으로 이루어져 있다. Github Action에 대한 자세한 내용은 깃헙 액션 공식문서(GitHub Actions 이해 - GitHub Docs)를 참고한다.
위 변경 사항을 main에 푸시하면 저장소의 action에서 workflow가 돌아가는 것을 확인할 수 있다.

❗ISSUE: test with pytest

Pasted image 20230721115734.png

python -m pytest tests 명령어를 실행하면서 tests 라는 파일이나 디렉토리를 찾을 수 없다는 에러 메시지가 발생했다.
pytest를 써본 적이 없기 때문에 여기서부터 삽질이 시작되었다.

tests.py 파일을 일단 생성만 해놨는데 여전히 에러가 발생한다. 음… 파일이 아니라 디렉토리를 생성해야 하는 것 같았다.
vscode 상에서 디렉토리 pytest를 설치하고 동일한 명령어를 입력했더니 우선 코드4 에러는 발생하지 않는다.

그런데 이번에는 코드5 에러가 발생했다.

Pasted image 20230721130031.png

흠, 왠지 테스트를 제대로 세팅하지 않아서 그런 것 같다는 느낌이 온다.
검색해보니 코드 5 에러는 pytest가 실행할 테스트를 찾지 못해서 그렇다고 한다.
테스트 검색이 되려면 어떻게 해야하는지 규칙을 찾아보자.

[!NOTE] pytest의 테스트 검색 명명 규칙
By default, pytest follows a certain naming convention for discovering tests. It looks for files that are named test_*.py or *_test.py in the current directory and its subdirectories. Inside those files, it looks for test functions named test_* and test methods that are part of test classes named Test*.

명명 규칙이 문제였다.
제대로 테스트 메소드를 작성하면 좋겠지만, 역시나 테스트 코드 학습이 주된 프로젝트 목표는 아니기 때문에 우선 다음 디렉토리에 아래와 같이 겉모습만 갖춘 테스트 메소드를 구현하여 github actions이 제대로 작동하는지 살펴본다.

# tests/test_example.py

def test_example():
    return

Pasted image 20230721131103.png

main push하는 경우, PR 올리는 경우 모두 workflow 잘 동작한다.

branch protection rule 설정하기

이전 단계에서 main에 바로 변경사항을 push 했다. 물론 나 혼자 사용하는 연습용 저장소이다보니 여기서 큰 문제는 없지만, 대부분 협업을 위한 저장소에서는 main에 직접 푸시하지 못하게 막아둔 경우가 많다. 알수 없는 코드를 함부로 메인에 push 했다가 어떤 일이 생길지 모른다. 브랜치 보호 규칙이 필요한 이유이다.

Pasted image 20230721131853.png

두 가지 규칙을 설정한다.

  1. 병합하기 전에 PR 올리기
    • 1명 이상의 approval 받기
  2. 병합하기 전에 status check 받기
    • 메인 브랜치에 올린 PR이 가장 최근 코드로 테스트 받도록 요구한다. 적어도 하나의 status check이 활성화되기 전까지는 효과가 없다.

이 외에도 브랜치를 read-only로 설정하는 규칙, linear history를 요구하는 규칙 등이 보인다.

이슈 템플릿, PR 템플릿 지정하기

PR을 올려야만 코드를 병합할 수 있게 규칙을 정했으므로 PR을 작성하기 위한 PR 템플릿을 지정한다. 나는 겸사겸사 심심해서(?) 이슈 템플릿도 지정해주었다.
About issue and pull request templates - GitHub Docs
위 깃헙 가이드를 잘 따라하면 어려움이 없다.

로컬 개발 머신에 Docker 이미지 만들고 테스트하기

  1. github에 issue를 만들고 branch를 파서 작업한다.
  2. dockerfile을 이용해 도커 이미지 빌드를 구성한다.

여기서부터는 나에게 이전에 가본 적 없는 생소한 길이다.
우선 이전에 다른 예제에서 작성해본 적 있었던 dockerfile + CI를 위해 위에서 작성한 workflow yaml 파일을 참고하여 아래처럼 구성해봤다. (불필요한 부분이 많아 추후 수정함.)

### 최종본 아닙니다. 

FROM ubuntu:latest
LABEL maintainer="EM Lee <100mgml@gmail.com>"
COPY . .

# install python
RUN apt-get update \
    && apt-get install -y python3

# install dependency
RUN python -m pip install --upgrade pip \
    && pip install flake8 pytest 

# lint with flake8 
RUN flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
RUN flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics

# Test with pytest 
RUN python -m pytest tests

# start app.py
CMD ["python", "app.py"]

❗ISSUE 도커 빌드 에러

Pasted image 20230721171231.png
python: not found 라니!
apt-get 명령어로 파이썬을 설치하는 레이어는 성공적으로 수행됐는데 어째서일까?

혹시 몰라 python3으로 명령어를 변경했다. (우분투에서 python 명령어는 2.x 버전에 연결되어 있다고 한다.)
그러자 이번에는 pip module not found가 뜨길래 아래처럼 python3-pip 모듈도 설치하는 명령어를 추가했다.

# install python
RUN apt-get update \
    && apt-get install -y python3 python3-pip
    

성공적으로 잘 된다.
하지만 또 다른 오류

❗ISSUE flake8 테스트 실패

Pasted image 20230721173945.png
flake8이 내 프로젝트 폴더가 아닌 python3.10 폴더의 파일을 linting 하고 있다. 뭔가 이상하다.
일단 의심되는 것은 workdir 와 copy 명령어를 설정하지 않은 것.
그래서 루트 디렉토리에서 flake8이 헤매고 있는 것 같다.

따라서 아래와 같이 workdir, copy 명령어를 위쪽에 추가한다.

WORKDIR /ossca-devops
COPY . /ossca-devops

Pasted image 20230721175334.png

빌드가 성공적으로 마무리됐다. 😊
docker run osscadevops:latest로 컨테이너 실행도 잘 되는 것을 확인.

자! 그리고 이맘 때쯤, 첫 프로젝트 오프라인 모임을 가졌고 CI/CD가 무엇인지 자세히 배울 기회가 있었다. 그리고나서 linting과 test는 이미 PR 올리면서 진행했기 때문에 굳이 컨테이너로 말면서 한 번 더 그 과정을 거칠 필요가 없겠다는 것을 깨달았다. (처음에 혹시나…? 하는 마음에 포함했었다.)
결과적으로 해당 부분을 삭제하고 dockerfile을 아래처럼 다시 작성했다.

FROM ubuntu:latest
LABEL maintainer="EM Lee <100mgml@gmail.com>"
WORKDIR /ossca-devops
COPY . /ossca-devops

# install python 
RUN apt-get update \
    && apt-get install -y python3 python3-pip

# install dependency
RUN python3 -m pip install --upgrade pip 

# start app.py
CMD ["python3", "./src/app.py"]

이슈를 해결한 것이 무색해졌지만 저 에러를 겪지 않았다면 WORKDIRCOPY를 무시하고 지나갔을 수 있기 때문에 어쨌든 수확이라고 생각한다.

마치며

다음 편에서는 깃헙 액션으로 Azure 서비스로 자동 배포하는 과정을 진행한다.