Jello's development blog

Jello's development blog

Fabric을 이용한 Django 프로젝트 배포 자동화

MAC을 사용하였고, 배포 서버는 Ubuntu으로 하였다.

들어가며

개발을 하면서 시간이 많이 걸리고 귀찮은 작업 중 하나가 바로 배포(Deploy)작업이다. Python의 패키지 중에서 shell 명령어를 실행할 수 있게 해주는 패키지가 있는데, 바로 Fabric이다. 이것으로 꽤나 유용하게 배포를 자동화할 수 있는데, 코드를 잘 짜면 배포에 걸리는 시간을 많이 단축시킬 수 있다. “파이썬을 이용한 클린 코드를 위한 테스트 주도 개발”이라는 책에 있는 코드를 하용하여 우분투 서버에 배포를 자동화하는 방법을 알아보도록 하자.

Fabric을 이용한 배포 자동화 함수 만들기

먼저 Python에 Fabric을 설치한다. Fabric은 2.5 ~ 2.7 버전의 패키지이기 때문에 범위 안의 가장 최신 버전인 2.7.12버전에서 설치하도록 하자.

$ pip install fabric

Fabric은 하나의 프로젝트에 종속되지 않고 여러 곳에서 쓰이기 때문에 virtualenv에 설치하지 않는다.

이제 Fabric으로 실행할 코드를 작성해보자. 파일 이름은 fabfile.py로 한다. Fabric 기본적으로 fabfile.py라는 파일을 찾아서, 그 안의 특정 함수를 실행한다.
Fabric의 명령어는 다음과 같다.

$ fab [FUNCTION_NAME]:[PARAMETER]=[VALUE]
먼저 사용할 함수나 변수를 설정한다.
from fabric.contrib.files import append, exists, sed
from fabric.api import run, env, local

REPO_URL = 'https://github.com/example/example.git

REPO_URL은 자신이 배포할 repository URL로 해준다.

다음 작업으로 코드를 배포할 디렉토리를 만들어 보자.
def _create_directory(site_folder):
    run('mkdir -p {0}'.format(site_folder))

run함수는 Fabric에서 가장 많이 사용되는 함수로, 쉘 명령어를 실행시킨다. 간단하지만 가장 중요한 함수이다.

mkdir-p 옵션은 상위 디렉토리까지 생성해주는 유용한 옵션이다. 예를 들어, mkdir -p foo/bar/ 명령은 foo라는 디렉토리까지 생성한다.

다음으로는 소스 코드를 다운로드하는 코드를 작성해보자
def _get_latest_source(site_folder):
    if exists(site_folder + '/.git'):
        run('cd {0} && git pull origin master'.format(site_folder))
    else:
        run('git clone {0} {1}'.format(REPO_URL, site_folder))

    current_commit = local("git log -n 1 --format=%H", capture=True)
    run('cd {0} && git reset --hard {1}'.format(site_folder, current_commit))

local함수는 서버 주소가 아니라 로컬에서 명령을 실행하는 함수이다. capture=True로 인해서 결과가 current_commit이라는 변수에 저장된다.

프로젝트 폴더에 이미 .git파일이 존재한다면 pull을 하고, 그것이 아니라면 clone한다.

그리고 reset 명령어를 이용하여 로컬에서 체크아웃한 상태의 가장 최근 커밋으로 서버의 소스를 되돌린다.

Django에서의 추가 자동화 함수 만들기

Django 프로젝트가 아니라면 이 장을 생략해도 좋다.

Django 프로젝트는 위의 내용에 추가로 여러가지 설정을 변경해주어야 한다.

프로젝트 내에 provision에 필요한 디렉토리를 생성해야 한다.
def _create_directory_structure(site_folder):
    for subfolder in ('database', 'static', 'virtualenv'):
        run('mkdir -p {0}/{1}'.format(site_folder, subfolder))

database, static, virtualenv 세 개의 디렉토리를 프로젝트 디렉토리 안에 생성하는 코드이다. 반드시 모두 생성할 필요는 없고, 자신의 프로젝트에 맞게 설정하면 된다.

settings.py파일을 수정한다.
def _update_settings(source_folder, site_name):
    settings_path = source_folder + '/[APP_NAME]/settings.py'
    sed(settings_path, "DEBUG = True", "DEBUG = False")
    sed(settings_path, 'ALLOWED_HOSTS =.+$', 'ALLOWED_HOSTS = ["{0}"]'.format(site_name))
    secret_key_file = source_folder + '/[APP_NAME]/secret_key.py'
    if not exists(secret_key_file):
        chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)'
        key = ''.join(random.SystemRandom().choice(chars) for _ in range(50))
        append(secret_key_file, "SECRET_KEY = '{0}'".format(key))
    append(settings_path, '\nfrom .secret_key import SECRET_KEY')

살짝 복잡해보이는 코드인데, 하나하나 짚어 이해해보자.
Django의 각 앱에 있는 settings.py를 수정하는 코드인데, 여기서 sed함수가 사용된다. 이 함수는 특정 파일에 있는 문자열이나 정규표현식을 찾아서 바꿔주는 기능을 한다. DEBUG항목을 TRUE에서 FALSE로 바꾸고, ALLOWED_HOSTS에서 해당 서버의 사이트 이름을 추가한다.

