Docker Build + Push + Alarm | Python Script

Seunghso·2023년 12월 15일

script

목록 보기
3/3
post-thumbnail

요약: Docker Image Build + Push + Alarm을 한 번에!

✏️ 작성 배경

  1. 클라이언트와 서버가 병행으로 개발하거나 버그 픽스를 하고 있는 상황.
    빠르게 도커 이미지를 build + push 후 클라이언트 개발자에게 알려주어야 한다.

  2. 빌드-푸시 명령어 입력 간의 대기시간이 길다
    Docker build 명령어 입력 -> (빌드 대기시간) -> Docker push 명령어 입력
    때문에 빌드하는 동안 다른 작업을 수행하지 못하거나 다른 작업을 수행하다가 빌드가 되었는지 계속 확인하고 docker push 명령어를 작성해야 한다.

  3. Push가 끝났다면 해당 업데이트를 클라이언트 개발자들에게 알려야 한다.

  1. docker build or push 명령어 입력 후
    a. 성공했다면 해당 업데이트를 클라이언트 개발자들에게 알려야 한다.
    b. 실패했다면 명령어를 입력한 사람에게 이를 알려야 한다.
  2. 클라이언트 개발자에게 업데이트 내용을 간략히 알려야 한다.

🚀 기능 요약

  1. 현재 디렉토리에서 Dockerfile을 찾아서 Docker Image를 빌드한다.

  2. Docker Image 빌드가 끝나면 Docker Image를 DockerHub에 푸시한다.

  3. 푸시가 끝나면 클라이언트 개발자들에게 이를 알린다. (지정된 이메일 주소로 이메일 발송)

  4. 빌드 혹은 푸시가 실패하면 이를 실행자에게 알린다. (경고음 출력)

✅ 사용 설명

  1. 환경 변수가 설정되어 있어야 한다. (DockerHub, Gmail 사용)
    vi ~/.zshrc 후 아래 환경변수 입력 -> source ~/.zshrc
# AutoBuild (ATB) 를 위한 환경 변수 모음
export ATB_DOCKERHUB_REPO={이미지를 푸시할 DockerHub의 레포지토리명 (이미지이름)}
export ATB_EMAIL_TITLE={이미지를 푸시할 DockerHub의 레포지토리명}
export ATB_EMAIL_SENDER_ADDRESS={Gmail 계정}
export ATB_EMAIL_SENDER_PASSWORD={Gmail 앱 비밀번호}
export ATB_EMAIL_RECEIVER_ADDRESS={이메일을 보낼 이메일 주소들(','로 구분)}
  1. Docker API 사용을 위해서 Docker 가 실행되어 있어야 한다.
docker.errors.DockerException: Error while fetching server API version: ('Connection aborted.', ConnectionRefusedError(61, 'Connection refused'))
  1. 스크립트 실행
    a. --help : 스크립트 설명
    b. --version | -v : 스크립트 버전 확인
    c. --no-email | -n : 이메일을 보내지 않는 옵션
    d. --linux | -l : 이미지 빌드에 --platform='linux/amd64' 옵션 추가


3. 스크립트를 실행하고 빌드할 이미지 버전을 입력하세요. (ex. latest, 1.2.5)

  1. 업데이트 설명을 입력하세요. (입력 종료: 빈 줄 입력)
  2. 실행 과정 중 에러가 발생할 경우 경고음이 발생하니 유의하세요!
전체 코드
import os
import io
import sys
import smtplib
import subprocess
import docker
from playsound import playsound
from math import sin, pi
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')


def parse_environment_variables(options):
   settings = {}
   settings['IMAGE_NAME'] = os.environ.get('ATB_DOCKERHUB_REPO')
   if settings['IMAGE_NAME'] is None:
       print('환경 변수 ATB_DOCKERHUB_REPO를 설정해주세요.')
       sys.exit(0)
   if options.no_email:
       return settings
   settings['EMAIL_TITLE'] = os.environ.get('ATB_EMAIL_TITLE')
   if settings['EMAIL_TITLE'] is None:
       print('환경 변수 ATB_EMAIL_TITLE가 설정되어 있지 않습니다.')
       print('이메일 제목이 "[Image Update] {ImageName}" 으로 설정됩니다.')
       settings['EMAIL_TITLE'] = '[Image Update] ' + settings['IMAGE_NAME']
   settings['SENDER_EMAIL'] = os.environ.get('ATB_EMAIL_SENDER_ADDRESS')
   settings['SENDER_PASSWORD'] = os.environ.get('ATB_EMAIL_SENDER_PASSWORD')
   settings['RECEIVER_EMAILS'] = list(map(str.strip, os.environ.get('ATB_EMAIL_RECEIVER_ADDRESS').split(',')))
   if settings['SENDER_EMAIL'] is None or settings['SENDER_PASSWORD'] is None or settings['RECEIVER_EMAILS'] is None:
       print('이메일 환경변수가 설정되어 있지 않습니다. 이메일을 보내지 않습니다.')
   return settings

email_address = ['min_gi1123@naver.com']
client = docker.from_env()
script_version = '[1.0]'
script_description = '\n현재 위치에서 docker image를 빌드하고 push합니다.\n' \
               '이미지 버전을 입력하고 설명을 입력하면 설정된 이메일들로 전송됩니다.\n' \
               '이미지 버전을 입력하지 않으면 자동으로 latest 버전으로 설정됩니다.\n'


