AWS Summit 2017. IoT와 AI.


AWS에서 많은 서비스를 제공하고 있지만, 지금 사용하는 서비스는 몇 가지 되지 않는다.
아마존 웹 서비스를 쓰면서 서비스 운영에 대한 부담이 많이 줄었으니 또 좋은 서비스가 눈에 띄면 써보고 싶은데,
문서 찾기가 영 까다롭다.
그래서 가끔은 이런 행사에 가서 정보를 얻는다.
이틀 동안 진행하는 행사였는데, 목요일 하루 오후에만 잠깐 다녀온 게 좀 아쉽지만,
제일 관심 있던 IOT와 AI 세션을 들었으니 만족한다.
이번 행사에서 가장 흥미로웠던 건 오프라인 환경에서 IOT 기기의 소통을 돕는 AWS greengrass였다.
이를 통해 정보기술 서비스를 적용하기 어려웠던 분야에 혁신이 이루어지길 기대해본다.

AWS Summit 2017 서울, 메모

AWS 기반 고급 하이브리드 IT 디자인

Direct connect (https://aws.amazon.com/ko/directconnect/)

비용

Port 사용비용/시간 + 데이터 전송료
데이터 IN $0, 데이터 아웃 - 리전별 상이
회선(전용선/MPLS) 비용
로케이션 상면 임대비용(필요시)
트래픽이 많으면 전용선을 쓰는게 오히려 비용 절감이 된다.

성능

1 Gbps 10Gbps
DX파트너를 통해 100, 200, 300, 400, 500 Mbps 까지 가능
VPC 까지 통신할 때는 VPC peering을 사용한다. (dev, test, production VPC)
원하는 수준의 네트워크 가용성을 가질 수 있다.

Amazon SNS로 지속적 관리가 가능한 대용량 푸쉬 시스템 구축 여정

레거시

GCM, APNS 이용해서 1000건씩 끊어 보냈음.
시간 오래걸리고 상태와 모니터링 어려움

SNS (https://aws.amazon.com/ko/sns/)

Topics : 어플리케이션 엔드포인트들이 구독 단위로 그루핑 되어있음
Application : 플랫폼 별 토큰
Subscriptions : 어플리케이션이 토큰을 구독하는 정보

aws 콘솔에서 csv토큰 등록할 때 오류발생 : cli나 api를 이용하자.
이벤트가 발생할 때마다 SQS에 넣고 람다로 파싱해서 로그를 관리 (예 : 발송 이벤트가 발생하면 해당 토큰이 발송 완료되었다는 것을 표시해두고 중복 전송을 막는다.)
병렬처리할 때 동일 데이터를 끌고가면 중복 푸쉬가 발생하는걸 염두에 두자.
APNS 어플리케이션 인증은 1년마다 만료되므로 미리미리 갱신해두자.
SNS - CloudWatchLogs 기록 IAM 설정해두자.

그룹 전송

조건에 일치하는 사용자 목록을 뽑아서 큐에 넣고 람다를 통해 SNS로 발송
회원 가입할 때 토픽을 정의해둔다.

AWS와 Docker Swarm을 이용한 쉽고 빠른 콘테이너 오케스트레이션

Docker swarm (https://docs.docker.com/engine/swarm/)

클러스터 구성 / ECS보가 몇가지 나은 기능도 있다.
멀티메니저 노드 지원
다양한 서비스를 연결해 사용해야 할 때 고려해볼 만 하겠다.
Docker for AWS (Cloud formation)
오토스케일링 설정 가능
클라우드와치 지원

docker network

동일 네트워크에 컨테이너를 등록
Ingress Network : 모든 노드에 포트를 오픈한다.
visualizer service로 각 노드의 상태를 알 수 있다.

AWS IoT 기반 사물 인터넷 아키텍쳐 구현하기

https://aws.amazon.com/ko/iot/

기능

Device gateway : MQTT와 HTTP(1.1)를 이용한 Thing과의 커뮤니케이션
Device sdk : 연결, 인증, 메시지교환
Rules Engine : 규칙 기반으로 메시지 변환 및 AWS 서비스로 전달 (sql로 토픽 필터 정의)
Rule Engine Action :
하나의 토픽에 여러 룰을 적용해서 연동
Shadow : 기기가 오프라인일 때 마지막 상태를 알아내거나 다음 동작을 저장해둠. (일종의 캐쉬로 보면 되겠다. 파이어베이스db처럼)

구성 축

Thing : Sense, act
Cloud : storage, computing
Intelligence : analysis

AWS Greengrass(https://aws.amazon.com/ko/greengrass/)

로컬 이벤트에 신속히 반응할 수 있음
오프라인 운영가능
디바이스에서 데이터 처리 가능
클라우드 비용 절감

AWS Greengrass 구성

  • 로컬 브로커
  • 로컬 람다
  • 로컬 디바이스 쉐도우
데이터 온도에 따라 다른 접근이 필요하다.
Hot - Kinesis, Warm - Lambda , Cool, Cold

Amazon AI 서비스를 통한 스마트 애플리케이션 개발

https://aws.amazon.com/amazon-ai/

Polly

텍스트를 음성으로 변환
SSML 지원 : 음선 합성을 위한 W3C레서 정한 XML 기반 언어 규약
Lexicons : 개발자가 단어의 실제 발음을 정의
텍스트 - 어떻게 읽을지 텍스트 문장 - 발음으로 변환 - 높낮이 규정 - 음성 스트리밍으로 변환
MP3로 다운받아 재사용 가능
cli, sdk로 사용 가능
aws polly
백만문자당 $4

Amazon Recognition API

이미지에서 객체 및 장면을 탐지해서 json으로 반환
안면인식, 비교
민감한 정보를 포힘하고 있는지 알려줌

Amazon Lex

쉬운 쳇봇 구현
콘솔에서 정의해서 바로 런칭 가능
versioning, alias 제공



by


Tags : , , , , , , ,

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

Multi-container 도커 애플리케이션 쉽게 쓰기. Docker Compose


Docker는 참으로 편리한 도구다. Dockerfile만 한번 잘 작성해 두면, 두고두고 잘 쓸 수 있다. 그러나 한 가지 아쉬운 점이 있다면, 복잡한 서비스는 Dockerfile이 지나치게 길어진다는 것이다. 데이터베이스와 캐시, 애플리케이션 설정을 한데 모아두면 오류가 났을 때 무엇이 문제인지 찾기도 어렵다.
그래서 각각의 설정을 따로 만들어서 실행 시에 --link 옵션으로 연결하기도 하지만, 귀찮은 일이다.
Docker compose는 그런 귀찮음을 줄여준다. 아래 docker compose 설정 파일을 보자.


docker-compose.yml
version: '2'
services:
postgres:
container_name: mypostgres
image: postgres
ports:
- "5432:5432"
redis:
container_name: myredis
image: redis
ports:
- "6379:6379"
webapp:
build: .
container_name: myapp
restart: always
ports:
- "80:8080"
depends_on:
- postgres
- redis


이렇게 설정 파일을 만들고,
docker-compose up
명령어만 실행하면 애플리케이션을 실행하기 전에 postgres와 redis를 띄우고 나서, webapp 컨테이너를 띄워준다.
webapp에서 해당 컨테이너에 접속하려면 host에 container_name을 넣어주면 된다.
예를 들어 postgres에 접속하려면 host에 mypostgres를 넣어주면 된다.
docker compose 덕분에 도커 컨테이너 관리의 신세계가 열렸다.

아마존 AWS의 EC2 Container service 에서도 docker compose 파일을 지원하기 때문에, 쉽게 배포할 수 있다.
다만 로컬 이미지를 지원하지 않으므로 Amazon EC2 Container Registry(ECR)에 이미지를 올려 사용해야 한다.
만약 배포해야하는 multi-container docker가 많다면 ECS를 고려해볼 만 하다.
하지만 ECS task를 실행하기 위해서는 인스턴스를 하나 올려야 하므로, 올려야 할 서비스가 많지 않다면 그냥 EC2 인스턴스 하나에서 docker-compose up 명령어를 사용해 띄우는 것이 경제적이다.
꼭 ECS를 사용하지 않더라도 오픈소스가 아닌 이미지 저장용으로 ECR은 쓸만하다.
따로 Docker registry를 위한 인스턴스를 띄우지 않아도 되고, 스토리지 요금과 데이터 전송 요금만 사용한 만큼 내면 되기 때문에 간편하다.


참고자료

https://docs.docker.com/compose/rails/
https://docs.docker.com/compose/compose-file/
https://docs.docker.com/compose/startup-order/

AWS ECS 관련

http://docs.aws.amazon.com/AmazonECS/latest/developerguide/launch_container_instance.html
https://aws.amazon.com/ko/ecr/
http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_CLI_installation.html
http://docs.aws.amazon.com/AmazonECS/latest/developerguide/ECS_CLI_Configuration.html
https://github.com/aws/amazon-ecs-cli
https://aws.amazon.com/ko/ecr/pricing/?nc1=h_ls



by


Tags : , , , , , , , , ,

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

아마존 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 : , , , , , , ,

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

아마존 클라우드 AWS. 효율적인 설계를 위한 Architecting on AWS.


퍼블릭 클라우드 하면 Amazon, Google, Microsoft 이 세 회사가 떠오른다.
이 세 곳 중에 어느 클라우드가 좋을까 여러 글을 찾아보았다.
모두 장단점이 있지만, 결국 개발자 입장에서 제일 편한 AWS를 쓰기로 마음을 먹었다.
Microsoft의 azure는 말로는 다 쉽게 된다는데,
막상 실제로 하려면 참조 자료도 별로 없고 원하는 데로 되질 않는다.
구글 클라우드 플랫폼은 꽤 안정적으로 보이긴 하지만,
이미 너무 많은 구글 제품을 쓰고 있는 나는 좀 다른 걸 써 보고 싶었다.
사실 더 큰 이유는 구글 클라우드를 쓰기로 마음먹고 개발한 것이 아니므로
이대로 구글에 배포하면 구글에서 제공하는 좋은 자원을 제대로 활용하지 못하기 때문이다.
그래서 결정한 AWS.
좀 써보니 라이브러리도 잘 되어있고 마음에 든다.
그러나 글로만 배운 것은 한계가 있는 법.
춤을 직접 춰봐야 늘지 글로 배워봤자 얼마나 배우겠는가?
기술도 마찬가지로 직접 써봐야 익숙해지고 는다.
그래서 실습과 교육이 적절히 이루어진 Architecting on AWS교육을 듣게 되었다.
교육이 재미있고 지루할 틈 없이 지나가서 아주 만족스럽다.
분기별 한 번씩은 열릴 예정이라고 하니,
AWS에 관심을 가진 사람은 한번 들어볼 만하다.

Architecting on AWS 메모와 팁


보안

설계

  • private subnet에서 밖으로 나갈 땐 Network address translation (NAT) 서버를 이용한다.
  • 서버에 바로 접속을 허용하지 말고, Bastion 서버를 통과해서만 접속이 가능하도록 한다.
  • EIP를 이용하는 것 보다 ELB를 이용하는편이 보안에 좋다.

권한

  • 마스터 유저는 사용하지 말고, IAM(Identity and Access Management)를 이용해 각각의 권한을 지닌 사용자를 만들어 쓴다.
  • 항상 권한을 가질 필요가 없다면, STS(Security Token Service)f를 이용해 사용자가 필요한 임시 권한을 주었다가 회수한다.
  • 유저는 Role을 가지거나 User Group에 속해서 권한을 가지는 경우가 있는데, STS를 통해 Role을 지닌 유저 키를 받아오면 보안에 더 좋다.

좋은 클라우드 설계를 위한 안티패턴과 패턴

안티패턴/ 패턴

  • 수동 프로세스 / 자동 프로세스
  • 밀결합(Thightly-coupled) / 소결합(Loosely-coupled)
  • 세션 사용(Stateful) / 세션 미사용 (Stateless)
  • 수직적 확장 / 수평적 확장

자동 구성 도구(Bootstrapping)

  • 스크립트 (Bash, 파워쉘)
  • 구성관리 도구(Chef, Puppet)
  • Amazon OpsWorks
  • http://169.254.169.254/latest/meta-data/

AWS 제품 소개 / 팁

네트워크

Router 53

서버 부하가 많이 걸리면 DNS(Router 53)에 Round-robin 설정 후,
동일 도메인 (예:dorajistyle.pe.kr)에 서브도메인(Record Type A)으로 각 서버의 ip를 설정한다.
www.dorajistyle.pe.kr은 CNAME타입으로 dorajistyle.pe.kr에 연결한다.
그러나 이 방법은 서버를 추가할 때마다 매번 설정을 변경해야 하는 불편함이 있다.
ELB를 사용하면 이런 불편함을 줄일 수 있다.
Router 53에는 Alias type이 있어서 value에 도메인을 넣으면 alias된다.
elb주소를 alias하려면 dorajistyle.pe.kr의 alias로 ELB주소를 붙인다.

ELB

ELB 참조 시, IP 어드레스를 절대로 사용하지 말고 A레코드를 항상 사용한다.

CloudFront

약정 할인 되고, 유일하게 네고가 가능한 서비스다.
월 10테라 이상이면 CloudFront가 S3보다 저렴하다.
정적(static) 컨텐츠 뿐 아니라 동적(dynamic) 컨텐츠도 캐쉬 가능하다.
그러나 동적 컨텐츠를 캐쉬하게 되면 비용적인 부담이 생길 수 있다.

스토리지

Amazon EBS

내구성이 필요하고 공유 가능한 스토리지를 원한다면 사용한다.

Amazon RDS

데이터베이스

DynamoDB

NoSQL 데이터베이스

S3

Eventually Consistent기 때문에 수정 결과가 바로 반영되지 않을 수 있다.
RRS(Reduced Redundancy Storage, RRS)
표준 스토리지보다 중복 수준을 낮추어 비용을 절감한다. 내구성 99.99%
S3는 Request당 과금하기 때문에, S3에 올리고, 그것을 가공해서 다시 S3에 올릴때도 비용이 발생한다.
한번의 Request에 여러 개의 파일을 묶어서 업로드가 가능하다.

배치

SNS

Http, Email, SMS, 모바일 푸쉬 등 여러 대상에 Push한다.

SQS

단순 큐.
큐에 있는 것을 처리하는 EC2서버는 ELB에 물려 있을 필요가 없다.
EC2서버가 항상 SQS 큐를 바라보기 때문에 큐 자체가 ELB역할을 하기 때문이다.

SWF

여러가지 작업을 실행할 때 여러개의 큐를 생성할 필요 없는 여러 작업의 워크플로우(BUS)
순차적으로 이루어져야 하는 경우에 유리하다.

CloudFormation

현재 구성을 JSON형식으로 정의한다.
다른 환경에서 같은 환경을 구축하려고 할 때 유용하다.

CloudInit

EC2 인스턴스를 생성할 때 userdata에 스크립트를 넣으면 첫번째 부팅시 사용자 데이터를 스크립트로 인식하여 실행한다.
#!(리눅스)
<script> (윈도우)

AMI

Ami에서 aws cli로 s3에서 원하는 부분 복사가 가능하다.

관리

CloudWatch

Metric 측정 도구이다.
5분 단위로 측정은 무료, 1분 단위로 측정하면 인스턴스당 $3.5
커스텀 측정치는 측정치당 월 $0.50

CloudWatch

지정된 범위가 침해되면, 경보를 발생하여 지정된 액션을 실행한다.
* SMS 통지
* 이메일 전송
* AutoScaling
* 인스턴스 정지
5분 주기면 무료이고, 1분 주기로 체크하면 과금이 된다.

Auto Scaling

보일러 온도 조절기를 생각하면 편하다.

desired capacity : 초기에 띄울 인스턴스 갯수
min : 최소 인스턴스 갯수
max : 최대 인스턴스 갯수
AZ-a,b : 어느 존에 인스턴스를 띄을 것인가? (desired capacity/AZ 해서 각 AZ에 인스턴스를 띄워준다.)
서버 갯수가 고정인 경우라도 Auto Scaling을 쓰면 안정성이 향상된다.
(예 : desired capacity = 1 max = 1 AZ-a,b)
어플리케이션 서버에 문제가 생기면 자동으로 새로 띄워준다.
AZ에 문제 발생시 문제가 없는 AZ에 인스턴스를 띄워준다.
사람이 직접 실행하거나, CloudWatch알람을 걸어서 자동으로 실행할 수 있다.

예)
인스턴스가 가동하기까지 준비시간이 필요하므로, 스케일 아웃은 미리 하고, 스케일 인은 보수적으로 한다.
인스턴스가 생성되면 기본 한시간 비용은 나오기 때문에, 스케일 아웃과 스케일 인이 너무 빈번하게 일어나면 좋지 않다.
트리거: CPULoad
측정치 (M) : 평균 CPU사용량
5분간 M > 80% 이면 스케일 아웃
20분간 M < 40% 이면 스케일 인

오토 스케일링 시나리오

Service - EC2 선택
ELB 생성 TCP프로토콜 + 80 포트, Health Check Interval은 적절한 값(예: 0.1)으로 설정한다.
Launch Configuration 생성
Security Group에 HTPP Rule을 추가한다.
Auto Scailing Group 생성
서브넷은 1개 이상 추가한다.
설정에서 Receive traffic from Elastic Load Balancer(s)에서 로드 발란서를 선택한다.
Keep this group at its initial size에 체크하고 생성한다.
인스턴스에 우클릭해서 오토 스케일링으로 생성된 인스턴스에 태그 부여.
설정 버튼에서 aws:autoscaling:groupName을 선택한다.
SNS토픽을 생성 한다.
E-mail Subscription을 생성한다.
Auto Scaling 연동 설정한다.
CloudWatch로 CPU Alert 생성한다.
Create Alarm > AutoScaling 검색 > CPUUtilization
Define Alarm Threshold 를 잘 설정한다.
(예) 부하가 50이상일 때 >= 50)
Auto Scaling Policy를 생성 생성한다.
해당 알림에 따라 실행할 동작 지정한다.

비용

네트웍 비용은 나가는 것을 기준으로 한다.
On-demand instances 소매가
Reserved instances 약정할인 (선납금을 조금 내고 조금 할인 받거나, 많이 내고 많이 할인 받는다.
Spot instances 남는 인스턴스를 경매 방식으로 구입해서 사용한다. (가격변동이 있다.) 경매에 지면 자동으로 꺼진다.
일반적인 백그라운드 프로세싱에 사용한다.
하나의 ELB에 여러개의 ASG(Autoscaling Group)을 붙일 수 있다. 예를 들면 하나는 온디멘드, 다른 하나는 스팟 인스턴스 그룹으로 만들어 두면 하이브리드 구성이 가능하다.

링크

아키텍팅

http://aws.amazon.com/ko/architecture/
http://prezi.com/cxpwi_og7lht/aws-regions-and-availability-zones/
http://translate.google.com/translate?hl=ko&sl=ja&tl=ko&u=http%3A%2F%2Faws.clouddesignpattern.org%2Findex.php%2F%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8

보안

http://blogs.aws.amazon.com/security

AWS Elastic Beanstalk

http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/concepts.platforms.html
http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/createdeployPythoncustomcontainer.html
http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customize-containers.html

최적의 비용 계산기(기술지원 신청하면 사용 가능)

http://aws.amazon.com/support/trustedadvisor



by


Tags : , , , , ,

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