TL;DR;

  • FastAPI를 사용하여 자동 요약 서비스 API를 개발하고 EC2 인스턴스에 배포
  • 깃헙과 EC2 서버를 연결하여 코드를 가져오고 miniconda 설치 필요
  • 깃헙 액션을 사용해 포스트 요약 생성 및 업데이트 작업 수행 가능

사이드 프로젝트 배경

  • 내 블로그 포스트 대부분이 긴 흐름이라 누군가 글을 읽는다면 피로를 많이 느끼지 않을까 싶음
  • 글이 긴 만큼 한 제목에 모든 내용을 담기 어려운 점도 있다
  • 마침 서쿼드 스터디에서 OpenAI API 기반 서비스를 구축해보는 공부를 하면서 이런 문제를 해결할 수 있지 않을까 싶었음
  • 블로그에 어떤 문서를 작성하면, 이 문서에 자동으로 요약본(TL;DR;)을 붙여서 업로드하는 요약 서비스를 만들고자 함
  • 현재 내 커리어와는 거의 관련 없는, 순수하게 재미와 호기심으로만 시작한 프로젝트다 (앞으로도 관련 없지 않을까..?)

세부 기획

  • 목적:
    • 블로그 포스트에 OpenAPI 사용한 요약본을 자동으로 붙여주는 서비스 개발
    • 내가 할 액션은 기존과 같이 포스트 작성 -> 깃헙 푸쉬만 해야함
  • 방법:
    1. 요약 서비스 API를 만들어야 하고
      • 굳이 API로 해야하는지? API를 직접 띄워보고 싶은 것이 컸는데, 하다보니 위 목적 중 최소한의 액션을 달성하기 위해선 API가 최적인 것 같음 (그 외엔 로컬에서 하는 방법이 있을 것 같은데, 수동으로 작업해야 할 부분이 많다고 판단)
      • API를 계속 띄워놓을 수는 없으니, 포스트 업로드 시에만 API를 서버에 띄우는 액션이 추가됨
      • API는 fastapi 사용하고, AWS EC2 인스턴스 띄워서 배포
    2. 위 API로 만들어낸 요약본을 포스트에 자동으로 붙이는 프로세스 구축해야 함
      • 작성한 포스트를 깃헙 블로그 레포에 푸쉬한 시점 이후로 자동으로 요약본이 생성, 붙여지고, 붙여진 포스트가 다시 커밋, 머지 되어야 함
      • 깃헙 서버 단에서 위 작업이 이뤄지는 것이 완전한 자동화에 가깝다고 판단, 깃헙 액션 사용 이러한 점 때문에 API를 로컬에 띄울 수 없고 EC2 기반 작업 필요함 (로컬에 띄울 시 깃헙 액션에서 API 호출 불가)
  • 어떤 상황에 어떻게 동작되어야 하는지
    • 새로운 포스트 업로드 시 -> 요약본 생성, 붙이기, 포스트 업데이트
    • 기존 포스트 수정 시 -> 기존 요약본 수정, 포스트 업데이트
    • 기존에 이미 작성한 포스트에 대해 -> 아무런 액션 없음
    • 포스트에 텍스트 외의 데이터가 포함된 경우 -> 포스트 데이터에서 텍스트만 필터링

구현 과정

LLM 중 ChatGPT에 대한 이론적인 설명도 같이 들어가면 좋을텐데, 이건 내용이 길어질 것 같아 나중에 별도 포스트에서 다루겠음

1. OpenAI API 사용 준비

  • OpenAI API 페이지 접속 해서 로그인
    • 첫 접속이라면 OpenAI API를 사용할 수 있는 포인트를 20$ 정도 주는 것 같던데, 나는 이미 계정이 있었고, 시간이 오래돼서 만료됨
    • 여러 API 중 Completion을 사용할 예정
  • 결제 (필요시)
    • Personal - Billing 에 들어가서 일부 금액 이미 충전해야 함 (선결제 방식임)
  • 토큰 발급 (결제 후 발급받아야 유효함)
    • 결제 완료 후 USER - API keys 탭에 들어가 ‘Create new secret key’ 클릭해서 API 토큰 발급 받음
    • 발급 받은 토큰은 최초 1회만 표시되니, 해당 모달에서 복사 후 개인적으로 저장하는 것이 좋음

