2부에서는 DB 설치 및 Django 세팅을 다룰 예정이다. PostgreSQL 9.3.8, Django 1.8.2 버전을 기준으로 작성했다. 작성하면서 Digital Ocean의 튜토리얼을 참고했다.

1. DB 설치 및 접속

  1. sudo apt-get install postgresql postgresql-contrib로 postgresql를 설치한다.
  2. virtualenv에 pip install psycopg2로 PostgreSQL 파이썬 커넥터를 설치한다. 오류가 발생하는 경우 apt-get으로 libpq-dev 패키지를 설치한다.
  3. sudo -u postgres psql로 SQL 환경을 시작할 수 있다.

유용한 명령어들

  • \? 명령어 도움말을 보여준다.
  • \l 데이터베이스 목록을 보여준다.
  • \dt 테이블 목록을 보여준다.
  • \dg 유저 목록을 보여준다.
  • \c DATABASE_NAME 데이터베이스에 접속
  • \q SQL Environment 종료

template database

PostgreSQL을 설치하면 기본적으로 postgres, template0, template1 데이터베이스가 있다. PostgreSQL에서는 데이터베이스를 만들 때 기본적으로 template1을 복제해서 만들어지기 때문에 여기에 모든 데이터베이스에 적용할 확장 기능을 설치하는 식으로 이용한다. template0은 데이터베이스 초기의 순수한 상태 템플릿으로 이용하며, template0을 복제할 때는 DB의 인코딩이나 locale을 세팅할 수 있다.

2. 사용자 계정 생성

  1. CREATE DATABASE toytest;로 toytest 데이터베이스 생성. #
  2. CREATE USER toytest with PASSWORD 'password';로 계정 생성. 패스워드는 랜덤 제너레이터를 사용했다. #
  3. GRANT ALL PRIVILEGES ON DATABASE toytest TO toytest;로 toytest DB에 대한 권한을 toytest Role에 할당한다.

3. Django 세팅 파일 수정

Django를 VCS로 관리할 때 비밀 정보들이나 사이트마다 다른 로컬 세팅들은 VCS에 포함해서는 안 된다. 이런 정보들은 local_settings.py에 옮겨서 저장하자.

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'SECRET_KEY'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False

ALLOWED_HOSTS = ['toytest.qwaz.io',]

# Database
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'toytest',
        'USER': 'toytest',
        'PASSWORD': 'PASSWORD',
        'HOST': 'localhost',
        'PORT': '', # default port
    }
}

PostgreSQL의 기본 포트는 5432고, 기본 포트를 사용하는 경우 비워 놓아도 된다.

settings.py에서는 겹치는 설정을 제거해주고 LANGUAGE_CODE와 TIME_ZONE을 수정한다. LANGUAGE_CODE는 프로젝트 성격에 따라 en-us 또는 ko-kr로 설정하고, TIME_ZONE은 서버가 있는 Asia/Tokyo로 설정해 주자.

설정을 마치고 settings.py에 from .local_settings import *를 추가한다.

수정한 프로젝트를 다시 읽어오기 위해서는 여러가지 방법이 있는데 touch toytest_uwsgi.ini가 가장 편리하다. Emperor Mode로 uWSGi를 실행시켜두었기 때문에 ini 파일의 수정을 감지해 uWSGI가 다시 로드된다.

DB 연결이 끝나면 python manage.py migrate를 실행해 데이터베이스 연결이 잘 되었나 확인한다.

동기

최근 도메인을 산 김에 토이 프로젝트로 요즘 유행하는 익명 질답 서비스를 간단하게 만들어보려고 했다. Bitnami에서 클릭 몇 번으로 스택을 구성할 수 있는 것처럼 보여서 AWS 프리 티어랑 합쳐서 두 세 시간 정도 삽질을 했다. 무료 티어에서는 micro급 EC2 인스턴스를 무료로 제공해 주는데, Auto Scaling이 필요한 수준은 돼야 AWS의 장점을 느낄 수 있을 것 같고 이 정도로는 다른 VPS랑 비교해서 딱히 장점이 느껴지지 않았다. 그래서 차라리 오래 쓸 목적으로 싸고 적당한 VPS 하나를 맞추고자 가상서버 호스팅 비교글을 보고 가격 대비 트래픽과 하드 용량이 좋은 ConoHa에서 제일 싼 서버를 하나 맞췄다.

