EC2の起動をlambdaとAPIGatewyで実現する

# なぜやるか
機械学習のシステム構築で一番コストがかかるところはどこでしょうか？
**訓練に必要なサーバ費用**でしょうか？**データサイエンティストの給料**でしょうか？
これは、プロジェクトの目的や性質によって変わってきますが、多くの場合で一番コストがかかるのは**推論部分**です。
つまり、運用にコストがかかります。
これらの課題を解決するために、弊社ではtensorflow jsを活用した端末側での処理やモデルの軽量化など、様々な取り組みを行っています。しかしながら、概念検証フェーズやモデルが大規模になるプロジェクトではGPUサーバーでAPIとして提供することも多いのが事実です。
そこで、簡易的にGPUサーバをON/OFFできる仕組みが必要です。それを実現するのが、この記事の目的です。

# What's System
APIGatewayで作成したAPIにアクセスしてEC2の起動・停止・ステータスチェックを行います．
lambdaへのデプロイにzappaを使用するため，Dockerの中にzappaとlambdaのリソースを詰め込みます．

# Architecture
- API Gateway
  >Amazon API Gateway は、規模に関係なく、独自の REST および WebSocket API を作成、公開、保守、モニタリング、保護できる AWS のサービスです。
  >AWS または他のウェブサービス、AWS クラウドに保存されているデータにアクセスする、堅牢かつ安全でスケーラブルな API を作成できます。
  >独自のクライアントアプリケーション (アプリ) で使用するための API を作成できます。または、API をサードパーティーのアプリ開発者に対して使用可能にできます。
  https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/welcome.html