2. FastAPI 기반 요약 API 개발

  • 내가 어떤 url에 json 데이터를 담아 post 요청을 보내면, 서버(백) 단에서 이 post 요청을 받아 요약 후 요약본을 반환하는 과정 필요
  • 이후엔 내가 post 요청을 보내는 것이 아니라, 깃헙 서버에서 post 요청을 보내는 방식으로 업데이트 필요
import openai
from fastapi import FastAPI
from pydantic import BaseModel  # pydantic은 post 메서드 사용할 때 필요

openai.api_key = "*"

def summarize(text):
    system_instruction = "assistant는 user의 입력을 bullet point로 반드시 3줄 요약해준다."

    messages = [{"role": "system", "content": system_instruction},
                {"role": "user", "content": text}]
    
    response = openai.ChatCompletion.create(model = 'gpt-3.5-turbo',
                                            messages = messages)
    
    result = response['choices'][0]['message']['content']

    return result

app = FastAPI()

class InputText(BaseModel):
    text: str

@app.post("/summarize")
def post_summarize(input_text: InputText):
    summary = summarize(input_text.text)

    return {"summary": summary}
  • 백엔드에 돌릴 스크립트는 위와 같이 작성했고, 이 API를 실행하려면 터미널에 아래 구문 실행시키면 됨
    • uvicorn [파일명]:app --reload
  • 이 API가 잘 동작하는지 swagger로 확인할 수 있음
    • 띄운 서버 주소 + /docs 로 확인 가능 (ex. 로컬에 포트 8000번으로 띄웠다면, 127.0.0.1:8000/docs 이동)
    • swagger 화면에서 직접 텍스트 입력해 요약되는지 확인 가능

3. AWS EC2 인스턴스에 위 API 띄우기

  • AWS 계정 만듦

  • ECS 대시보드 -> EC2 인스턴스 생성하면 위와 같은 화면 나타남
    • 인스턴스 시작 전 리전을 서울로 변경 (우측 상단)
    • OS는 우분투 프리티어
    • 인스턴스 유형은 성능을 결정하는 것 (t2.micro 가 프리티어)
    • 키 페어: 서버에 SSH 프로그램으로 원격 로그인할 때 사용되는 비밀번호. 숫자가 아닌 파일 개념
      • 키 페어 유형은 RSA
      • 프라이빗 키 파일 형식은 .pem
    • 네트워크 설정: 외부에서 이 서버에 접근하는 것을 제한하는 항목. 지금은 그냥 놔둠
      • 추후 streamlit 등에 외부에서 접근할 때 이 항목 조작 필요
    • 스토리지 구성: 서버가 사용할 스토리지 설정하는 것. 프리티어는 최대 30gb
    • 인스턴스 시작 하면 생성됨
  • 이제 이렇게 띄운 인스턴스에 직접 접근해볼 수 있는데, SSH 로그인 가능
    • pem 파일을 특정 경로에 옮김
    • 이 위치로 터미널에서 이동
    • ssh -i fc_chatgpt.pem ubunto@43.201.66.120
      • 퍼블릭 주소 붙인 것
    • 처음엔 에러. 퍼미션이 잘못 되었다고 함
    • chmod 400 fc_chatgpt.pem 을 실행하면 권한이 다르게 설정됨
      • change mode 인 것 같고.. 400은 뭐지
    • 여튼 다시 실행하면 로그인 됨
    • ls 하면 아무것도 안 뜸
    • pwd 하면 /home/ubuntu 정도로만 뜸
    • mkdir projects/ 로 프로젝트 경로 만듦
  • 이렇게 띄운 가상 서버에 위에서 작업한 백엔드 스크립트를 옮기면 됨
    • 단순히 복붙 보다는 스크립트 관리를 위해 git으로 작업

4. 깃헙 코드를 원격 서버에 옮기기

  • 인스턴스 서버 접근를 용이하게 하려면, 아래와 같은 파일(login_aws.sh) 작성 후
    • ``` #!/bin/bash

    ssh -i fc_chatgpt.pem ubuntu@43.201.66.120 ```

    • 이 파일에 실행 권한은 아래 코드 실행시켜서 부여 후
      • chmod +x login_aws.sh
    • 터미널에 ./login_aws.sh 실행하면 간단히 EC2 서버 접속 가능함
  • 깃헙 코드를 원격 서버로 가져오려면, EC2 서버와 깃헙 레포를 연결지어야 함
    • 원격 서버에서 생성한 SSH를 깃헙에 등록해야 함
    • 등록 후 인스턴스에서 git clone git@github.com:s-seo/fc_chatgpt.git
    • miniconda 설치 필요
  • 설치 후 uvicorn backend_summary:app --host 0.0.0.0 --port 8000 --reload 하면 아까 만든 API가 EC2 서버에서 동작하는 것
    • 특정 포트에서 동작하려면 포트를 외부에서 접근할 수 있게 권한 설정 필요. EC2 인스턴스 대시보드에서 설정 가능

