AWS Lambda向けデプロイ・管理ツール「Lamvery」について

こちらは[今年もやるよ！AWS Lambda縛り Advent Calendar 2015](http://qiita.com/advent-calendar/2015/lambda)の16日の記事ですが、随時更新してます（最終更新： 2016-07-15）

# はじめに
なお、記載されている情報は全て2015-07-15現在のものです。
開発は随時行っているため変わる可能性があります。

# Lamveryとは？
## いいからソース
コチラになります
https://github.com/marcy-terui/lamvery

## 概要
Lambda functionのデプロイやFunctionそのものを含めた周辺機能の設定・管理を支援するツールです。
Python製で自分自身主にPythonで使っていますが、Node.jsも一応対応していて、最低限の動作確認もしてます。

`User-friendly deploy and management tool for AWS Lambda function` というコンセプトの元、一つのFunctionを如何に楽に便利に扱うかと実践的なデプロイフローの確率に主眼を置いて開発しており、あくまでフレームワークではなくツールという位置づけです。

また、こういったツールはFunctionのdeployにフックしないと実現できないような付加機能を付けられることに価値があると思っていて、そういったものを積極的に追加しているのが特長です。

## 読み方
作者は「らんべりー」と読んでます。  

## 語源
元々は[AWSのドキュメントに書いているPythonコードのデプロイ方法](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html)があまりにも面倒で、これをなんとかできないかなと思って作ったので、「**Lam** bda + **v** irtual **e** nv + p **y** thon」[^1]と適当にピックアップして繋げた感じですが、今ではもうちょと短くてかっこ良い名前が良かったなとか思わなくもないですｗ

# 何故他のような同じようなツールとの比較

## lambda-uploader, Kappa等

- 設定ファイルがJSON（YAMLの方がマシ）
- 設定ファイルとコマンドラインオプションの切り分けが不明確
- virtualenvのライブラリのパスがベタ書きだったり、格納効率が悪くてデプロイパッケージ(zip)が肥大化する
    - `.py`,`.pyc`の区別がなく、片方あれば良いのに全てアーカイブに含まれる[^3]

## Apex
こちらをご確認ください。特にrollbackの仕様は一目見て「あ、これダメなやつだ」って分かったので注意した方が良いです。
http://qiita.com/marcy-terui/items/db8dae512af3c553fe72

## 上記を使わずにLamveryを作ったのはなぜか？
- 扱いやすい設定ファイルが欲しかった
    - 設定ファイルはYAML、さらに[jinja2](http://jinja.pocoo.org/docs/dev/) templateとして解釈されるためコードや変数の埋め込みができる
- 多くのツールが実践を意識したパラメータやオプション設計がなされていない
  - 私的な基準ではありますが、設定ファイルとコマンドラインオプションを以下の様なポリシーで分けています
  - 設定ファイル＝バージョン管理されるべきもの
  - コマンドオプション＝一時的に使用するもの
- 実践的なデプロイフローの確率
    - http://qiita.com/marcy-terui/items/900b72efb38f9b26e8f0
- Pythonおよびvirtualenvの仕様にあったデプロイパッケージ作成
    - `.py`はアーカイブに格納せず`.pyc`のみ格納など[^4]

などなど、手に馴染んで実践的に使えるツールが欲しかったのと、使っていく中で色々欲しくなりそうだったのでいっその事自分で作っちゃおうかなと。

# ざっくり特長とできること一覧
- YAML + Jinja2テンプレートによる設定記述
- 格納効率に優れたパッケージ作成（Pythonのみ）
- 安全なデプロイおよびロールバック
- デプロイフック
- 環境変数の受け渡し
- KMSを用いた機密情報の受け渡し
- KMSを用いた機密ファイルの受け渡し
- API GatewayへのAPIデプロイおよび管理
- CloudWatch Eventsによるスケジュール実行の設定と管理
- CloudWatch Logsログ閲覧

# 使い方
## インストール

- PyPI  
アーカイブの効率が悪くなりますし、ディレクトリが汚くなるのでFunction本体を置くディレクトリ自体ではなくサブディレクトリにvirtualenv仮想環境を展開することをオススメします。

```shell-session
virtualenv -p <path-to-python2.7> .venv
. .venv/bin/activate
pip install lamvery
```

- Apt

```shell-session
echo "deb https://dl.bintray.com/willyworks/deb trusty main" | sudo tee -a /etc/apt/sources.list
sudo apt-get update
sudo apt-get install lamvery
export PATH=/opt/lamvery/bin:$PATH
```

- Yum

```shell-session
echo "
[bintraybintray-willyworks-rpm]
name=bintray-willyworks-rpm
baseurl=https://dl.bintray.com/willyworks/rpm/centos/\$releaserver/\$basearch/
gpgcheck=0
enabled=1
" | sudo tee -a /etc/yum.repos.d/bintray-willyworks-rpm.repo
sudo yum install lamvery
export PATH=/opt/lamvery/bin:$PATH
```

## 設定方法
まず、以下のコマンドで設定ファイルの雛形を作成します。

```shell-session
lamvery init
```
いくつかの設定ファイルの雛形が出来上がるので、それを編集します。
上述のとおり、[jinja2](http://jinja.pocoo.org/docs/dev/) templateとして解釈されるため`{{ }}`で囲むと変数展開、`{% %}`で囲むとコードの埋め込みができます。
また、よく使われると思われる環境変数については簡潔に書けるよう`env`という変数に入れておきました。

以下、設定例です。

### 基本設定

```yaml:.lamvery.yml
profile: private
region: us-east-1
versioning: true
default_alias: test
clean_build: false
configuration:
  name: lamvery-test
  runtime: python2.7
  role: {{ env['AWS_LAMBDA_ROLE'] }}
  handler: lambda_function.lambda_handler
  description: This is sample lambda function.
  timeout: 10
  memory_size: 128
  vpc_config:
    subnets:
    - subnet-cadf2993
    security_groups:
    - sg-4d095028
```

### イベント(CloudWatch Events)設定

```yaml:.lamvery.event.yml
rules:
- name: foo
  description: bar
  schedule: 'rate(5 minutes)'
  targets:
  - id: test-target-id
    input:
      this:
      - is: a
      - sample: input
```

### 機密情報(KMS)に関する設定

```yaml:.lamvery.secret.yml
key_id: xxxx-yyyy-zzzz
cipher_texts: {}
secret_files: {}
```

### 除外リスト

```yaml:.lamvery.exclude.yml
- ^\.lamvery\.yml$
- ^\.lamvery\.event\.yml$
- ^\.lamvery\.secret\.yml$
- ^\.lamvery\.exclude\.yml$
```

### アクションフック

```yaml:.lamvery.hook.yml
build:
  pre:
  - pip install -r requirements.txt -t ./
  post: []
```

### API Gateway

```yaml:.lamvery.api.yml
api_id: myipugal74
stage: dev
cors:
  origin: '*'
  methods:
  - GET
  - OPTION
  headers:
  - Content-Type
  - X-Amz-Date
  - Authorization
  - X-Api-Key
configuration:
  swagger: '2.0'
  info:
    title: Sample API
  schemes:
  - https
  paths:
    /:
      get:
        produces:
        - application/json
        parameters:
        - name: sample
          in: query
          required: false
          type: string
        responses:
          '200':
            description: 200 response
            schema:
              $ref: '#/definitions/Sample'
  definitions:
    Sample:
      type: object
```

## 設定項目

### 基本設定

- profile  
`aws configure --profile hoge`で設定する `hoge` の部分です。  
Credential情報やRegionの設定が入るやつです。  
http://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-getting-started.html  
指定しない場合（`null`と書く or 項目ごと削除）は`AWS SDK(boto3)`の挙動に準じます（EC2上ならIAM Role、ローカルだと`AWS_SECRET_KEY`等の環境変数とか）
- region  
そのまんまリージョン名
- versioning  
  バージョニングの有効(`true`)、無効(`false`)
- default_alias  
オプションで指定しない場合のエイリアス
- clean_build  
デプロイパッケージ作成時にソースコードをテンポラリのディレクトリに移動した上でパッケージを作成します。ビルドにフックしてライブラリなどをインストールしてデプロイしたい場合などに作業ディレクトリを汚さずに済みます（フックについては後述）
- configuration  
基本的にLambdaの設定項目そのままです。
  - name  
  Functionの名前です。APIでFunctionの特定に使われるため重複不可。
  - runtime  
  Functionのランタイム。現状は `python2.7` or `nodejs`をサポートしています。
  - role  
  Lambda実行時に与えるIAM RoleのARN。
  - handler  
  Functionのエントリーポイントとなる関数の名前空間。
  - description  
  Functionの説明。
  - timeoute
  Functionの最大実行時間（秒）
  - memory_size  
  Function（のコンテナ）に与えるメモリサイズ（MB）
  - vpc_config  
  VPC内で動かすための設定  
  `subnets`にサブネットID,`security_groups`にセキュリティグループのIDをそれぞれリストで指定します。
    

### イベント(CloudWatch Events)設定
- name  
Eventの一意なルール名
- description  
ルールの説明
- targets  
Eventのターゲット（複数可）
  - id  
  ターゲットの一意なID
  - input  
  ターゲットに渡す入力（Functionの`event`引数）に入る
  - input_path  
  `input`との併用不可。JSONPath形式で、`input`を指定しない場合にデフォルトで渡される引数の中から任意の部分だけ扱いたい場合に使用する

### 機密情報に関する設定

- key_id  
[KMS](https://aws.amazon.com/jp/kms/)の秘密鍵ID（ARNではなくIDの部分だけ）
- cipher_texts  
特定の文字列をKMSによって暗号化された暗号文が入ります。基本的にコマンドから設定するため、この項目を手動で書くことはまず無いです。
ここに入った情報がFunctionへ渡され、機密情報を安全に受け渡すことができます（詳しくは後述）
- secret_files  
特定のファイルの中身をKMSによって暗号化された暗号文とが入ります。基本的にコマンドから設定するため、この項目を手動で書くことはまず無いです。
ここに入った情報がFunctionへ渡され、機密情報が含まれたファイルを安全に受け渡すことができます（詳しくは後述）

### 除外リスト
ただひたすら正規表現で除外したいファイルのパスを列挙する

### アクションフック
現状は `build` のみサポートします。 `build` はデプロイパッケージを作成する処理です。
`pre` に前、 `post` に後に実行させたいコマンドを列挙します。

### API Gateway

- api_id  
API GatewayにおけるAPIのID(rest-api-id)です。  
`api`コマンドでAPIを初回作成する際に `-w` オプションを付けると自動で記入されます。
- stage  
APIをデプロイするステージ名です。`api` コマンド実行時にオプションで指定されない場合に使用されます。  
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/stages.html
- cors  
APIの各リソースに自動的に付与したいCORS (Cross-Origin Resource Sharing)オプションを指定します。  
    - origin  
    `Access-Control-Allow-Origin` に指定するオプション
    - headers  
    `Access-Control-Allow-Headers` に指定するオプション
    - methods  
    `Access-Control-Allow-Methods` に指定するオプション
- configuration  
Swagger形式でAPIの設定を記述します。  
Lambdaファンクションを実行するためやCORSの設定等に必要な `x-amazon-apigateway-integration` の内容は、設定ファイルに明示的に記載されていない場合は自動的に付与されます。Mapping Templateも[全てのパラメータを渡すもの](http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html)を自動で設定します（この挙動はオプションで抑制可能です）


## コマンド
コマンドライン引数については、よく使用するもののみは記載しています。
全てを知りたい場合は[README](https://github.com/marcy-terui/lamvery/blob/master/README.md#options)からご確認ください。

### init
```shell-session
$ lamvery init -h
usage: lamvery init [-h] [-c CONF_FILE]

optional arguments:
  -h, --help            show this help message and exit
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
```

各種設定ファイルの雛形を作成するインストール後に最初に実行するコマンドです。

### generate
```shell-session
$ lamvery generate -h
usage: lamvery generate [-h] [-c CONF_FILE] -k KIND

optional arguments:
  -h, --help            show this help message and exit
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -k KIND, --kind KIND  The kind of the file # accepts "function"
```

各種プログラムの雛形を生成するコマンドです。
`-k` で生成するファイルの種別を指定しますが現状は `function` でFunctionの雛形のみ生成可能です。

### build
```shell-session
$ lamvery build -h
usage: lamvery build [-h] [-c CONF_FILE] [-s] [-l] [-e ENV]

optional arguments:
  -h, --help            show this help message and exit
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -s, --single-file     Only use the main lambda function file
  -l, --no-libs         Archiving without all libraries
  -e ENV, --env ENV     Environment variables that pass to the function
```

ソースコードとライブラリのアーカイブ（ZIP）ファイルを作成します。
確認用や変則的なフローを組む場合向け。
ファイル名は`Function名.zip`となります。設定ファイルがない場合などは`実行したディレクトリ名.zip`です。

### deploy
```shell-session
$ lamvery deploy -h
usage: lamvery deploy [-h] [-a ALIAS] [-c CONF_FILE] [-d] [-s] [-l] [-p]
                      [-e ENV]

optional arguments:
  -h, --help            show this help message and exit
  -a ALIAS, --alias ALIAS
                        Alias for a version of the function
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -d, --dry-run         Dry run
  -s, --single-file     Only use the main lambda function file
  -l, --no-libs         Archiving without all libraries
  -p, --publish         Publish the version as an atomic operation
  -e ENV, --env ENV     Environment variables that pass to the function
```

以下の全てをまとめて行います。

- ソースコードとライブラリのアーカイブを作成
- アーカイブの中に機密情報の暗号文を埋め込む
- アーカイブの中にオプションで指定された環境変数を埋め込む
- Lambdaの実行設定を更新
- アーカイブをアップロード
- 前のバージョンに別のエイリアスを付ける（指定されている場合）
- 新しいバージョンにエイリアスを付ける（指定されている場合）

また、日頃お世話になっている[codenize.tools](http://codenize.tools/)で重宝しているDry run（更新せずに差分だけ表示する）オプションを付けました。`-d`または`--dry-run`となっています。

`-e` または `--env` で環境変数を指定できます。

### rollback
```shell-session
$ lamvery rollback -h
usage: lamvery rollback [-h] [-a ALIAS] [-c CONF_FILE] [-v VERSION]

optional arguments:
  -h, --help            show this help message and exit
  -a ALIAS, --alias ALIAS
                        Alias for a version of the function
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -v VERSION, --version VERSION
                        Version of the function
```

`deploy`の巻き戻しを行います。Dry runにも対応しています。
Rollbackの仕様については[こちら](http://qiita.com/marcy-terui/items/db8dae512af3c553fe72#deploy--rollback)に記載しています。

### configure
```shell-session
$ lamvery configure -h
usage: lamvery configure [-h] [-c CONF_FILE] [-d]

optional arguments:
  -h, --help            show this help message and exit
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -d, --dry-run         Dry run
```

ソースコードの更新は行わず、Functionの設定のみ更新します。もちろんDry run対応。
ただし、バージョニングしている場合は新しいバージョンを発行するまで設定は反映されませんので注意してください。
デプロイ時にも設定を同期するので、ほぼバージョニングしない場合にしか使えないコマンドですｗ

### set-alias
```shell-session
$ lamvery set-alias -h
usage: lamvery set-alias [-h] [-a ALIAS] [-c CONF_FILE] [-d] [-v VERSION]
                         [-t TARGET]

optional arguments:
  -h, --help            show this help message and exit
  -a ALIAS, --alias ALIAS
                        Alias for a version of the function
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -d, --dry-run         Dry run
  -v VERSION, --version VERSION
                        Version of the function
  -t TARGET, --target TARGET
                        The alias of the version that is targeted for setting
                        alias
```

エイリアスを設定します。こちらもDry run対応。
`-a` or `--alias`でエイリアス名、`-v` or `--version`でエイリアスを付けるバージョンを指定します。


### encrypt
```shell-session
$ lamvery encrypt -h
usage: lamvery encrypt [-h] [-c CONF_FILE] [-n SECRET_NAME] [-s] text

positional arguments:
  text                  The text value to encrypt

optional arguments:
  -h, --help            show this help message and exit
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -n SECRET_NAME, --secret-name SECRET_NAME
                        The name of the secret value
  -s, --store           Store encripted value to the configuration file
                        (default: .lamvery.secret.yml)
```

[KMS](https://aws.amazon.com/jp/kms/)を使って与えられた値を暗号化します。
`-n` or `--name`で取り出す際に使用する名前を指定し、`-s` or `--store`を付けるとその名前で設定ファイルに暗号文が登録されます。

### encrypt-file
```shell-session
$ lamvery encrypt-file -h
usage: lamvery encrypt-file [-h] [-c CONF_FILE] -p PATH [-s] file

positional arguments:
  file                  The file path to encrypt

optional arguments:
  -h, --help            show this help message and exit
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -p PATH, --path PATH  The path to put the decrypted file in the function
  -s, --store           Store encripted value to the configuration file
                        (default: .lamvery.secret.yml)
```

[KMS](https://aws.amazon.com/jp/kms/)を使って指定ファイルを暗号化します。
`-n` or `--name`でFunctionで扱う際に使用するファイル名前を指定し、`-s` or `--store`を付けるとその名前で設定ファイルに暗号文とファイル名が登録されます。

### decrypt
```shell-session
$ lamvery decrypt -h
usage: lamvery decrypt [-h] [-c CONF_FILE] [-n SECRET_NAME]

optional arguments:
  -h, --help            show this help message and exit
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -n SECRET_NAME, --secret-name SECRET_NAME
                        The name of the secret value
```

設定ファイル内の暗号文を復号します。
`-n` or `--name`で暗号化時に決めた名前を指定します。
`decrypt-file` は元ファイル見ればええやんって話なので実装してませんｗ


### events
CloudWatch Eventsの設定を行います。もちろんDry run対応です。
差分更新となっており、Event設定がない場合は作成、あれば更新します。
Functionに紐づくEventの中で記述されていないものは削除されますが、別のFunctionに紐づくものはTargetから外すのみで削除は行いません。

```shell-session
lamvery events [-k]
```

### invoke
```shell-session
$ lamvery invoke -h
usage: lamvery invoke [-h] [-a ALIAS] [-c CONF_FILE] [-v VERSION] json

positional arguments:
  json                  The JSON string or file that pass to the function

optional arguments:
  -h, --help            show this help message and exit
  -a ALIAS, --alias ALIAS
                        Alias for a version of the function
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -v VERSION, --version VERSION
                        Version of the function
```

Functionを起動します。
起動した結果のLogは標準出力に吐かれます。
引数で入力を指定します。JSONフォーマットで渡せばそのまま渡され、JSONの書かれたファイルのパスを指定するとその中身を渡します
バージョン指定（`-v`）やエイリアス指定（`-a`）もできます。

### logs
```shell-session
$ lamvery logs -h
usage: lamvery logs [-h] [-c CONF_FILE] [-f] [-F FILTER] [-i INTERVAL]
                    [-s START]

optional arguments:
  -h, --help            show this help message and exit
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -f, --follow          Watch the log events and updates the display (like
                        `tail -f`)
  -F FILTER, --filter FILTER
                        Filtering pattern for the log messages
  -i INTERVAL, --interval INTERVAL
                        Intervals(seconds) to watch the log events
  -s START, --start START
                        Time to start the log events watching
```

CloudWatch Logsに吐かれるfunctionのログを閲覧します。  
`-f` で `tail -f` 的に流し続けたり、`-s` で開始日時を指定して閲覧できます。

### api
```shell-session
$ lamvery api -h
usage: lamvery api [-h] [-c CONF_FILE] [-d] [-n] [-r] [-s STAGE] [-w]

optional arguments:
  -h, --help            show this help message and exit
  -c CONF_FILE, --conf-file CONF_FILE
                        Configuration YAML file (default: .lamvery.yml)
  -d, --dry-run         Dry run
  -n, --no-integrate    Without automatic integration
  -r, --remove          Remove your API
  -s STAGE, --stage STAGE
                        The name of the stage in API Gateway
  -w, --write-id        Write the id of your API to the configuration file
                        (default: .lamvery.api.yml)
```

API GatewayにAPIをデプロイします。`-r` を付けると削除します。  
`-n` で `x-amazon-apigateway-integration` 等を自動設定する挙動を抑制し、設定ファイルに書かれたそのままでデプロイできます。

## 機密情報を使用するLambda functionの作成の流れ（参考）

#### 1. KMSで秘密鍵を作ります。  
https://docs.aws.amazon.com/kms/latest/developerguide/create-keys.html

#### 2. Function実行時のIAM Roleに以下の権限を追加します。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:kms:us-east-1:<your-account-number>:key/<your-key-id>"
            ]
        }
    ]
}
```

#### 3. 設定ファイルやFunctionの雛形を生成します
```shell-session
lamvery init
lamvery generate -k function
````

#### 4. 以下のように設定ファイルに1で作った鍵のIDを設定します

```yaml:.lamvery.yml
 profile: default
  region: us-east-1
  configuration:
    name: sample_lambda_function
    runtime: python2.7 # or nodejs
    role: arn:aws:iam::000000000000:role/lambda_basic_execution
    handler: lambda_function.lambda_handler
    description: This is sample lambda function.
    timeout: 10
    memory_size: 128
```

```yaml:.lamvery.secret.yml
key_id: a5ed61a9-fa57-4ebf-9b3f-457b95de05ce # <-ここ！！！
cipher_texts: {}
```

#### 5. 機密情報を暗号化して保存します

``` shell-session
lamvery encrypt -s -n foo "This is a secret"
```

#### 6. ソースを書きます

```py:lambda_function.py
import lamvery

def lambda_handler(event, context):
    print(lamvery.secret.get('foo'))
```

```js:lambda_function.js
var lamvery = require('./lamvery.js');

exports.lambda_handler = function(event, context) {
    lamvery.secret.get('foo', function(err, data) {
        console.log(data);
    });
}
```

#### 7. Deploy!!

```shell-session
lamvery deploy
```

#### 8. Functionを起動

```shell-session
lambery invoke {}
```


以下のように復号された結果が得られます。

```
START RequestId: 13829c9c-9f13-11e5-921b-6f048cff3c2d Version: $LATEST
This is a secret
END RequestId: 13829c9c-9f13-11e5-921b-6f048cff3c2d
```

# 最後に
かなり機能は充実してきましたが、まだまだ色々追加する予定です！
良かったら使ってみてフィードバックをいただけると嬉しいです :-)

フィードバックはコチラから↓
https://github.com/marcy-terui/lamvery


[^1]: Pythonのライブラリ等が隔離された仮想環境を提供するライブラリ
      https://pypi.python.org/pypi/virtualenv
[^2]: 2015.12.16現在。API無い(内部的にはあるんでしょうが)とか「APIで会話しろ」という名言を持つAWSさんにあるまじき（ｒｙ
      https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/getting-started-scheduled-events.html
[^3]: `.py`は素のソース、`.pyc`はコンパイル済みのソース
[^4]: ベンチとってないのでアレですが、コンパイルが短縮されるので起動時間がごくわずかですが向上するかも？
[^5]: virtualenvを使わないとローカルだとシステムライブラリでたまたま動いてたソースが動かなくて・・・みたいなことになるので使わない選択肢なんてない・・・ですよね？