- lambda
  >AWS Lambda はサーバーをプロビジョニングしたり管理する必要なくコードを実行できるコンピューティングサービスです。 
  >AWS Lambda は必要時にのみてコードを実行し、1 日あたり数個のリクエストから 1 秒あたり数千のリクエストまで自動的にスケーリングします。
  >必要な操作は、AWS Lambda がサポートするいずれかの言語 (現在は Node.js、Java、C#、Go および Python) でコードを指定するだけです。
  https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html
- Cloud Formation
  >AWS CloudFormation は Amazon Web Services リソースのモデル化およびセットアップに役立つサービスです。
  >リソース管理に割く時間を減らし、AWS で実行するアプリケーションにさらに注力できるようになります。
  >使用するすべての AWS リソース (Amazon EC2 インスタンスや Amazon RDS DB インスタンスなど) を記述するテンプレートを作成すれば、AWS CloudFormation がお客様に代わってこれらのリソースのプロビジョニングや設定を受け持ちます。AWS リソースを個別に作成、設計して、それぞれの依存関係を考える必要はありません。
  https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html
- zappa
  aws で serverless python web service を実現する flask ベースのフレームワーク。
  api gateway + lamdba + cloudwatch events を駆使。
  https://github.com/Miserlou/Zappa

# Dependency
- python 3.6
- zappa
- docker
- python-lambda-local

# SetUp
## IAM
### Environment Variable
CloudFormationで使用する変数をセットするshellを作成します．

```bash:env/env.sh
# CFN STACK NAME
export PROJECT_NAME='AutoEC2'
export IAM_STACK_NAME="${PROJECT_NAME}"-IAM-Stack
```

続いてzappaのDockerが使用するAWSのクレデンシャルファイルを作成します．

```env:.docker/.Credentials.env
[default]
aws_access_key_id = XXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXX/XXXXXXXXXXX
region = ap-northeast-1
```

### Create IAM Role
EC2を操作する権限があるIAMロールを作成します．
以下の3つのファイルを作成して，deploy.shファイルを実行するとCloudFormationがAutoEC2-Lambda-roleという名前のIAMロールを作成してくれます．

```yml:cfn/config/iam.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "Create EC2, API Gateway and LambdaRole"
Resources:
  lambdaIAMRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: 'AutoEC2-Lambda-role'
      ManagedPolicyArns: 
        - 'arn:aws:iam::aws:policy/AmazonEC2FullAccess'
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - "sts:AssumeRole"
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
      Policies:
        - PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Effect: "Allow"
                Resource: "*"
          PolicyName: "lambda"
Outputs: 
  lambdaIAMRoleArn: 
    Description: "Arn of lambdaIAMRole"
    Value: !GetAtt lambdaIAMRole.Arn 
    Export: 
      Name: !Sub "${AWS::StackName}-lambdaIAMRoleArn"
```

```bash:cfn/deploy/createIAM.sh
#!/bin/sh
echo "Creating "${IAM_STACK_NAME}""
aws cloudformation create-stack \
--stack-name ${IAM_STACK_NAME} \
--template-body=file://config/iam.yml \
--capabilities CAPABILITY_NAMED_IAM
echo "Waiting "${IAM_STACK_NAME}" ..."
aws cloudformation wait stack-create-complete \
    --stack-name ${IAM_STACK_NAME}
echo "Success! "${IAM_STACK_NAME}""
```

```bash:cfn/deploy.sh
#!/bin/sh
source ../env/env.sh
./deploy/createIAM.sh
```

# Deploy
zappaというツールをDockerコンテナ内で動かしてデプロイします．
## Ready for Deploy
Dockerfileとdocker-compose.ymlファイルを作成します．

```Dockerfile:.docker/Dockefile
FROM python:3.6-alpine3.8
RUN apk --update add \
    gcc \
    curl \
    groff
WORKDIR /src/api
RUN python -m venv zappa
WORKDIR /src/api
EXPOSE 5000
```

```yml:.docker/docker-compose.yml
version: '3'
volumes: 
  zappa:
    driver: local
services: 
  api:
    build: 
      context: .
    image: autoec2:1.0
    container_name: autoec2
    volumes: 
      - ./src/api:/src/api
      - zappa:/src/api/zappa
      - .credentials.env:/root/.aws/credentials
    command: ash
    ports: 
      - 5000:5000
    tty: true
    working_dir: /src/api
```

Dockerfileではpythonの仮想環境の作成しかしていません．
モジュールディレクトリはホスト側にマウントする必要はないので，docker-compose.ymlのvolumesでzappaディレクトリはdockerのボリューム化をしています，
APIのソースは`./src/api`ディレクトリに配置しています．

初回コンテナ起動時のみ，pipでモジュールをインストールする必要があります，

```bash
$ docker-compose exec api sh
$ source zappa/bin/activate
$ pip install -r requirements.txt
```

これでモジュールのインストールが完了し，モジュールがインストールされたディレクトリはボリューム化されているので二回目のコンテナ起動時からはインストールする必要がありません．

## How to Deploy
コンテナ起動，モジュールのインストールが済んだら下記コマンドで`.src/api`にあるコードをlambdaにデプロイ，APIGatewayの作成ができます．

```bash
$ docker-compose exec api sh deploy.sh dev # or prod
```

アップデート
```bash
$ docker-compose exec api sh update.sh dev # or prod
```

削除
```bash
$ docker-compose exec api sh undeploy.sh dev # or prod
```

それぞれのシェルスクリプトの中は以下のようになっています．
```bash:.docker/src/api/deploy.sh
#!/bin/sh
DEPLOY_ENV=$1
source zappa/bin/activate
zappa deploy $DEPLOY_ENV
```

```bash:.docker/src/api/update.sh
#!/bin/sh
DEPLOY_ENV=$1
source zappa/bin/activate
zappa update $DEPLOY_ENV
```

```bash:.docker/src/api/delete.sh
#!/bin/sh
DEPLOY_ENV=$1
source zappa/bin/activate
zappa delete $DEPLOY_ENV
```

# zappa
zappaは`zappa_settings.json`に基づいてAPIGatewayの作成，lambdaへのデプロイを行います．
profile_nameはAWSのクレデンシャル情報のprofile名です．
prod,staging,devと３つの環境の定義を行っています．

```json:zappa_settings.json
{
    "prod": {
        "apigateway_enabled": true,
        "app_function": "api.app",
        "aws_region": "ap-northeast-1",
        "profile_name": "default",
        "project_name": "auto-ec2",
        "runtime": "python3.6",
        "s3_bucket": "bh-autoec2-src",
        "role_name": "AutoEC2-Lambda-role",
        "lambda_description": "Lambda for running AutoEC 2 API"
    },
    "staging": {
      "apigateway_enabled": true,
      "app_function": "api.app",
      "aws_region": "ap-northeast-1",
      "profile_name": "default",
      "project_name": "auto-ec2",
      "runtime": "python3.6",
      "s3_bucket": "bh-autoec2-src",
      "role_name": "AutoEC2-Lambda-role",
      "lambda_description": "Lambda for running AutoEC 2 API"
  },
    "dev": {
      "apigateway_enabled": true,
      "app_function": "api.app",
      "aws_region": "ap-northeast-1",
      "profile_name": "default",
      "project_name": "auto-ec2",
      "runtime": "python3.6",
      "s3_bucket": "bh-autoec2-src",
      "role_name": "AutoEC2-Lambda-role",
      "lambda_description": "Lambda for running AutoEC 2 API"
  }
}
```

# local devlopment
lambdaの開発で動作確認をするために毎回lambdaへソースコードをデプロイするのは面倒なので，ローカルのDocker環境で動作確認をできるようにします．
下記のコマンドで5000ポートでAPIが動きます．

```bash
$ docker-compose exec api sh lambda-local.sh
```

lambda-local.shは以下のようになっています．

```bash:lambda-local.sh
#!/bin/sh
source zappa/bin/activate
python-lambda-local -f lambda_handler api.py event.json -t 300
```

python-lambda-localツールを使ってローカルでapi.pyを動かします．
-t XXXで何秒間動かすのかを定義する必要があります．

以上になります．

# 終わり
これでEC2をON/OFFできる仕組みが整いました。
cloudwatch等と組み合わせることで、使われていないときはサーバを落とすといった処理ができるようになります。