예전에 학교 서버에서 급식 평가 사이트 돌릴 때 서버 세팅에서 삽질을 많이 했었기 때문에 이번에는 세팅하면서 삽질 과정을 포스팅에 메모해 놓기로 했다. 서버 첫 설치부터 시작해서 삽질의 과정을 내가 나중에 다시 보면 기억날 수준으로만 간단하게 정리해 보았다. nginx + Django + uWSGI로 디플로이 할 일이 제일 많을 것 같아서 이 세팅으로 저장해 두었고, 사용하는 프레임워크 따라 적절히 변경해서 사용하면 된다. 작성하면서 이 글을 참고했다.

1. 설치

처음 설치하고 나서 root 계정으로 필요한 것들을 깔아준다. Ubuntu 14.04 기준이고 대부분의 작업에 python3를 사용한다.

$ apt-get update
$ apt-get upgrade
$ apt-get install build-essential zsh git python3-dev python3-pip nginx lrzsz
$ pip3 install virtualenv

2. SSH 키 생성하기

  1. useradd qwaz로 유저를 추가한다.
  2. groupadd admin으로 어드민 그룹 생성.
  3. usermod -a -G admin qwaz로 유저를 어드민 그룹에 추가.
  4. login qwaz로 로그인 후 sudo 되는지 테스트.
  5. XShell 등에서 사용자 키를 생성하고 공개키를 .ssh/authorized_keys 위치에 업로드한다. 디렉터리가 없는 경우 생성하고 권한은 700으로 준다.
  6. 로그아웃 후 Key 사용해서 로그인 되는지 테스트.

3. zsh 세팅하기

  1. curl -L https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh | sh로 oh-my-zsh 설치
  2. chsh -s /bin/zsh로 기본 셸을 zsh로 변경 후 터미널 재접속.
  3. .zshrc를 수정해 테마나 플러그인을 취향대로 변경. bullet-train이 괜찮더라.

4. virtualenv 세팅

개발용 계정은 하나를 쓰고 projects 디렉터리 내부에 개발하는 프로젝트를 관리할 것이다. 프로젝트마다 계정을 생성해도 되는데 개인적으로는 이 방식이 편하게 느껴진다.

프로젝트 디렉터리가 없는 경우 mkdir projects로 생성하고 ~/projects 디렉터리로 이동한다.

toytest라는 프로젝트를 예시로 진행해보겠다.

$ virtualenv toytest
$ cd toytest
$ source bin/activate
$ pip install django
$ cd django

virtualenv toytest로 가상 환경을 만든다. 이렇게 만든 가상환경은 source toytest/bin/activate로 활성화하고 deactivate로 비활성화 할 수 있다.

virtualenv를 켜기 전에는 파이썬 버전에 따라 pip, pip3로 명령어가 다르지만 virtualenv를 python3로 설치했기 때문에 활성화 후에는 그냥 pip 명령어만 입력해도 python3 버전으로 작업할 수 있다. pip가 사용중인 파이썬 버전은 pip -V로 확인할 수 있다.

5. Django 프로젝트 생성

서버에서 바로 django-admin startproject toytest로 시작해도 되고, 로컬에서 생성한 뒤 git clone 등을 이용해 가져오게 설정해도 된다. 과정은 거의 동일하기 때문에 서버에서 바로 생성하는 방법으로 설명하겠다.

~/projects/toytest 디렉터리에서 django-admin startproject toytest 명령으로 toytest 프로젝트를 시작하자.

6. nginx 세팅

생성한 Django 프로젝트를 수정하기 전에, ~/projects/toytest에 로그나 소켓 등을 먼저 설정한다.

$ mkdir misc log
$ cd log
$ touch access.log error.log toytest.log
$ cd ..
$ cd misc

misc 디렉터리에 toytest_nginx.conf를 만들고 다음과 같은 내용을 추가하자.