Django에선 암호화(쿠키와 CSRF보호)를 위해서 SECRET_KEY를 이용한다. 당연하게도, 이 키를 이용할 때는 repository에 올라가 있는 키와 다른 키를 사용해야만 한다.append함수는 파일의 끝에 새로운 내용을 추가해주는 기능을 한다. 파일이 없으면 새로 만들고 추가한다. 코드는 secret_key.py가 없으면 새로운 키를 만들고 secret_key.py에 넣는다. 그리고 마지막으로 settings.py에 sys.path가 아닌 로컬 모듈에서 SECRET_KEY를 import하도록 한다.

virtualenv 설정을 자동화한다.
def _update_virtualenv(python_version, python_version_folder, virtualenv_folder, site_folder):
    if not exists(python_version_folder + '/bin/pip'):
        run('pyenv install {0}'.format(python_version))
    if not exists(virtualenv_folder + '/bin/pip'):
        run('pyenv virtualenv {0} [ENV_NAME]'.format(python_version))
    run('{0}/bin/pip install -r {1}/requirements.txt'.format(virtualenv_folder, site_folder))

virtualenv가 설치되지 않을 경우를 대비해서 특정 virtualenv를 설정한다. 먼저 프로젝트에 사용되는 Python의 버전을 설치해주고, 그 뒤에 특정 virtualenv를 설치해준다. 그리고 프로젝트의 패키지를 명시해놓은 requirements.txt를 참조해 패키지를 설치한다. 코드 내의 [ENV_NAME]에 자신의 virtualenv 이름을 넣어주면 된다.

database, static 파일을 생성한다.
def _update_static_files(source_folder, virtualenv_folder):
    run('cd {0} && {1}/bin/python manage.py collectstatic --noinput'.format(source_folder, virtualenv_folder))

def _update_database(source_folder, virtualenv_folder):
    run('cd {0} && {1}/bin/python manage.py migrate --noinput'.format(source_folder, virtualenv_folder))

collectstatic 명령어로 static파일을 생성하고, migrate 명령어로 database 관련 파일을 생성한다.

배포 함수 만들고 배포하기

지금까지 각각의 역할을 하는 함수들을 만들었는데, 이제 이들을 조합하여 엄청난 자동화를 해보자.

def deploy():
    python_version = '3.5.2'
    python_version_folder = '/home/{0}/.pyenv/versions/{1}'.format(env.user, python_version)
    virtualenv_folder = '/home/{0}/.pyenv/versions/[ENV_NAME]'.format(env.user)
    site_folder = '/home/{0}/sites/{1}'.format(env.user, env.host)
    source_folder = site_folder + '/[APP_NAME]'
    _create_directory(site_folder)
    _get_latest_source(site_folder)
    _create_directory_structure(site_folder)
    _update_settings(source_folder, env.host)
    _update_virtualenv(python_version, python_version_folder, virtualenv_folder, site_folder)
    _update_static_files(source_folder, virtualenv_folder)
    _update_database(source_folder, virtualenv_folder)

사실 변수 몇개 선언하고, 위의 함수들을 조합한 것에 불과하다. 하지만 이들을 모음으로써 하나의 배포 자동화 코드가 만들어졌다.

가장 위에 Python, virtualenv의 버전과 디렉토리명을 선언해준다. 이는 _update_virtualenv함수에서 버전을 설치할 때에 참조하게 될 것이다.

site_foldersource_folder를 선언하는데, env.userenv.host는 이 파일을 Fabric으로 실행할 때에 입력해준다. 여기서는 프로젝트 디렉토리의 이름을 호스트 이름으로 해주었다.

Django 프로젝트를 사용하지 않는다면 Python과 관련된 변수와_create_directory_structure이후로는 제거해야 한다.

이제 명령어로 배포해보자!

$ fab deploy:host=[USER_NAME]@[HOST_NAME]

[process...]

여기서 [USER_NAME][HOST_NAME]이 각각 env.userenv.host로 들어가게 된다.

이제 굳이 쉘에서 shell 명령어를 이용하여 원격으로 접속한 다음에 일일이 베포 명령어를 하나 하나 쳐가면서 배포하지 않아도 된다!

마치며

Fabirc은 2.5 ~ 2.7 버전의 패키지이기 때문에 현재 최신 버전인 3.5 버전에서는 쓰지 못하게 되었다. 버전 문제를 보완하고자 Fabric을 대신하여 invoke라는 패키지가 등장하긴 했지만, 아직 정보도 부실하고 기능이 없어 많이 쓰지는 않는 실정이다. ‘Fabric은 버전이 너무 낮아!’ 하면서 invoke로 바꾸려는 시도를 하다가 엄청난 삽질을 했다..

버전이 낮더라도, Python으로 만들어진 프로젝트가 아니라 쉘 명령어가 필요한 곳이면 어디에나 사용할 수 있기 때문에 아주 유용한 도구인 것 같다.