5. 깃헙 액션 붙이기

  • 먼저 깃헙 액션이란? 소프트웨어 개발 시 필요한 워크플로우가 있을텐데(예를들어 코드 커밋 시 자동으로 빌드, 테스트하고 통과하면 클라우드 서비스에 자동으로 배포까지 하는 것) 이걸 자동화하는데 사용하는 일종의 플랫폼
  • 이 기능을 사용해 내 블로그 포스팅을 커밋할 때 자동으로 요약본을 붙이는 워크플로우를 자동화시킬 계획

내가 워크플로우를 만들면 위와 같은 UI로 실시간 확인할 수 있다.

  • 내 깃헙 블로그 레포 내 아래 py를 작성함
  • 깃헙 액션에 yml 파일을 등록해서 아래 py를 실행시키게끔 설정
  • 아래 과정이 필요함
    • 요약해야 하는 md 파일을 필터링하고
    • 필터링한 md 파일을 아래 py의 인풋으로 넣음
    • 인풋 데이터를 요약 API에 post 해서 요약본 받음
    • 받은 요약본을 원래 md 파일에 붙여넣음
    • 업데이트 된 md 파일을 다시 main branch에 commit, push 함
import os
import sys
import requests
import json

def summarize_file(file_path):
    with open(file_path, 'r') as f:
        post_content = f.read()

    length = 4000
    summarized_contents = []
    for i in range(0, len(post_content), length):
        response = requests.post('http://43.201.66.120:8000/summarize', json = {'text': post_content[i:i + length]})
        summarized_contents.append(response.json()['summary'])
    
    response = requests.post('http://43.201.66.120:8000/summarize', json = {'text': '\n'.join(summarized_contents)})
    summarized_content = response.json()['summary']

    front_matter, main_content = post_content.split('\n---')
    
    with open(file_path, 'w') as f:
        f.write(front_matter)
        f.write("\n---\n")
        f.write('TL;DR; (OpenAI API, github actions 기반 자동 요약문)')
        f.write('\n***\n')
        f.write(summarized_content)
        f.write('\n***\n')
        f.write(main_content)

if __name__ == "__main__":
    changed_files = sys.argv[1].split()
    print(changed_files)
    for file in changed_files:
        if file.endswith('.md') and 'docs/' in file:
            print(file)
            summarize_file(file)

= 받은 요약본을 원래 md 파일에 붙여넣을 때 위와 같이 직접 문서 포맷을 설정해야 하는데, 이 부분이 꽤나 흥미로웠다. 어떻게보면 서비스의 UI를 다루는 부분이라 볼 수 있으니까 간결한걸 좋아하는 내 성향을 잘 드러낼 수 있기 때문 아닐까

  • 이제 이렇게 만든 py 파일을 워크플로우에 등록시켜야 함
  • 워크플로우는 yml 파일에 기술하기 때문에 아래에 해당 py를 가져오는 부분을 확인할 수 있음
name: Auto Summarize Post
on:
  push:
    paths:
    - 'docs/**'
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v2
      with:
        fetch-depth: 0
        token: $
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.x'
    - name: Install dependencies
      run: |
        pip install requests
    - name: Get list of changed files
      id: getfile
      run: |
        echo "files=$(git diff --name-only $ $)" >> $GITHUB_ENV        
    - name: Run summarization script
      run: python summarize_post.py $
    - name: Commit and push if changed
      run: |
        git config --global user.email "baoro9394@gmail.com"
        git config --global user.name "s-seo"
        git add -A
        git diff-index --quiet HEAD || git commit -m "Summary Added" && git push

6. QA

  • 나름 일종의 서비스이기 때문에 의도한 대로 작동하는지 직접 테스트해서 개선시켜야 하는 점을 정리해봤다. (QA라고 할 수 있지 않을까..?)