server {
        # the port your site will be served on
        listen 80;

        # the domain name it will serve for
        server_name     toytest.qwaz.io;
        charset         utf-8;

        access_log      /home/qwaz/projects/toytest/log/access.log;
        error_log       /home/qwaz/projects/toytest/log/error.log;

        location / {
                include /etc/nginx/uwsgi_params;
                uwsgi_pass unix:///home/qwaz/projects/toytest/misc/toytest.sock;
        }
}

이 파일을 symlink로 nginx 디렉터리에 추가한다.
sudo ln -s ~/projects/toytest/misc/toytest_nginx.conf /etc/nginx/sites-enabled/

7. uWSGI 세팅

여기를 참고했다.

우선 virtualenv를 잠시 꺼두고, pip3로 uWSGI를 system-wide로 설치한다.

$ deactivate
$ sudo pip3 install uwsgi

virtualenv를 다시 켜고 python manage.py runserver 0.0.0.0:8000로 Django를 돌려보자. 웹브라우저에서 toytest.qwaz.io:8000로 접속해 접속이 되는지 테스트 해본다.

잘 동작했다면 이번에는 uwsgi --http :8000 --module toytest.wsgi -H ~/projects/toytest로 uWSGI를 사용해 다시 한 번 테스트 해본다.

여기까지 성공했다면 이제 아래 toytest_uwsgi.ini파일을 misc 폴더에 만들자. 설정 파일 만들 때는 여기를 참고했다.

[uwsgi]

# Django-related
chdir           = /home/qwaz/projects/toytest/toytest
module          = toytest.wsgi

# virtualenv
home            = /home/qwaz/projects/toytest

# process-related
master          = true
processes       = 8

# permission
uid             = qwaz
gid             = www-data

# misc files
socket          = /home/qwaz/projects/toytest/misc/toytest.sock
chmod-socket    = 664
pidfile         = /home/qwaz/projects/toytest/misc/toytest.pid
daemonize       = /home/qwaz/projects/toytest/log/toytest.log

harakiri        = 60
max-requests    = 4096
reload-on-as    = 512
reload-on-rss   = 192
limit-as        = 1024

no-orphans      = true
vacuum          = true

uwsgi --ini toytest_uwsgi.ini로 uwsgi를 실행시키고 sudo service nginx restart로 추가한 sites-enabled를 읽도록 nginx를 재시작하고 브라우저로 접속해보자. 접속에 성공했다면 sudo kill -9 $(cat /home/qwaz/projects/toytest/misc/toytest.pid)로 프로세스를 죽인다.

8. Emperor Mode

웹서버 관련 설정은 거의 다 끝났다. 이제 uWSGI를 Emperor Mode로 실행시키고 서버가 시작했을 때 자동으로 실행되도록 만들어주자. Emperor Mode에서는 ini 파일이 변경될 때마다 새로운 인스턴스를 띄워준다.

sudo mkdir /etc/uwsgi
sudo mkdir /etc/uwsgi/vassals
sudo ln -s /home/qwaz/projects/toytest/misc/toytest_uwsgi.ini /etc/uwsgi/vassals/
sudo uwsgi --emperor /etc/uwsgi/vassals --uid qwaz --gid www-data

실행 되는 것을 확인했다면 프로세스를 종료하고, 마지막으로 서버가 켜질 때 uWSGI가 뜨도록 해 보자.

/etc/rc.local 파일을 열어 /usr/local/bin/uwsgi --emperor /etc/uwsgi/vassals --uid qwaz --gid www-data를 추가하고 서버를 재부팅해서 사이트가 뜨는지 확인한다.

여기까지 마치면 디렉터리 구조는 다음과 같이 된다.

~/
└ projects/
  └ toytest/
    ├ (virtualenv 관련 파일들)
    ├ log/
    │├ access.log
    │├ error.log
    │└ toytest.log
    ├ misc/
    │├ toytest_nginx.conf
    │└ toytest_uwsgi.ini
    └ toytest/
      ├ manage.py
      └ toytest/
        ├ settings.py
        ├ urls.py
        └ wsgi.py

