아마존 AWS Elastic Beanstalk에 Python Flask 배포환경 구축을 위한 설정

아마존 Elastic Beanstalk은 애플리케이션을 올리기만 하면 Elastic Beanstalk이 용량 프로비저닝, 로드 밸런싱, 자동 조정,
애플리케이션 상태 모니터링에 대한 배포 정보를 자동으로 처리한다.
개발자는 개발에만 신경 쓰면 인프라는 아마존에서 다 해주겠다는 말이다.
이 얼마나 반가운 소리인가?
그러나 막상 Elastic Beanstalk를 쓰려면 손수 설정해야 하는 부분이 많다.
이 글에서는 static 파일을 아마존 CDN인 CloudFront를 통해 제공한다는 가정하에 크게 세 부분으로 나누어 설명하겠다.
첫째는 아마존 콘솔 단에서 IAM,S3,CloudFront설정이고,
둘째는 Elastic Beanstalk .ebextensions 설정.
마지막은 Python boto를 이용한 배포 스크립트다.
게으른 개발자로서 좀 편해 보고자 아마존 AWS Elastic Beanstalk을 쓰면서 환경 설정 때문에 애를 많이 먹었다.
AWS Elastic Beanstalk을 고려 중인 또 다른 개발자가 이 글을 읽고 같은 삽질을 않으면 좋겠다.

AWS 콘솔 설정

IAM 설정