기획테스트 결과액션
새로운 포스트 업로드 시요약본 생성, 붙이기, 포스트 업데이트 (Done)- OpenAI Completion API의 글자 수 제한으로 인해 4,000자로 나누어 요약
기존 포스트 수정 시기존 요약본 수정, 포스트 업데이트 (No)- 수정한 모든 포스트 인식
- 기존 요약본 확인 및 수정 필요
기존에 이미 작성한 포스트에 대해아무런 액션 없음- 수정하지 않은 기존 포스트에 대해선 그대로 유지
포스트에 텍스트 외의 데이터가 포함된 경우포스트 데이터에서 텍스트만 필터링 (No)- 이미지 등은 GPT 모델에서 자동으로 필터링됨
요약본을 안 붙이고 싶은 경우요약본 미부착- API 미실행 시 summary yml 에러 발생, 이메일로 에러 알림 전송

7. 결과

  • 요약본은 성공적으로 붙여진다. 다만 요약본을 붙이기 전 수동으로 예열시켜주는 작업이 필요하다.
  • 깃헙 블로그 포스팅 시 추가하는 작업
    • fc_chatgpt로 터미널 생성, ./login_aws.sh 실행해서 서버 접속
    • cd projects/fc_chatgpt 로 폴더 위치한 경로로 이동
    • uvicorn backend_summary:app --host 0.0.0.0 --port 8000 --reload 로 API 뛰우기
    • 앤드포인트는 http://43.201.66.120:8000/
    • 다시 깃헙 레포 있는 터미널로 돌아와서, 푸쉬
    • 깃헙 actions에서 정상적으로 summary 붙은 것 확인 후
    • 내 로컬 저장소에 다시 pull 해줘야 함 (summary는 깃헙 서버 단에서 작업한거라 로컬에는 안 붙어있음)
  • api 서버 끄는건 VSC 종료하면 알아서 셧다운 됨 (좀 더 건전한(?) 방법이 있을지)
  • pull 안하면 로컬, 원격 버전 달라져서 문제 생김. 그 떄 마다 fetch 시켜주고 있음.. 이런 휴먼 에러의 가능성을 남겨두는게 영 찝찝하다
    •   git fetch origin
        git merge origin/master
      

8. 프로젝트 결론 & 회고

  • 재미로 시작한 것 치곤 배운게 많았음
    • 그동안 API라는 개념을 뜬구름 잡듯이 알고 있었는데, 이번 스터디로 좀 더 명확히 알게된, 뜻밖의 수확(?)이 가장 큼
    • 사실 API 위에 API를 감싼 형태라 수박 겉핥기 느낌으로 구현한건데, API의 앞단, 뒷단까지 내가 직접 구현해보는 개발을 해보는 것도 재밌을 것 같음
    • 깃에 더 친숙해짐
  • OpenAI API 비용은 아직까진 그렇게 유의미하지 않음. 만약 이 서비스를 배포한다면… 그 땐 고려해야 할 것이 훨씬 더 많아지는게 귀찮다. 그렇게까지 할 프로젝트인가 싶음 (난 사업가는 아닌 것 같다)
  • 한번에 4,000자 제한을 좀 더 현명하게 해결 할 수 있을 것 같은데, 현재 구현한 방법은 아쉬운 것이 있음
    • md 파일의 텍스트를 순차적으로 4,000자 씩 끊어서 요약 후 요약된 여러 말뭉치를 다시 최종 요약하는 방법
  • 깃헙 액션으로 일종의 배치를 yml 구현하는 방식의 경우, 코드 수정 했을 때 결과 확인하기까지 약 3~4분 텀이 있는 것이 작업을 루즈하게 만듦
    • 로컬에서 하는 방법도 있던데 시도 안 함
  • API를 사용하는데서 그치지 않고 LLM을 이해해서 직접 요약 모델을 구현, 배포 해보는 것도 재밌을 것 같고, 로깅을 직접 심어서 로깅에 대한 이해를 높이는 것도 재밌을 것 같다
  • 로컬에 pull 시키는 것까지 자동화시킬 수 없을까?
  • 레슨런이라면…
    • 실무하면서 개발자와 소통할 일이 없진 않은데, 그 사람들의 사고방식(?)을 좀 더 잘 알게 된 것 같다