몇 부까지 있을지는 모르겠지만 다음에는 DB 세팅과 Django 세팅 파일 설정 등을 건드릴 예정이다.

[이 글은 이전에 쓰던 블로그에서 작성된 이후 새 블로그로 이전된 글입니다.]

사실 코드만 공개해도 되지만, 플래시의 보안 정책에 대한 이해 없이 코드만 본다면 아무런 도움이 안 될 것 같아서 설명하다 보니 조금 길어졌네요

다큐멘트 클래스처럼 적용시킵니다. AIR 설정에서 ‘포함된 파일’ 부분에 ‘policy.xml’ 이름을 가진 정책 파일 형태의 파일을 넣어주세요.

MIT 라이센스하에 배포하구요, 이것만 가지고는 소켓 서버를 못 만듭니다. 그냥 정책 파일 보내는 예제입니다.

정책 파일 보내는 포트와 실제 연결 포트가 다르다고 가정하고 짠 소스입니다.

클라이언트 측에서는 Security.loadPolicyFile("xmlsocket://127.0.0.1:843"); 등의 형태로 정책 파일을 요청하면 됩니다.

package {
    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.IOErrorEvent;
    import flash.events.ProgressEvent
    import flash.events.ServerSocketConnectEvent;
    import flash.filesystem.File;
    import flash.filesystem.FileMode;
    import flash.filesystem.FileStream;
    import flash.net.ServerSocket;
    import flash.net.Socket;

    import fl.controls.TextArea;

    public class MySocketServer extends Sprite {
        private const TARGET_IP:String = "127.0.0.1"; //타겟 IP

        private var policyServer:ServerSocket;
        private const POLICY_PORT:int = 843; //타겟 포트

        public function MySocketServer():void {
            policyServer.addEventListener(ServerSocketConnectEvent.CONNECT, onPolicyConnected);
            policyServer.addEventListener(Event.CLOSE, serverClosed);

            policyServer.bind(POLICY_PORT, TARGET_IP); //서버 소켓을 바인드시킵니다.
            policyServer.listen();
        }

        private function onPolicyConnected(e:ServerSocketConnectEvent):void {
            var tmpSocket:Socket = e.socket as Socket;

            tmpSocket.addEventListener(ProgressEvent.SOCKET_DATA, policyDataHandler); //소켓의 정책 파일 요청을 감지합니다.
            tmpSocket.addEventListener(Event.CLOSE, onPolicyClosed);
            tmpSocket.addEventListener(IOErrorEvent.IO_ERROR, IOErrorOccured);
        }

        private function policyDataHandler(e:ProgressEvent):void {
            var tmpSocket:Socket = e.target as Socket;

            var message:String = tmpSocket.readUTFBytes(tmpSocket.bytesAvailable);

            sendPolicyFileTo(tmpSocket);
        }

        private function onPolicyClosed(e:Event):void {
            var tmpSocket:Socket = e.target as Socket;

            tmpSocket.removeEventListener(ProgressEvent.SOCKET_DATA, policyDataHandler);
            tmpSocket.removeEventListener(Event.CLOSE, onPolicyClosed);
            tmpSocket.removeEventListener(IOErrorEvent.IO_ERROR, IOErrorOccured);
        }

        private function IOErrorOccured(event:IOErrorEvent):void {
            trace("IOError : " + event.text);
        }

        private function sendPolicyFileTo(target:Socket):void {
            //AIR FLA 안에 정책 파일의 형태로 policy.xml을 넣어주세요.
            //AIR2 설정의 포함된 파일 부분에 넣으시면 됩니다.

            var file:File = new File(File.applicationDirectory.nativePath + File.separator + "policy.xml");
            var stream:FileStream = new FileStream();
            stream.open(file, FileMode.READ);
            var data:String = stream.readUTFBytes(stream.bytesAvailable);

            target.writeUTFBytes(data);
            target.writeByte(0);
            target.flush(); //소켓에 정책 파일을 보냅니다.
        }
    }
}

[이 글은 이전에 쓰던 블로그에서 작성된 이후 새 블로그로 이전된 글입니다.]