배포 권한을 가진 Group를 만든다.
예제에서 그룹명은 Dorajistyle-deploy로 하겠다.
User인 dorajistyle은 Dorajistyle-deploy 그룹에 소속되어, 배포시에 dorajistyle유저 정보로 배포하게 된다.
Dorajistyle-deploy그룹은 아래의 policy를 가진다.
{
  "Version": "2012-10-17",
  "Statement": [
     {
      "Effect": "Allow",
      "Action": [
        "elasticbeanstalk:*",
        "ec2:*",
        "elasticloadbalancing:*",
        "autoscaling:*",
        "cloudwatch:*",
        "s3:*",
        "sns:*",
        "cloudformation:*",
        "rds:*",
        "iam:AddRoleToInstanceProfile",
        "iam:CreateInstanceProfile",
        "iam:CreateRole",
        "iam:PassRole",
        "iam:ListInstanceProfiles"
      ],
      "Resource": "*"
    },
    {
      "Sid": "QueueAccess",
      "Action": [
        "sqs:ChangeMessageVisibility",
        "sqs:DeleteMessage",
        "sqs:ReceiveMessage"
      ],
      "Effect": "Allow",
      "Resource": "*"
    },
    {
      "Sid": "MetricsAccess",
      "Action": [
        "cloudwatch:PutMetricData"
      ],
      "Effect": "Allow",
      "Resource": "*"
    },
    {
      "Sid": "Stmt110100200000",
      "Effect": "Allow",
      "Action": [
        "s3:*"
      ],
      "Resource": [
        "arn:aws:s3:::dorajistyle/*",
        "arn:aws:s3:::dorajistyle",
        "arn:aws:s3:::dorajistyle-static/*",
        "arn:aws:s3:::dorajistyle-static",
        "arn:aws:s3:::dorajistyle-deploy/*",
        "arn:aws:s3:::dorajistyle-deploy",
        "arn:aws:s3:::elasticbeanstalk-ap-northeast-1-000000000000",
        "arn:aws:s3:::elasticbeanstalk-ap-northeast-1-000000000000/*"
      ]
    },
    {
      "Sid": "Stmt130000013000",
      "Effect": "Allow",
      "Action": [
        "rds:*"
      ],
      "Resource": [
        "arn:aws:rds:ap-northeast-1:000000000000:db:dorajistyle"
      ]
    },
{
      "Sid": "Stmt1399636332000",
      "Effect": "Allow",
      "Action": [
        "elasticbeanstalk:*"
      ],
      "Resource": ["*","arn:aws:elasticbeanstalk:ap-northeast-1:000000000000:application/dorajistyle",
"arn:aws:elasticbeanstalk:ap-northeast-1:000000000000:environment/dorajistyle/dorajistyle"
,"arn:aws:elasticbeanstalk:ap-northeast-1:000000000000:applicationversion/dorajistyle/*"]
    },
    {
      "Effect": "Allow",
      "Action": [
        "cloudformation:DescribeStacks",
        "cloudformation:DescribeStackEvents",
        "cloudformation:DescribeStackResources",
        "cloudformation:GetTemplate",
        "cloudformation:List*"
      ],
      "Resource": "*"
    }


S3 설정

S3버켓은 총 3개가 필요하다.

dorajistyle-deploy
배포용 zip파일을 업로드할 버켓이다. 사용자 dorajistyle만 접근 가능하며, 버켓 설정은 기본 그대로 사용하면 된다.

CORS Configuration
<CORSConfiguration>
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Authorization</AllowedHeader>
    </CORSRule>
</CORSConfiguration>


dorajistyle-static
static 파일을 저장할 버켓이다. 누구나 읽을 수 있는 버켓이다.

Bucket policy
{
 "Version": "2008-10-17",
 "Id": "Policy1394587645145",
 "Statement": [
  {
   "Sid": "Stmt1394587643817",
   "Effect": "Allow",
   "Principal": {
    "AWS": "*"
   },
   "Action": "s3:GetObject",
   "Resource": "arn:aws:s3:::dorajistyle-static/*"
  }
 ]
}


CORS Configuration
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>http://*.dorajistyle.pe.kr</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>DELETE</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
    <CORSRule>
        <AllowedOrigin>http://*.www.dorajistyle.pe.kr</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>DELETE</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
    <CORSRule>
        <AllowedOrigin>http://dorajistyle.elasticbeanstalk.com</AllowedOrigin>
        <AllowedMethod>PUT</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>DELETE</AllowedMethod>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedHeader>Authorization</AllowedHeader>
        <AllowedHeader>x-requested-with</AllowedHeader>
        <AllowedHeader>origin</AllowedHeader>
    </CORSRule>
</CORSConfiguration>


dorajistyle
이미지등의 유저 컨텐츠를 저장할 버켓이다.
예제에서는 모든 유저가 읽을 수 있도록 설정되었는데, 이는 사용 용도에 따라 변경이 가능하다.

Bucket Policy
{
 "Version": "2008-10-17",
 "Id": "Policy1394587559249",
 "Statement": [
  {
   "Sid": "Stmt1394587510887",
   "Effect": "Allow",
   "Principal": {
    "AWS": "*"
   },
   "Action": "s3:GetObject",
   "Resource": "arn:aws:s3:::dorajistyle/*"
  }
 ]
}


CORS Configuration
<CORSConfiguration>
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Authorization</AllowedHeader>
    </CORSRule>
</CORSConfiguration>


CloudFront 설정

dorajsityle-static S3에 CloudFront를 연결한다.
Origin Domain Name에 S3 버켓 주소를 적으면 된다.
만약 CloudFront에 연결이 잘 되었는데도 리소스를 찾지 못한다면 Invalidations에서 해당 리소스를 무효화한다.

.ebextensions 설정

Elastic Beanstalk에 어플리케이션을 올리면 마법처럼 돌아간다고는 하지만,
각 어플리케이션마다 필요한 라이브러리를 모두 설치해 둘 순 없다.
그래서 .ebextensions 설정을 통해 각 어플리케이션에 맞는 라이브러리 설치와, 서버 설정 변경등이 가능하다.
.ebextensions에 대한 설명은 아마존의 컨테이너 맞춤 설정 안내(http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customize-containers.html)를 참조하면 된다.
이 글에서는 yum 패키지 업데이트와 라이브러리 설치, 배포 hook 설정 변경과 아파치 서버 변경을 다룬다.

01_yum_update.config

yum 패키지를 업데이트 한다.
commands:
  yum_updates: 
    command: "yum --security update -y"


02_package_update.config

필요한 yum 패키지를 설치한다.
packages키를 이용해도 되지만, 충돌이 일어날 경우 command키를 이용한 설치도 한 방법이다.
commands:
  yum_package_updates: 
    command: "yum install python-devel python-pip libtiff-devel libjpeg-turbo-devel libzip-devel freetype-devel lcms2-devel tcl-devel php -y --skip-broken"


03_pre_requirements.config

Elastic Beanstalk에 파이썬 어플리케이션을 업로드 하면,
requirements.txt파일을 찾아 필요한 파이썬 라이브러리를 자동으로 설치해 준다.
그런데 간혹 한번에 설치가 안되어 나누어 설치해야 하는 라이브러리가 있다.
몇몇 라이브러리를 설치할때 pip에서 발생하는 문제로 미리 설치할 라이브러리를 pre_requirements.txt에 넣어두고 먼저 설치하면 문제없이 설치된다.
다만 pre_requirements.txt 파일을 먼저 설치하려면 배포 hook코드를 변경해야 한다.
files:
  "/opt/elasticbeanstalk/hooks/appdeploy/pre/03deploy.py":
    mode: "000755"
    owner: root
    group: users
    content: |
      #!/usr/bin/env python
      import os
      from subprocess import call, check_call
      import sys
      sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
      import config


      def install_virtualenv():
          # If python 2.7 is installed, make the virtualenv use it. Else use the default system 2.6
          if config.get_python_version() == '2.7':
              cmd = 'virtualenv -p /usr/bin/python27 {0}'
          else:
              cmd = 'virtualenv {0}'

          return_code = call(cmd.format(config.APP_VIRTUAL_ENV), shell=True)

          if return_code != 0:
              print "WARN: error running '%s'" % cmd


      def install_dependencies():
          pre_requirements_file = os.path.join(config.ON_DECK_DIR, 'app', 'pre_requirements.txt')
          requirements_file = os.path.join(config.ON_DECK_DIR, 'app', 'requirements.txt')
          if os.path.exists(pre_requirements_file):
              check_call('%s install --use-mirrors -r %s' % (os.path.join(config.APP_VIRTUAL_ENV, 'bin', 'pip'), pre_requirements_file), shell=True)
          if os.path.exists(requirements_file):
              # Note, we're sharing the virtualenv across multiple deploys, which implies
              # this is an additive operation. This is normally not a problem and is done
              # to minimize deployment time (the requirements are not likely to drastically
              # change between deploys).
              check_call('%s install --use-mirrors -r %s' % (os.path.join(config.APP_VIRTUAL_ENV, 'bin', 'pip'), requirements_file), shell=True)


      def main():
          try:
              install_virtualenv()
              install_dependencies()
          except Exception, e:
              config.emit_error_event(config.USER_ERROR_MESSAGES['badrequirements'])
              config.diagnostic("Error installing dependencies: %s" % str(e))
              sys.exit(1)

      if __name__ == '__main__':
          config.configure_stdout_logger()
          main()


04_wsgi.config

아파치 서버 설정 파일을 입맛에 맞게 변경한다. wsgi.conf 파일을 .ebextensions 폴더에 넣어두고,
wsgi.config 훅에 아래 코드를 넣으면 서버로 설정을 복사한다.
container_commands:
  replace_wsgi_config:
    command: "cp .ebextensions/wsgi.conf /opt/python/ondeck/wsgi.conf"


wsgi.conf

캐쉬와 gzip압축등 설정을 담았다.
# LoadModule wsgi_module modules/mod_wsgi.so
WSGIPythonHome /opt/python/run/baselinenv
WSGISocketPrefix run/wsgi
WSGIRestrictEmbedded On

<VirtualHost *:80>
###############
# TYPES FIX #
###############
AddType text/css .css
AddType text/javascript .js

############################
# IE 11 Prevent Cache      #
############################
BrowserMatch "MSIE 11.0;" IE11FOUND
BrowserMatch "Trident/7.0;" IE11FOUND
# FileETag None
Header unset ETag env=IE11FOUND
Header set Cache-Control "max-age=0, no-cache, no-store, must-revalidate"  env=IE11FOUND
Header set Pragma "no-cache" env=IE11FOUND
Header set Expires "Thu, 24 Feb 1983 02:50:00 GMT" env=IE11FOUND

####################################
# Serve Pre-Compressed statics #
####################################
RewriteEngine On
RewriteCond %{HTTP:Accept-encoding} gzip
RewriteCond %{REQUEST_FILENAME}\.gz -s
RewriteRule ^(.*)\.(html|css|js|json|woff) $1\.$2\.gz [QSA]

# Prevent double gzip and give the correct mime-type
RewriteRule \.css\.gz$ - [T=text/css,E=no-gzip:1,E=FORCE_GZIP]
RewriteRule \.js\.gz$ - [T=text/javascript,E=no-gzip:1,E=FORCE_GZIP]
RewriteRule \.html\.gz$ - [T=text/html,E=no-gzip:1,E=FORCE_GZIP]
RewriteRule \.json\.gz$ - [T=application/json,E=no-gzip:1,E=FORCE_GZIP]
RewriteRule \.woff\.gz$ - [T=application/x-font-woff,E=no-gzip:1,E=FORCE_GZIP]

Header set Content-Encoding gzip env=FORCE_GZIP

#######################
# GZIP COMPRESSION #
######################
SetOutputFilter DEFLATE
AddOutputFilterByType DEFLATE text/html text/css text/plain text/xml text/javascript application/x-javascript application/x-httpd-php
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip
Header append Vary User-Agent env=!dont-vary

################################
# Leverage browser caching #
###############################
<FilesMatch ".(ico|pdf|jpg|jpeg|png|gif|html|htm|xml|txt|xsl)$">
Header set Cache-Control "max-age=31536050"
</FilesMatch>

############################
# CDN Rewrite Setting   #
############################
# Header set Access-Control-Allow-Origin: "*"
# UseCanonicalName On
# Don't redirect if the static hostname(s) loops back.
# RewriteCond %{HTTP_HOST} !^static\.
# Include only those static file extensions that we want to off-load.
# RewriteCond %{REQUEST_FILENAME} ^/.*\.(html|xml|txt|zip|gz|tgz|swf|mov|wmv|wav|mp3|pdf|svg|otf|eot|ttf|woff|jpg|jpeg|png|gif|ico|css|js|json)$
# RewriteRule /static/(.*)? http://static.dorajistyle.pe.kr/$1 [redirect=permanent,last]

#########################
# WSGI configuration #
#########################

WSGIScriptAlias / /opt/python/current/app/application.py

<Directory /opt/python/current/app/>
# Order allow,deny
# Allow from all
Require all granted
</Directory>

WSGIDaemonProcess wsgi processes=1 threads=15 display-name=%{GROUP} \
python-path=/opt/python/current/app:/opt/python/run/venv/lib/python2.7/site-packages user=wsgi group=wsgi \
home=/opt/python/current/app
WSGIProcessGroup wsgi
# WSGIScriptReloading On
</VirtualHost>


배포 스크립트.


deploy.sh

static폴더를 최적화하고, DB스키마가 변경되었을 경우 업데이트 하며, 필요한 파일만 압축하여 aws에 올린다.
optimize_static.sh와 upload_to_aws.py이 중요하다.
#!/bin/bash
STARTTIME=$(date +%s)
./optimize_static.sh
rm *.zip
prefix=$(sed -n '/^PREFIX/ s/.*\= *//p' ./application/config/guid.py | tr -d \')
guid=$(sed -n '/^GUID/ s/.*\= *//p' ./application/config/guid.py | tr -d \')
name="$prefix-$guid"
zip -r $name * ./.ebextensions/* -x ./.git\* ./.idea\* ./docs\* ./node_modules\* ./alembic\* ./tests\* ./images\* *.zip  *.DS_Store  ./application/frontend/static/\*
zip_file=$name'.zip'
echo "$zip_file"
echo -e "Do you want to upgrade alembic schema? (Yes/No) : \c"
read ANSWER
if [ "$ANSWER" == "Yes" ]
then
    alembic revision --autogenerate -m "Alembic initilized boilerplate tables."
fi
echo -e "Do you want to update schema? (Yes/No) : \c"
read ANSWER
if [ "$ANSWER" == "Yes" ]
then
    alembic upgrade head
fi
echo -e "Did you reviewed source and confirmed running status? (Yes/No) : \c"
read ANSWER
if [ "$ANSWER" == "Yes" ]
then
    python2 upload_to_aws.py $name
else
    echo "Checking status and trying to deploy again."
fi
ENDTIME=$(date +%s)
echo "$name"
echo "It takes $(($ENDTIME - $STARTTIME)) seconds to complete this task..."


optimize_static.sh

guid를 생성하고 총 4개까지 히스토리를 남긴다. 혹시 배포가 잘못되어 롤백을 하게될 경우 이전 4버전까지 롤백이 가능하도록 한다.
테스트 서버에 먼저 배포하여 테스트 하고 문제가 없으면 실 서버에 배포를 하기 때문에 실 서버에서 4버전이나 롤백할 가능성은 상당히 희박하다.
static 파일은 require optimizer를 사용해 하나의 js와 하나의 css파일로 합치고, sed를 이용해 디버깅을 위해 사용하던 로그 코드와 공백을 날려 용량을 줄인다.
그리고 각 파일을 gzip으로 압축하여 용량을 다시 한번 줄인다.
#!/bin/bash
guid=$(uuidgen | tr -d '\n-' | tr '[:upper:]' '[:lower:]')
guid=${guid:0:8}
today=$(date '+%Y%m%d')
guid=$today'-'$guid
echo "$guid"
extremely_very_old_guid=$(sed -n '/^VERY_OLD_GUID/ s/.*\= *//p' ./application/config/guid.py)
sed -i "s/^EXTREMELY_VERY_OLD_GUID = .*/EXTREMELY_VERY_OLD_GUID = $extremely_very_old_guid/" ./application/config/guid.py
very_old_guid=$(sed -n '/^OLD_GUID/ s/.*\= *//p' ./application/config/guid.py)
sed -i "s/^VERY_OLD_GUID = .*/VERY_OLD_GUID = $very_old_guid/" ./application/config/guid.py
old_guid=$(sed -n '/^GUID/ s/.*\= *//p' ./application/config/guid.py)
sed -i "s/^OLD_GUID = .*/OLD_GUID = $old_guid/" ./application/config/guid.py
sed -i "s/^GUID = .*/GUID = '$guid'/" ./application/config/guid.py
cd './application/frontend/compiler/'
grunt static
grunt --gruntfile Gruntfile_uncss.js
cd '../../..'
cd './optimizer'
node ./r.js -o build.js
cd "../"
sed -i -r "s/(\ ?|\ +),(\ ?|\ +)[a-zA-Z]\.log(Error|Json|Object|Trace|Debug|Info|Warn)(\ ?|\ +)\([^)]*\)(\ ?|\ +),(\ ?|\ +)/,/g" ./application/frontend/static-build/js/app.js
sed -i -r "s/(\ ?|\ +),(\ ?|\ +)[a-zA-Z]\.log(Error|Json|Object|Trace|Debug|Info|Warn)(\ ?|\ +)\([^)]*\)(\ ?|\ +),?(\ ?|\ +);/;/g" ./application/frontend/static-build/js/app.js
sed -i -r "s/(\ ?|\ +),?(\ ?|\ +)[a-zA-Z]\.log(Error|Json|Object|race|Debug|Info|Warn)(\ ?|\ +)\([^)]*\)(\ ?|\ +),?(\ ?|\ +);//g" ./application/frontend/static-build/js/app.js
sed -i -r "s/(\ ?|\ +),?(\ ?|\ +)[a-zA-Z]\.log(Error|Json|Object|race|Debug|Info|Warn)(\ ?|\ +)\([^)]*\)(\ ?|\ +),?(\ ?|\ +);?/\n/g" ./application/frontend/static-build/js/app.js

cd './application/frontend/static-build/locales'
find . -name '*.json' -exec sed -i '/^\s∗\/\//d' {} \;
find . -name '*.json' -exec sed -i 's/^[ \t]*//g; s/[ \t]*$//g;' {} \;
find . -name '*.json' -exec sed -i ':a;N;$!ba;s/\n/ /g' {} \;
find . -name '*.json' -exec sed -i 's/\"\s*:\s*\"/\":\"/g' {} \;
find . -name '*.json' -exec sed -i 's/\"\s*,\s*\"/\",\"/g' {} \;
find . -name '*.json' -exec sed -i 's/\s*{\s*/{/g' {} \;
find . -name '*.json' -exec sed -i 's/\s*}\s*/}/g' {} \;
cd '../..'
gzip -r --best ./static-build
rename .gz '' `find static-build -name '*.gz'`


upload_to_aws.py

보토를 이용해 Elastic Beanstalk을 업데이트 한다. 배포가 끝나면 guid를 검사하여 오래된 버전 소스를 삭제한다.
static파일 업로드에서 눈여겨 볼 점은 key에 Content_Encoding 메타 데이터를 gzip으로 해 주어야 하는 것이다.
이는 위의 optimize static에서 이미 gzip으로 압축했기 때문이다.

# coding=UTF-8
"""
    application.util.__init__
    ~~~~~~~~~~~~~~~~~~~~~~~~~~
    by dorajistyle

    __init__ module

"""
from Queue import Queue
import logging
import os
import string
import boto
from boto.beanstalk import connect_to_region
import sys
# import time
from application.config.aws import AWS_STATIC_S3_BUCKET_NAME, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, \
    AWS_ELASTIC_BEANSTALK_REGION, AWS_ELASTIC_BEANSTALK_APP_NAME, AWS_ELASTIC_BEANSTALK_ENVIRONMENT_ID, \
    AWS_ELASTIC_BEANSTALK_ENVIRONMENT_NAME, AWS_ELASTIC_BEANSTALK_S3_BUCKET_NAME
from application.config.guid import OLD_GUID, VERY_OLD_GUID, PREFIX_GUID, EXTREMELY_VERY_OLD_GUID
from application.properties import STATIC_GUID
from threading import Thread
logging.basicConfig()
logger = logging.getLogger()


def log_exception(text):
    logger.error(msg=text)

q = Queue()
# source directory
sourceDir = 'application/frontend/static-build/'
# destination directory name (on s3)
destDir = STATIC_GUID+'/'

conn = boto.connect_s3(AWS_ACCESS_KEY_ID,
                       AWS_SECRET_ACCESS_KEY)
bucket = conn.get_bucket(AWS_STATIC_S3_BUCKET_NAME)
eb_bucket = conn.get_bucket(AWS_ELASTIC_BEANSTALK_S3_BUCKET_NAME)
keys = list()
old_keys = list()
eb_keys = list()
for key in bucket.list():
    keys.append(key.name)
for key in eb_bucket.list():
    eb_keys.append(key.name)
eb = connect_to_region(AWS_ELASTIC_BEANSTALK_REGION,
                       aws_access_key_id=AWS_ACCESS_KEY_ID,
                       aws_secret_access_key=AWS_SECRET_ACCESS_KEY)
uploadDirNames = []
uploadFileNames = []
for path in os.listdir( sourceDir ):
    if not os.path.isfile(os.path.join( sourceDir, path)):
        uploadDirNames.append(path+'/')


for (sourceDir, dirnames, filenames) in os.walk(sourceDir):
    for subDir in dirnames:
        # print('dirname:'+ subDir)
        for (subDir, sub_dirnames, subfilenames) in os.walk(sourceDir+subDir):
            for subfilename in subfilenames:
                sub_path = string.replace(subDir, sourceDir, '')
                uploadFileNames.append(os.path.join(sub_path, subfilename))
    uploadFileNames.extend(filenames)
    break


def percent_cb(complete, total):
    sys.stdout.write('.')
    sys.stdout.flush()


def upload_deploy(source_path, dest_path):
    """
    Upload static files to S3 bucket.
    :return:
    """

    try:
        dest_path = dest_path.encode('utf-8')
        key = eb_bucket.new_key(dest_path)
        key.set_contents_from_filename(source_path,
                                       cb=percent_cb, num_cb=10)
    except BaseException as be:
        log_exception(be)
        return False
    return True


def upload_static(source_path, dest_path):
    """
    Upload static files to S3 bucket.
    :return:
    """

    try:
        dest_path = dest_path.encode('utf-8')
        key = bucket.new_key(dest_path)
        # if key.name.endswith(('.gz', '.gzip')):
        key.set_metadata('Content-Encoding', 'gzip')
        key.set_contents_from_filename(source_path,
                                       cb=percent_cb, num_cb=10)
    except BaseException as be:
        log_exception(be)
        return False
    return True


def worker():
    while True:
        item = q.get()
        if item['source_path'] == item['dest_path']:
            upload_deploy(item['source_path'], item['dest_path'])
        else:
            upload_static(item['source_path'], item['dest_path'])
        q.task_done()
        print 'Uploading %s to Amazon S3 bucket %s' % \
              (item['source_path'], item['dest_path'])

# threads = []
if len(sys.argv) == 2:
    eb_app = eb.describe_applications(application_names=AWS_ELASTIC_BEANSTALK_APP_NAME)
    versions = eb_app['DescribeApplicationsResponse']['DescribeApplicationsResult']['Applications'][0]['Versions']
    # if len(versions) > 2:
    #     versions = versions[:2]
    latest_version = PREFIX_GUID.replace('\'', '')+'-'+OLD_GUID.replace('\'', '')
    very_old_version = PREFIX_GUID.replace('\'', '')+'-'+VERY_OLD_GUID.replace('\'', '')
    extremely_very_old_version = PREFIX_GUID.replace('\'', '')+'-'+EXTREMELY_VERY_OLD_GUID.replace('\'', '')
    try:
        if latest_version in versions:
            versions.remove(latest_version)
        if very_old_version in versions:
            versions.remove(very_old_version)
        if extremely_very_old_version in versions:
            versions.remove(extremely_very_old_version)

        for key in bucket.list(prefix=OLD_GUID.replace('\'', '')):
            keys.remove(key.name)
        for key in bucket.list(prefix=VERY_OLD_GUID.replace('\'', '')):
            keys.remove(key.name)
        for key in bucket.list(prefix=EXTREMELY_VERY_OLD_GUID.replace('\'', '')):
            keys.remove(key.name)

        for eb_key in eb_bucket.list(prefix=latest_version):
            eb_keys.remove(eb_key.name)
        for eb_key in eb_bucket.list(prefix=very_old_version):
            eb_keys.remove(eb_key.name)
        for eb_key in eb_bucket.list(prefix=extremely_very_old_version):
            eb_keys.remove(eb_key.name)

        file_name = sys.argv[1]
        zip_file = file_name + '.zip'
        for i in range(8):
            t = Thread(target=worker)
            t.daemon = True
            t.start()
        item = {}
        item['source_path'] = zip_file
        item['dest_path'] = zip_file
        q.put(item)
        for filename in uploadFileNames:
            source_path = os.path.join(sourceDir + filename)
            dest_path = os.path.join(destDir, filename)
            item = {}
            item['source_path'] = source_path
            item['dest_path'] = dest_path
            q.put(item)
        q.join()
        eb.create_application_version(AWS_ELASTIC_BEANSTALK_APP_NAME, version_label=file_name,
                                      description=None, s3_bucket=AWS_ELASTIC_BEANSTALK_S3_BUCKET_NAME, s3_key=zip_file)
        eb.update_environment(environment_id=AWS_ELASTIC_BEANSTALK_ENVIRONMENT_ID,
                              environment_name=AWS_ELASTIC_BEANSTALK_ENVIRONMENT_NAME,
                              version_label=file_name)
        bucket.delete_keys(keys)
        eb_bucket.delete_keys(eb_keys)
        for version in versions:
            eb.delete_application_version(application_name=AWS_ELASTIC_BEANSTALK_APP_NAME, version_label=version, delete_source_bundle=False)
    except BaseException as be:
        print(str(be))
        if latest_version is not None:
            eb.update_environment(environment_id=AWS_ELASTIC_BEANSTALK_ENVIRONMENT_ID,
                                  environment_name=AWS_ELASTIC_BEANSTALK_ENVIRONMENT_NAME,
                                  version_label=latest_version)
    # print('eb application' + str(eb.retrieve_environment_info(environment_id=AWS_ELASTIC_BEANSTALK_ENVIRONMENT_ID,
    #                       environment_name=AWS_ELASTIC_BEANSTALK_ENVIRONMENT_NAME)))
    print('AWS Elastic Beanstalk updated.')
print('Bye Bye!')



by


Tags : , , , , , , ,

  • 재미있게 읽으셨나요?
    광고를 클릭해주시면,
    블로그 운영에 큰 도움이 됩니다!

파이썬 Flask에서 RequireJS Optimizer를 이용한 static폴더 최적화.

자바스크립트MVC를 이용할 경우 자바스크립트와 뷰 템플릿때문에 static폴더가 무겁습니다.
파일의 공백을 모두 제거하여 용량을 줄이면 좀 가벼워 지지요.
허나 파일 공백을 없애면 가독성이 떨어집니다.
난감한 상황이 발생해요.
길이 1m짜리 한줄코드를 고치고 기능 추가하는건 어렵잖아요?
변경이 빈번하게 일어나는 static 폴더.
개발 편의와 성능 두마리 토끼를 잡을 좋은 수가 없을까요?
RequireJS를 이용해 자바스크립트를 관리하신다면 방법이 있습니다!

우선 노드JS(http://nodejs.org)를 설치합니다.

RequireJS Optimizer를 설치합니다.

npm install -g requirejs

3. r.js를 다운로드 받습니다.(http://requirejs.org/docs/release/2.1.8/r.js)

4. 예제에서 사용할 어플리케이션 디렉토리 구조는 다음과 같습니다.

/build
  - r.js
  - build.js
/application
  __init__.py
  /static
    /views
      template.ejs
    /js
      app.js
      /vendor
        jquery-1.10.1.min.js
        bootstrap.min

application/init.py

from flask import Flask
app = Flask(__name__)
# static 폴더 경로를 지정해 줍니다. debug일땐 ‘static’폴더로 경로를 설정하고 나머지는 ‘static-build’를 경로로 잡습니다.
app.static_folder = 'static-build'
if app.debug:
        app.static_folder = 'static'

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

application/static/js/app.js

자바스크립트 어플리케이션 파일입니다.
requirejs의 기본 설정을 담고 있습니다.

require.config({
    "baseUrl": "/static/js",
    "paths": {
        "jquery": [
            "//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min",
            "vendor/jquery-1.10.1.min"
        ],
        "jquery.bootstrap": [
            "//netdna.bootstrapcdn.com/bootstrap/3.0.0-rc1/js/bootstrap.min",
            "vendor/bootstrap.min"
        ],
    },
    shim: {
        "jquery.bootstrap": {
            deps: ["jquery"],
            exports: '$.fn.bootstrap'
        },
        enforceDefine: true
    }
});

requirejs(['jquery','jquery.bootstrap'],
    function ($) {
    // 어플리케이션 코드 
    }
);

/build/build.js

RequireJS용 build파일입니다.

{
    appDir: '../application/static',
    baseUrl: 'js/vendor',
    mainConfigFile: '../application/static/js/app.js',
    paths: {
        "jquery": "jquery-1.10.1.min",
        "jquery.bootstrap": "bootstrap.min",
    },
    dir: '../application/static-build',
    optimize: "uglify2",
    optimizeCss: "standard.keepLines",
    removeCombined: true, // combine된 파일은 남겨두지 않습니다.
    preserveLicenseComments: false,
    modules: [
        {
            name: '../app'
        }
    ]
}

프로젝트 루트 페이지에서 다음을 실행합니다.

JS/CSS파일을 최적화 하여 application/static-build 폴더에 저장합니다.
node ./build/r.js -o ./build/build.js

단 이는 js와 css만 최적화를 해 주기 때문에 Ejs 템플릿도 용량을 줄이려면 아래 커맨드를 이용하면 됩니다.
find ./application/static-build/views -name '*.ejs' -exec sed -i '/^\s∗\/\//d' {} \;
find ./application/static-build/views -name '*.ejs' -exec sed -i 's/^[ \t]*//g; s/[ \t]*$//g;' {} \;
find ./application/static-build/views -name '*.ejs' -exec sed -i ':a;N;$!ba;s/\n/ /g' {} \;

쉘 스크립트를 작성해 한번에 실행하면 간편합니다.

optimizer.sh

#!/bin/sh
cd './build'
node ./r.js -o build.js
cd '../application/static-build/views'
find . -name '*.ejs' -exec sed -i '/^\s∗\/\//d' {} \;
find . -name '*.ejs' -exec sed -i 's/^[ \t]*//g; s/[ \t]*$//g;' {} \;
find . -name '*.ejs' -exec sed -i ':a;N;$!ba;s/\n/ /g' {} \;

참고 자료



by


Tags : , , , , , , ,

  • 재미있게 읽으셨나요?
    광고를 클릭해주시면,
    블로그 운영에 큰 도움이 됩니다!

Flask와 Canjs를 이용한 다국어 어플리케이션 토대

canjs와 python flask를 이용한 다국어 어플리케이션 제작을 위한 토대입니다.
Flask 부분은 Matt Wright님의 에서 영감을 얻었습니다.



Flask-Canjs-i18n-Boilerplate 소스



특징

  • Flask/Canjs 연동.
  • 다국어 지원.
  • 사용자 CRUD 구현.
  • 사용자 following/followers 기능 구현.
  • 소셜 네트워크 연결 구현.
  • 페이스북
  • 기타 (TODO)
  • RESTful API 구현.
  • 반응형 웹 디자인.
  • 구글 크롤러 친화적 url('#!').
  • 간편한 개발 문서 제작.(Sphinx)
  • 쉬운 테스팅. (nosetests)
  • 다양한 브라우저 호환 (IE7, IE8, IE9, IE10, Firefox, Chrome, Opera..)
  • 더 많은 기능을 윈하신다면, 함께 만들어 가요! :D


써보기

http://flask-canjs-i18n-boilerplate.ap01.aws.af.cm/



어떻게 동작하나요?

flask-canjs-i18n-boilerplate



환경


클라이언트 사이드

  • Canjs, 클라이언트 사이드 자바스크립트 프레임워크.
  • RequireJS, 자바스크립트 파일•모듈 로더.
  • Initializr, HTML5 템플릿.
  • Bootstrap, 프론트엔드 프레임워크.
  • Bootstrap-ie7, 부트스트랩 3에서 IE7을 지원을 도움.
  • mustache, 템플릿 엔진.
  • i18next, 자바스크립트 다국어 도구.
  • Font Awesome, 크기 변환등 개별화 가능한 백터 아이콘 모음을 제공.
  • typeahead.js, 텍스트 자동 완성 라이브러리.
  • spin.js, 작업중 활동 표시기.
  • Placeholders.js, 자바스크립트를 통해 HTML5의 placeholder 속성을 사용 할 수 있게 함.
  • Jade(pyjade), 파이썬용 Jade 템플릿 엔진 확장.
  • jQuery, 작고 빠르고 유용한 자바스크립트 라이브러리.
  • jQuery BBQ, HTML5 hashchange 이벤트 사용을 도와줌.

서버 사이드

  • Flask, 가벼운 파이썬 프레임워크.
  • Flask-Babel, 플라스크용 다국어 확장.
  • Flask-Gravatar, 플라스크용 그라바타 확장.
  • Flask-Security, 플라스크용 로그인 보안 확장.
  • Flask-Social, Flask-Security에서 Oauth를 지원하는 확장.
  • Flask-Assets, 자바스크립트와 스타일시트를 관리하는 플라스크용 확장.
  • Flask-SQLAlchemy, SQLAlchemy사용을 위한 플라스크용 확장.
  • Flask-WTF, WTForms 통합 확장.
  • Flask-Cache, 캐쉬 사용을 도와주는 플라스크용 확장.
  • Alembic, SQLAlchemy 데이터베이스 마이그레이션 관리 도구.
  • Celery, 분산 메시지 전달 기반의 비동기 작업 queue/job 큐.

디버깅과 테스팅

  • flask-debugtoolbar, django-debug-toolbar를 플라스크 용으로 포팅한 디버깅 툴바.
  • nose, 멋진 파이썬용 테스팅 도구.
  • BusterJS, 자바스크립트 테스팅 도구.

문서 생성

  • Sphinx, 지적이고 아름다운 문서를 만들어주는 도구.
  • JSDoc, 자바스크립트 문서화 도구.


설치


도구 설치

  • virtualenv, 분리된 파이썬 환경을 만들어줍니다.
  • virtualenvwrapper, virtualenv를 보다 편리하게 사용하도록 돕습니다.

프로젝트 복제

$ git clone https://github.com/dorajistyle/flask-canjs-i18n-boilerplate.git
$ cd flask-canjs-i18n-boilerplate.git

프로젝트를 위한 virtualenv 생성

$ mkvirtualenv flask-canjs-i18n-boilerplate

필요 라이브러리 설치

$ pip install -r requirements.txt

데이터베이스 마이그레이션

$ alembic revision --autogenerate -m "Alembic initilized boilerplate tables."
$ alembic upgrade head

관리자 추가

'[email protected]'와 'password'로 로그인 하시면 됩니다.
$ python manage.py init_user

설정

설정 값을 올바르게 설정해야 합니다.(데이터베이스, 메일, 소셜 네트워크 정보...)

  • application/settings.py엔 서버 사이드 설정이 들어 있습니다.
  • application/frontend/static/js/settings.js엔 클라이언트 사이드 설정이 들어 있습니다.
  • ./alembic.ini엔 alembic설정이 들어있습니다.

번역

  • 서버 사이드 번역(Babel)은 applications/frontend/translations 폴더에 있습니다.
  • 클라이언트 사이드 번역(i18next)은 applications/frontend/static/locales 폴더에 있습니다.

바벨 번역 컴파일

$ python tr_compile.py



사용


어플리케이션 실행

$ python wsgi.py

어플리케이션 프로파일 실행

$ python profile.py

문서 생성

$ python generate_documents.py


정적 폴더 최적화

Nodejs 와 RequireJS가 설치되어 있어야 합니다.
더 자세한 문서를 원하신다면 다음을 클릭하세요. RequireJS Optimizer.
$ optimize_static.sh

테스트

$ nosetests

아래의 테스트 스크립트를 이용하셔도 됩니다.

$ python run_nosetests.py



사용권

Flask-canjs-i18n-boilerplate는 MIT license를 따릅니다.



by


Tags : , , , , , ,

  • 재미있게 읽으셨나요?
    광고를 클릭해주시면,
    블로그 운영에 큰 도움이 됩니다!

파이썬 flask 개발 문서 만드는데 5분. Sphinx.

문서 작성도 개발의 일환입니다.
개발자는 깔끔한 문서를 만들고 싶지만,
문서 작성에 많은 시간을 들이고 싶지는 않지요.
Sphinx가 바로 그 딜레마를 해결해 주는 도구입니다.


Sphinx로 파이썬 Flask 개발 문서 만들기 순서

  1. Sphinx와 sphinxcontrib-httpdomain를 설치합니다.
    $ pip install sphinx
    $ pip sphinxcontrib-httpdomainproject 폴더에 doc 폴더 생성합니다.
  2. doc 폴더로 갑니다.
  3. sphinx-quickstart를 실행하여 기본 설정을 잡습니다.
    $ sphinx-quickstart
  4. conf.py에 다음을 추가합니다.
    sys.path.append(os.path.abspath('..'))
    # 확장
    extensions = ['sphinx.ext.autodoc',
    'sphinx.ext.intersphinx',
    'sphinxcontrib.autohttp.flask']
    # 테마 (default|basic|sphinxdoc|scrolls|agogo|traditional|nature|haiku|pyramid)
    html_theme = "nature"

    기본으로 제공하는 테마 외에도 내려받거나 직접 만든 테마도 사용 가능합니다.
  5. rst파일을 생성합니다.
    API 예시
    Users
    --------------------------
    .. autoflask:: application.api:create_app()
    :undoc-static:
    # api에 포함시키고 싶지 않은 blueprints.
    :undoc-blueprints:
  6. 문서를 생성합니다.
    $ make html

참고 자료

http://sphinx-doc.org/tutorial.html
http://sphinx-doc.org/theming.html
http://pythonhosted.org/sphinxcontrib-httpdomain/



by


Tags : , , , , ,

  • 재미있게 읽으셨나요?
    광고를 클릭해주시면,
    블로그 운영에 큰 도움이 됩니다!

Canjs와 Flask를 이용한 웹개발 예제

클라이언트 사이드 자바스크립트 프레임워크인 Canjs와
파이썬 마이크로 프레임워크인 Flask를 이용한 웹 어플리케이션 예제입니다.
이 Canjs + Flask 예제가 프레임워크 이해에 도움이 되길 바랍니다.


Canjs + Flask 예제 소스


예제에 사용된 라이브러리

  • Canjs, 클라이언트 사이드 자바스크립트 프레임워크.
  • RequireJS, 자바스크립트 파일•모듈 로더.
  • Initializr, HTML5 템플릿.
  • Bootstrap, 프론트엔드 프레임워크.
  • JSDoc, 자바스크립트 문서화 도구.
  • BusterJS, 자바스크립트 테스팅 도구.
  • mustache, 템플릿 엔진.
  • i18next, 자바스크립트 다국어 도구.
  • Flask, 가벼운 파이썬 프레임워크.
  • Flask-SQLAlchemy, SQLAlchemy의 플라스크 플러그인.
  • Jade(pyjade), 파이썬용 Jade 템플릿 엔진 플러그인.
  • Flask-Babel, 플라스크용 다국어 플러그인.

예제의 이해를 돕는 글



by


Tags : , , , , , ,

  • 재미있게 읽으셨나요?
    광고를 클릭해주시면,
    블로그 운영에 큰 도움이 됩니다!

파이썬 Flask 플러그인 안내와 팁.

데이터베이스
Flask-SQLAlchemy : OR매핑을 지원하는 플라스크용 파이썬 SQL toolkit
SQLAlchemy에서 상속(Inheritance) : http://docs.sqlalchemy.org/en/rel_0_7/orm/inheritance.html

SQLAlchemy 컬럼에 데이터가 생성될때나 업데이트 될때 현재 시각을 자동으로 넣는 방법.
updated_at = db.Column(db.DateTime, default=db.func.now(), onupdate=db.func.now())

로그인·보안
Flask-Social : 소셜 네트워크와의 연결을 간편하게 해 줌.
Flask Security(Auth) : 로그인과 권한관리.

관리자 페이지
Flask-Admin : 관리자 페이지를 손쉽게 만들도록 도와줌.

다국어 지원
Flask-Babel : 파이썬용 다국어 지원 Babel의 Flask 플러그인

캐쉬
flask-cache : 캐쉬 설정을 도와줌.

에셋 관리
flask-assets : 각종 static 에셋 관리를 편하게 도와줌.
사용하고자 하는 기능에 따라 추가 모듈이 필요하다. (css 압축, scss사용)
pip install cssmin
pip install pyscss
Flask-Assets extension에서 pyScss컴파일러 사용 설정법

from flask import Flask, render_template
from flask.ext.assets import Environment, Bundle

app = Flask(__name__)

assets = Environment(app)
assets.url = app.static_url_path
scss = Bundle('foo.scss', 'bar.scss', filters='pyscss', output='all.css')
assets.register('scss_all', scss)
And in the template include this:

{% assets "scss_all" %}
{% endassets %}

SCSS file은 debug모드에서도 컴파일 된다.

캐쉬
Flask-Cache : 플라스크에서 캐쉬 사용 하기 쉽게 도와줌.
# Flask-Cache Cache Type
CACHE_TYPE = 'simple' # 개발 시
CACHE_TYPE = 'redis' # 배포 시. redis 서버 구동 필요.
from flask.ext.cache import Cache

app = Flask(__name__)
cache = Cache()
cache.init_app(app, config={'CACHE_TYPE': CACHE_TYPE})

파일 업로드
Flask-Uploads : 파일 업로드 관련 처리를 도와줌.
from flask.ext.uploads import UploadSet
images = UploadSet()
images.__init__('upload', IMAGES)
filename = images.save(files, folder='/images', name='test.')
image_path = images.path(filename)
url = images.url(thumbnail)

텍스트 검색
Flask-WhooshAlchemy : 텍스트 기반 검색을 쉽게 도와줌

오류 추적
flask-exceptional : 오류 추적 서비스인 exceptional를 flask에서 이용 가능하게 도와줌.

전자상거래(e-commerce) [flask용 extension 아님]
stripe : 결제 모듈을 간편하게 시스템에 붙이도록 도와주는 서비스.
satchmoproject : 오픈소스 전자 상점 프레임워크 (satchmo Wiki)
satchless : 프레임워크에 종속적이지 않은 e-commerce용 클래스와 패턴 모음
flamaster : flask용 e-commerce eventing 시스템

Flask에서 JSON 다루기(JSON Handling in Flask)
http://flask.pocoo.org/docs/api/#module-flask.json

import requests
r = requests.get(QUERY_URL)
return r.json
//normal return
return jsonify(username=g.user.username,
email=g.user.email,id=g.user.id)

jsonify a SQLAlchemy result set in Flask
http://stackoverflow.com/questions/7102754/jsonify-a-sqlalchemy-result-set-in-flask
jsonify의 문제는 object가 자동으로 json화 되지 않는다는 것이다.
serialize를 위해 다음을 모델에 추가 해 준다.


def dump_datetime(value):
"""Deserialize datetime object into string form for JSON processing."""
if value is None:
return None
return [value.strftime("%Y-%m-%d"), value.strftime("%H:%M:%S")]

class Foo(db.Model):
//... SQLAlchemy defs here..
def __init__(self, ...):
//self.foo = ...
pass
@property
def serialize(self):
"""Return object data in easily serializeable format"""
return {
'id' : self.id,
'modified_at': dump_datetime(self.modified_at),
# This is an example how to deal with Many2Many relations
'many2many' : self.serialize_many2many
}
@property
def serialize_many2many(self):
"""
Return object's relations in easily serializeable format.
NB! Calls many2many's serialize property.
"""
return [ item.serialize for item in self.many2many]

뷰에서는 아래처럼 사용한다.

return jsonify(json_list=[i.serialize for i in qryresult.all()])

gunicorn서버 사용 설정.

from werkzeug.contrib.fixers import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)

if __name__ == '__main__':
port = int(os.environ.get('PORT', 5000))
app.run()

Blueprint
routing관리에 이용한다.
application/feedback 폴더에 모듈을 넣을 경우.

from flask import Blueprint
mod = Blueprint('root', __name__, url_prefix='') // prefix를 공란으로 두면 root를 의미한다.
mod = Blueprint('feedback', __name__, url_prefix='/feedbacks') // prefix를 채우면 route('/')가 해당 prefix와 같다.
@mod.route('/')

새로운 디렉토리를 만들고 __init__.py파일을 추가해야 import시 인식한다.

모듈을 사용하려면 어플리케이션에서 등록해 준다.

from application.feedback.views import mod as feedbacksModule
app.register_blueprint(feedbacksModule)

파일 여러개 올리기(Uploading multiple files with Flask)
http://stackoverflow.com/questions/11817182/uploading-multiple-files-with-flask


@app.route("/upload", methods=["POST"])
def upload():
uploadedfiles = flask.request.files.getlist("file[]")
print uploadedfiles
return ""

동적으로 생성된 이미지 파일의 url 받기(How to get url for dynamically generated image file?)
http://stackoverflow.com/questions/12034949/flask-how-to-get-url-for-dynamically-generated-image-file

@app.route("/imgs/")
def images(path):
generateimg(path)
fullpath = "./imgs/" + path
resp = flask.makeresponse(open(fullpath).read())
resp.content_type = "image/jpeg"
return resp

mydomain.com/static/test.jpg

from flask import Flask, redirect, url_for

app = Flask(__name__)
@app.route('/')
def index():
generate_img("test.jpg"); #save inside static folder
return '<img src=' + url_for('static',filename='test.jpg') + '>'

Jade
두개의 속성을 지정할 땐 콤마(,)를 잊지 않는다.

script(data-main='js/app', src='js/vendor/require.js')



by


Tags : , , , , , ,

  • 재미있게 읽으셨나요?
    광고를 클릭해주시면,
    블로그 운영에 큰 도움이 됩니다!

기름기 쫙 빠진 웹 프레임워크. Python Flask.

15년 전 웹(Web)에 처음 관심을 후로 여러 언어를 접해봤습니다.

우선
HTML, CSS를 사용했고,
더 나아가 Javascript를 사용하게 되었습니다.
백 엔드는 20대 이후에 접하게 되었네요.
그러다 대학에 들어가게 되면서 PHP를 잠시 만졌고,
Java에 빠진 뒤론 JSP가 최고인 줄 알았습니다.
객체지향 개발.
MVC!
그러나 자바에 맛 들인지 얼마 되지 않아,
편한 개발을 돕는다는 명목으로 각종 프레임워크가 난무했고,
새로운 프레임워크를 익히는데 염증을 느꼈습니다.
프레임워크는 개발의 편의를 돕기 위한 것인데 개발이 재미없어지다니, 슬픈 일이었죠.
웹 개발이 싫어졌었어요.

그러다가 Ruby on Rails를 만났습니다.
아~ 이건 정말 신세계에요.
개발하는 재미가 쏠쏠합니다.
루비를 레일즈에 얹으니, 정말 이보다 좋은 개발 도구가 있을까요?
그렇게 RoR에 좋은 감정을 유지해 왔습니다.
하지만 Rails는 군더더기가 좀 있어요.
다음엔 Rails 보다 좀 더 가벼운 Sinatra도 한번 써봐야겠다는 마음먹고 있었죠.

그러다가 우연한 기회에 파이썬 플라스크를 만났습니다.


python-'Python Flask 개발 from http://imgs.xkcd.com'
Python!!!

파이썬.
루비나 펄 같은 스크립트 언어를 접해봐서 그런지 진입 장벽은 그리 높지 않았습니다.
문법이 조금 다르긴 하지만 필요한 함수는 안내에서 찾아 개발하면 됩니다.

마이크로 프레임워크인 플라스크(Flask)를 처음 깔았을 때 좀 당황했습니다.
달랑 7줄 짜리 헬로우 월드 코드를 보고 고민에 빠졌죠.
‘어떻게 사용해야 하지?’
‘정말 이게 다야?’
Rails는 프레임워크를 설치했을 때 설정 파일을 비롯하여 수많은 파일이 생성됩니다.
그런데 Flask는 참 단순하더군요.
아마 덩치와 편의성은 Django가 Rails와 비슷하겠지요.

짧은 튜토리얼만 봐도 바로 개발할 수 있니다.
DB는 Flask-SQLAlchemy를 사용하여 만들었는데, 이 역시 익히기가 간편하여 좋더군요.

그런데 막상 개발하다 보니 눈이 어지럽습니다.
html 코드 때문이었는데요.
즐겨쓰던 haml 템플릿 엔진을 쓰려고 했으나, 지원이 시원치 않아서 Jade로 갈아탔습니다.
설정이 아주 간편해요.

자. 이제 본격적으로 개발해 볼까요?
그런데 뭔가 허전합니다.
템플릿에서 파이썬 함수를 쓰고 싶어요.
레일즈의 helper처럼 말이죠.
그건 flask의 context_processor를 이용하면 됩니다.
자세한 내용은 flask 안내서 있어요.

대체로 개발이 수월했지만,
문법이 달라서 골치가 잠깐 아픈 부분이 한곳 있습니다.
형 변환이 자동으로 안되어서 수동으로 해야 하는데,
jade템플릿에서 숫자를 문자로 바꾸는 함수가 안 통하는 거에요.
찾아보니 바꾸는 방법이 4가지씩이나 된다고 합니다.
이 중 3번과 4번은 Jade템플릿에서도 잘 동작해요.

  1. str(숫자)
  2. repr(숫자)
  3. '숫자'
  4. '%d' % 숫자

간단한 웹 어플리케이션을 만들며, 파이썬 플라스크 개발을 맛보았습니다.

고객제안과 투표 기능을 구현했어요.

코드는 아래 주소에서 보실 수 있습니다.

Python + Flask + Flask-SQLAlchemy + Jade Proposal Center Example



Python Flask 개발에 도움이 되는 링크

파이썬 함수 도움말 (http://docs.python.org/2/library/functions.html)

플라스크(http://flask.pocoo.org/)

Flask-SQLAlchemy(http://pythonhosted.org/Flask-SQLAlchemy/index.html)

플라스크 다국어 지원 (http://pythonhosted.org/Flask-Babel)

파이썬 Jade(https://github.com/syrusakbary/pyjade)

파이썬 Scss(https://github.com/Kronuz/pyScss)



by


Tags : , , , , , , ,

  • 재미있게 읽으셨나요?
    광고를 클릭해주시면,
    블로그 운영에 큰 도움이 됩니다!