def get_current_git_branch():
   try:
       branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"]).strip()
       return branch.decode('utf-8')
   except subprocess.CalledProcessError:
       return("현재 디렉토리는 Git 저장소가 아닙니다.")


def build_docker_image(dockerfile_path, tag, options):
   global client
   build_success = False
   try:
       if options.linux:
           generator = client.api.build(path=dockerfile_path, tag=tag, decode=True, platform='linux/amd64')
           print('Linux 이미지 빌드 시작')
       else:
           generator = client.api.build(path=dockerfile_path, tag=tag, decode=True)
       for chunk in generator:
           if 'stream' in chunk:
               msg = chunk['stream'].strip()
               if msg.startswith("Step"):
                   print(msg)
               if msg.startswith("Successfully"):
                   build_success = True
                   print(msg)
       if not build_success:
           raise Exception()
       print(f"\nDocker Image Build Success: {tag}\n")
   except Exception as e:
       print("Docker Image Build Fail, enter command 'docker system prune'")
       play_beef()
       sys.exit(0)
   finally:
       client.close()


def push_docker_image(tag, description):
   global client
   try:
       response = client.images.push(tag, stream=True, decode=True)
       for chunk in response:
           msg = ''
           if 'error' in chunk:
               raise Exception(chunk['error'])
           if 'status' in chunk:
               msg += chunk['status'].strip()
               if 'Pushing' in msg:
                   print('Pushing: ', chunk['progress'], end='\r')
                   continue
           if 'id' in chunk:
               msg += ': ' + chunk['id']
           print(msg)
       print(f"\nDocker Image Push Success: {tag}")
   except Exception as e:
       print(f"\nDocker Image Push Failed: {e}")
       play_beef()
       sys.exit(0)
   finally:
       client.close()


def send_email(settings, message, email_to):
   msg = MIMEMultipart()
   msg['From'] = settings['SENDER_EMAIL']
   msg['To'] = email_to
   msg['Subject'] = settings['EMAIL_TITLE']

   # 이메일 본문 추가
   msg.attach(MIMEText(message, 'plain'))

   # Gmail 서버를 통해 이메일 전송
   try:
       server = smtplib.SMTP('smtp.gmail.com', 587)
       server.starttls()
       server.login(settings['SENDER_EMAIL'], settings['SENDER_PASSWORD'])
       text = msg.as_string()
       server.sendmail(settings['SENDER_EMAIL'], email_to, text)
       server.quit()
       print(f'\n {email_to}에게 이메일이 성공적으로 전송되었습니다.')
   except Exception as e:
       play_beef()
       print(f'\n{email_to}에게 이메일 전송을 실패했습니다: {e}')


def parse_args():
   global script_version, script_description
   import argparse
   parser = argparse.ArgumentParser()
   parser.add_argument('-v', '--version',
                       help='스크립트 버전',
                       action='version',
                       version='%(prog)s ' + script_version + '\n' + script_description)
   parser.add_argument('-n', '--no-email', help='이메일을 보내지 않음', action='store_true')
   parser.add_argument('-l', '--linux', help='리눅스 이미지 빌드', action='store_true')
   return parser.parse_args()


def print_help(settings):
   len_box = 75
   len_description = 20
   print('# --help 옵션을 사용하면 도움말을 볼 수 있습니다.\n')
   branch = get_current_git_branch()
   reservation_email = os.environ.get('ATB_EMAIL_RECEIVER_ADDRESS')
   print('# 확인하세요!')
   print('#' * len_box)
   print(f'#     현재 브랜치: {branch}' + ' ' * (len_box - len(branch) - len_description) + '#')
   print(f'#     이미지 이름: ' + settings['IMAGE_NAME'] + (' ' * (len_box - len(settings['IMAGE_NAME']) - len_description) + '#'))
   print(f'#     이메일 예약: {reservation_email}' + ' ' * (len_box - len(reservation_email) - len_description) + '#')
   print('#' * len_box + '\n')


def read_image_version():
   try:
       print("\n버전을 입력해주세요(빈 칸 입력:'latest'):", end=" ")
       version = sys.stdin.readline().strip()
       if version == "":
           version = 'latest'
       return version
   except KeyboardInterrupt:
       sys.exit(0)


def read_image_description():
   try:
       print("\n설명을 입력해주세요 (종료: 빈 줄 입력)")
       description = ""
       while True:
           line = sys.stdin.readline()
           if line == "\n":
               break
           description += line
       return description
   except KeyboardInterrupt:
       sys.exit()


def send_result(tag, description, settings, options):
   if options.no_email:
       print('\n이메일을 보내지 않습니다.')
       return
   settings['EMAIL_TITLE'] += tag
   email_body = tag + ' Docker Image가 업데이트 되었습니다.' + '\n\n' + description
   for address in settings['RECEIVER_EMAILS']:
       send_email(settings, email_body, address)


def play_beef():
   playsound("warning.mp3")


def main():
   try:
       options = parse_args()
       settings = parse_environment_variables(options)
       print_help(settings)

       version = read_image_version()
       description = read_image_description()

       tag = settings['IMAGE_NAME'] + ':' + version
       build_docker_image(".", tag, options)
       push_docker_image(tag, description)
       send_result(tag, description, settings, options)
   except KeyboardInterrupt:
       print('\n\n스크립트 실행을 종료합니다.')
       sys.exit(0)


if __name__ == '__main__':
   main()
profile
Better than yesterday

0개의 댓글