정책 파일은 서버 내에 여러 개 위치할 수 있습니다. 플래시 플레이어에서 연결을 요청하면 기본적으로 루트 도메인의 /crossdomain.xml이라는 URL 정책 파일과 포트 843에서 소켓 정책 파일을 찾습니다.

정책 파일 중 이러한 정책 파일들을 ‘마스터 정책 파일’이라고 합니다.

소켓 연결의 경우 소켓 연결 위치와 같은 포트에서도 정책 파일을 찾지만, 이러한 정책 파일은 ‘마스터 정책 파일’로 간주되지 않습니다.

마스터 정책 파일은 일반적인 액세스 지정 외에도 메타 정책 문이 포함될 수 있습니다. 이 ‘메타 정책 문’에서 서버 내의 정책 파일들의 권한을 지정할 수 있습니다.

URL 정책 파일

URL 정책 파일에 대한 기본 메타 정책은 master-only이므로, 마스터 정책 파일 이외의 다른 정책 파일들은 무시됩니다.

플래시 플레이어 9 까지 URL 정책 파일에 대한 기본 메타 정책은 all이었으나,
사용자가 서버에 crossdomain.xml을 업로드 -> 그 crossdomain.xml을 정책 파일로 사용 -> 서버 데이터 접근
과 같은 방식으로 악용될 수 있어 플래시 플레이어 10에서 바뀌게 되었습니다.

보다 자세한 사항은 링크를 참조하세요.

swf 파일에서 Security.loadPolicyFile()을 이용해 다른 정책파일이나 다른 디렉터리 위치를 참조할 수 있습니다. 그러나 마스터 정책 파일에서 허용된 파일이 아니라면 이 요청은 무시됩니다.

(myserver.com/flash/crossdomain.xml을 로드 요청 하더라도 myserver.com/crossdomain.xml에서 메타 정책이 허용되어 있어야 함)

마스터 정책 파일을 확인할 때 플래시 플레이어는 3초간 서버 응답을 기다리고, 서버 응답이 없다면 마스터 정책 파일이 없다고 간주합니다.

그러나 loadPolicyFile()를 사용할 경우 플래시 플레이어는 호출되는 파일이 있다고 가정하고 파일을 로드하는데 필요한 시간동안 기다립니다. 따라서 loadPolicyFile()를 사용하여 명시적으로 파일을 호출하는 방법을 권장합니다.

소켓 정책 파일

플래시 플레이어는 연결을 요청하면 포트 843에서 제공되는 소켓 정책 파일을 찾습니다. URL 정책 파일과 마찬가지로 이것을 마스터 정책 파일이라 합니다.

URL 정책 파일과 마찬가지로 소켓 정책 파일은 정책 파일을 제공할 수 있는 포트를 지정할 수 있는 메타 정책 문을 지원합니다. 하지만 소켓 정책 파일의 기본 메타 정책은 all이므로, 마스터 정책 파일에서 제한을 두지 않는 한 모든 포트에서 정책 파일 로드가 가능합니다.

플래시 플레이어는 연결 요청 시 포트 843과 연결이 요청된 포트에서 소켓 정책 파일을 찾습니다. 다른 포트에서 정책 파일을 찾으려면 다음과 같이 loadPolidyFile()을 이용해 포트를 지정해 주어야 합니다.

Security.loadPolicyFile("xmlsocket://myserver.com:2525");

소켓 서버를 구현하고 소켓 정책 파일을 제공해야 할 필요가 있는 경우 연결되는 소켓과 같은 포트에서 정책 파일을 제공할 지 아니면 다른 포트에서 제공할 지를 결정해야 합니다. 두 경우 모두 서버에서 소켓 정책 파일을 보내기 전 클라이언트의 요청을 기다려야 합니다.

플래시 플레이어에서 연결을 요청할 때는 항상 연결이 설정된 직후 다음 문자열을 전송합니다.
<policy-file-request/>

플래시 플레이어의 요청은 null 바이트로 종료되며, 서버의 응답도 null 바이트로 종료되어야 합니다.

참조

링크 1
링크 2