c4se記:さっちゃんですよ☆

.。oO(さっちゃんですよヾ(〃l _ l)ノ゙☆)

.。oO(此のblogは、主に音樂考察Programming に分類されますよ。ヾ(〃l _ l)ノ゙♬♪♡)

音樂は SoundCloud に公開中です。

考察は現在は主に Scrapbox で公表中です。

Programming は GitHub で開發中です。

Pythonアプリケーション毎に環境を分ける (pyenv + virtualenv (or venv) + pip)

Ruby では rbenv + Bundler でやる。Haskell では Stack で。Elixir では exenv + Hex で。Node.js では ndenv + npm で。PHP では phpenv + composer で。

API Gateway と Lambda の組み合はせでリリースするバージョンを制御する の樣に Lambda で色々やってゐるので此の邊りを整へた。

Ruby Python
実行環境のバージョンを分ける rbenv pyenv + virtualenv
依存ライブラリをインストールする gem pip
依存ライブラリのバージョンを固定する Bundler pip + virtualenv
依存ライブラリを更新する Bundler pip

pip の色んなオプションを駆使する事に成る。

実行環境のバージョンを分ける

ではやる。

システムには Python2 と Python3 を入れてある。此れは開發には使はないが、システムの色んな何かが依存してゐるので、其の儘にしておく。必ず其の儘にしておく。

pyenv を入れる。anyenv を入れてあるので其れを使ふ。

anyenv install pyenv
exec $SHELL -l

Python の 2 系と 3 系を両方入れる。本日の最新版は 3.5.2 と 2.7.12 だ。

pyenv install 2.7.12
pyenv install 3.5.2

pyenv は pip も入れてくれる。

virtualenv を入れる。pyenv のプラグインが在るので此れを使ふ。

git clone https://github.com/yyuu/pyenv-virtualenv.git $(pyenv root)/plugins/pyenv-virtualenv
echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.zprofile
exec $SHELL -l

example と云ふプロジェクトを開發するとしやう。AWS Lambda なので 2 系である。つらい。

mkdir example
cd example
pyenv virtualenv 2.7.12 example
pyenv local example

此れで example と云ふ環境が virtualenv 側に作られた。virtualenv のオプションに渡した example と云ふ名は、システムの中では一意でなければならない。此の名は他人と共有しないので、自分の事だけを考へて附ければよい。環境の設定を共有しないのは Ruby からの Python の違ひだ。開發環境を閉じ込めるのは Ruby では Bundler だけでやるが、Python では virtualenv と pip に分離されてゐる。環境を閉じ込めるのは virtualenv でやり、環境の設定は pip で行なふ。pip で設定したところは、他人と共有する。

pyenv-virtualenv は virtualenv を pyenv と協働させてくれる。example ディレクトリの .python-version に virtualenv で作った環境の名が書かれてある。此のディレクトリ下に cd してくる度に virtualenv の環境が切り替はる。べんり。また 2 系では virtualenv を、3 系では Python 附属の venv を使って呉れるらしい。べんり。

環境を消すには、

pyenv uninstall example

する。

依存ライブラリをインストールする

依存ライブラリを入れてゆく。virtualenv で環境が閉じ込められてゐるので、何も考へず example ディレクトリ下でインストールすればよい。

pip install boto3
pip install invoke
pip install watchdog
pip install unittest2
# 等

AWS Lambda では依存ライブラリを纏めて zip に入れて上載しなければならない。Lambda の実行時に使ふ依存ライブラリは、virtualenv の環境ではなく、プロジェクトのディレクトリにインストールしなければならない。

pip install pytz -t .
# 等

boto3 は AWS 環境に既に在るので、開發時の依存関係扱ひである。

import boto3
from pytz import timetone

def lambda_handler(event, context):
    # ゴニョゴニョ

序でに云ふと、

rm -f ~/Desktop/example.zip
zip -r ~/Desktop/example.zip .

AWS Lambda に上載できる zip が作られる。

依存ライブラリのバージョンを固定する

pip freeze と云ふコマンドで Gemfile.lock 相当のものが出力される。

pip freeze > requirements.txt
argh==0.26.2
boto3==1.3.1
botocore==1.4.41
docutils==0.12
futures==3.0.5
invoke==0.13.0
jmespath==0.9.0
linecache2==1.0.0
pathtools==0.1.2
python-dateutil==2.5.3
PyYAML==3.11
six==1.10.0
traceback2==1.4.0
unittest2==1.1.0
watchdog==0.8.3

ファイル名は任意である。requirements.txt や requirements-dev.txt, requirements-test.txt, requirements27.txt 等のファイル名にする場合が多いやうだ。

requirements.txt を git に含める。記録したライブラリをインストールするには、

pip install -r requirements.txt

とする。

依存ライブラリを更新する

全て更新する場合は、

pip install --upgrade -r requirements.txt
pip freeze > requirements.txt

一つだけ更新する場合は、

pip install --upgrade boto3
pip freeze > requirements.txt

アンインストールするには、

pip uninstall boto3
pip freeze > requirements.txt

総括

安心して virtualenv (or venv) に任せやう。

ハイ。

AWS EC2インスタンスの一覧を.ssh/config形式で吐く (Crystalで)

aws ec2 describe-instancesJSON として取得する。

JSON.mapping で Crystal の class に対応させる。

よしなに。

#!/usr/bin/env crystal run

require "json"

module Instances
  class Tag
    JSON.mapping(
      value: {type: String, key: "Value"},
      key: {type: String, key: "Key"},
    )
  end

  class Instance
    JSON.mapping(
      private_ip_address: {type: String, key: "PrivateIpAddress", nilable: true},
      tags: {type: Array(Tag), key: "Tags", nilable: true},
    )
  end

  class Reservation
    JSON.mapping(
      instances: {type: Array(Instance), key: "Instances"},
    )
  end

  class Instances
    JSON.mapping(
      reservations: {type: Array(Reservation), key: "Reservations"},
    )
  end
end

instances = Instances::Instances.from_json(`aws ec2 describe-instances --profile example-production --output json`)
hosts = instances
  .reservations
  .flat_map(&.instances)
  .map { |instance| {instance.private_ip_address, instance.tags.try(&.find { |tag| tag.key == "Name" }).try(&.value)} }
  .select { |private_ip_address, name| private_ip_address && name }
  .map { |private_ip_address, name| "Host #{name}\n  HostName #{private_ip_address}" }
  .join("\n")
puts hosts
config = File.read("#{ENV["HOME"]}/.ssh/config", "UTF-8")
config += "\n### EXAMPLE BEGIN ###\n### EXAMPLE END ###" unless config =~ %r(^### EXAMPLE BEGIN ###\n.+^### EXAMPLE END ###$)m
config = config.sub(%r(^### EXAMPLE BEGIN ###\n.+^### EXAMPLE END ###$)m, "\n### EXAMPLE BEGIN ###\n#{hosts}\n### EXAMPLE END ###")
File.write("#{ENV["HOME"]}/.ssh/config", config, File::DEFAULT_CREATE_MODE, "UTF-8")

# vim:set ft=crystal:

cf. 殺したサーバーを一覧する

API GatewayとLambdaの組み合はせでリリースするバージョンを制御する

追記 20161018 Serverless Framework をお勧めします。1.0 が出ました。 Serverless Framework (1.0.0-beta2) が在ります - c4se 記:さっちゃんですよ ☆

サーバーレスって奴です。

API Gateway から Lambda を呼ぶ

此の節は前説なので、図だけ見つつ次の節へ読み飛ばすことも出來ます。

version を制御しない所からはじめてみます。

を実装してゆきます。

f:id:Kureduki_Maari:20160726121749p:plain

事前に以下のものを作成します。

上では「sample.example」と成ってゐるドメインを取得しておいてください。例へば私であれば「c4se.jp」です。其れを Route53 に Host zone として登録しておきます。

resource "aws_route53_zone" "primary" {
  name = "sample.example"
}

resource "aws_route53_record" "primary_root_a_record" {
  name = "sample.example"
  records = ["xxx.xxx.xxx.xxx"]
  ttl = "300"
  type = "A"
  zone_id = "${aws_route53_zone.primary.zone_id}"
}

記法は Terraform です。

HTTPS が使へるやうに、*.sample.example 等の証明書を取得しておいてください。

DynamoDB のテーブルを設計し作成します。DynamoDB でなくとも RDS でも何でも好ろしいです。御好きな奴でやるので、此所では DynamoDB でやります。

resource "aws_dynamodb_table" "example_user" {
  name = "example_user"
  attribute {
    name = "id"
    type = "S"
  }
  hash_key = "id"
  read_capacity = 1
  write_capacity = 1
}

IAM ロールを作ります。Lamnda 函數に割り当てるロールです。Lambda に対し、Lambda 実行権限 (arn:aws:iam::aws:policy/AWSLambdaExecute) をアタッチし、DynamoDB の讀み書き権限 (dynamodb:GetItem, dynamodb:PutItem) を與へます。「lambda_example_exec」の名で IAM ロールを作ってみます。

resource "aws_iam_policy_attachment" "AWSLambdaExecute" {
  name = "AWSLambdaExecute"
  policy_arn = "arn:aws:iam::aws:policy/AWSLambdaExecute"
  roles = [
    "${aws_iam_role.lambda_example_exec.name}"
  ]
}

resource "aws_iam_role" "lambda_example_exec" {
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Effect": "Allow",
      "Principal": {
        "Service": ["lambda.amazonaws.com"]
      }
    }
  ]
}
EOF
  name = "lambda_crud_random_exec"
}

resource "aws_iam_role_policy" "lambda_example_exec" {
  name = "lambda_example_exec"
  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem"
      ],
      "Effect": "Allow",
      "Resource": [
        "${aws_dynamodb_table.example.arn}"
      ]
    }
  ]
}
EOF
  role = "${aws_iam_role.lambda_example_exec.id}"
}

aws_iam_policy_attachment は一つのポリシーに一回しか使へないので Terraform のコードを分割する時に aws_iam_policy_attachment もバラさないやう注意してください。

此所迄が事前準備です。やっと Lambda 函數を作ります。「example」といふ Lambda 函數を作りませう。node.js でも Python でも Java でも node.js にぶら下げた任意のバイナリ (多くの Lambda 用ツールは此の方法で Go を実行可能にしてゐます。Crystal でも Haskell でも何でも) でも何でもよいので、此所では Python でコードを書く事にします。Python で書いた理由はAnsible モジュールを書いて Python に慣れたからです。

加へて API Gateway からの呼び出しを許可しておきます。

resource "aws_lambda_function" "example" {
  description = "Example."
  filename = "example.zip"
  function_name = "example"
  handler = "main.lambda_handler"
  memory_size = 128
  role = "${aws_iam_role.lambda_example_exec.arn}"
  runtime = "python2.7"
  source_code_hash = "${base64sha256(file("example.zip"))}"
  timeout = "${var.timeout}"
}

resource "aws_lambda_permission" "example_apigateway" {
  action = "lambda:InvokeFunction"
  depends_on = ["aws_lambda_function.example"]
  function_name = "${aws_lambda_function.example.function_name}"
  principal = "apigateway.amazonaws.com"
  statement_id = "example_apigateway"
}

先に作った IAM ロールを Lambda 函數に當ててゐます。

Lamnbda のコードは此んな感じです。

import boto3
import logging
import re
import traceback

logger = logging.getLogger()
logger.setLevel(logging.INFO)

class EventValidationException(Exception):
    def __str__(self):
        return "400: %s" % self.message

def get_user(event):
    for key in ["id"]:
      if key not in event:
          raise EventValidationException("`id` should be required.")
    # 色々やる
    return {
        "id": id,
        "name": name
    }

def post_user(event):
    for key in ["id", "name"]:
      if key not in event:
          raise EventValidationException("`%s` should be required." % key)
    if not re.match(r"\A[0-9A-Za-z]{8,64}\Z", event["id"]):
        raise EventValidationException("`id` should match [0-9A-Za-z]{8,64}")
    # 色々やる
    return {
        "id": id,
        "name": name
    }

def lambda_handler(event, context):
    try:
        logger.info(event)
        if event["_method"] == "GET":
            get_user(event)
        elif event["_method"] == "POST":
            post_user(event)
        else:
            raise NotImplementedError()
    except Exception as e:
        logger.error("%s\n%s" % (e, traceback.format_exc()))
        m = re.match(r"\A\d{3}: ", e.__str__())
        if (not m) or (m and m.group(0)[0:3] not in ["400", "500"]):
            e = Exception("500: %s" % e)
        raise e

DynamoDB とごにょごにょやるのは boto3 を呼び出して色々やるだけです。「色々やる」の所で色々やってください。

此の Python コードを「main.py」として保存し、zip に格納します。

example.tf
example.zip
├ main.py
└ other.py等

先に Lambda 函數のハンドラを handler = "main.lambda_handler" と定義しました。此れはおそらく Lambda 側で from main import lambda_hander として呼ばれるので、さうできるやうにしませう。

正常実行時には dict を返します。異常終了には Exception を raise します。node.js だと異常終了時には context.fail("Message.") します。後で API Gateway でエラーのレスポンスを設定する時に関ります。

さて API Gateway をやります。先は長い。以下のものを作る必要があります。

aws_api_gateway_rest_api (example)
├ aws_api_gateway_deployment (prod)
└ aws_api_gateway_resource (/user)
    └ aws_api_gateway_resource (/user/{id})
        ├ aws_api_gateway_method (GET)
        │  ├ aws_api_gateway_integration
        │  ├ aws_api_gateway_method_response
        │  └ aws_api_gateway_integration_response
        └ aws_api_gateway_method (POST)
            ├ aws_api_gateway_integration
            ├ aws_api_gateway_method_response
            └ aws_api_gateway_integration_response

f:id:Kureduki_Maari:20160726121808p:plain

example と云ふ APIAPI Gateway に作ります。

Terraform でいふ「deployment」は AWS コンソールでいふと「ステージ」です。development と production 等と分ける事が出來ます。今は「prod」だけ作ります。

rest_apiAPI の集合です。rest_api に resource で URL を作り、method をぶら下げて REST API を作ります。

method では method、integration、integration_response、method_response の四つの部品を組みます。method と integration で入力を処理し、method_response と integration_response で出力を処理します。method と method_response で HTTP リクエストからの入出力を始末し認証やパラメータや HTTP ステータスコードの許可を行います。integration と integration_response でバックエンドとの入出力を司りパラメータを JSON に変換したりエラーを HTTP ステータスコードに変換したりします。今回のバックエンドは Lambda です。

  • var.aws_region
  • var.aws_account_id

は適当に設定してあるものとします。

resource "aws_api_gateway_rest_api" "example" {
  description = "Dummy endpoint for example."
  name = "example"
}

resource "aws_api_gateway_deployment" "example_prod" {
  depends_on = [
    "aws_api_gateway_integration.example_user_id_get",
    "aws_api_gateway_integration.example_user_id_post"
  ]
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  stage_description = "production"
  stage_name = "prod"
}

# /user
resource "aws_api_gateway_resource" "example_user" {
  parent_id = "${aws_api_gateway_rest_api.example.root_resource_id}"
  path_part = "user"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

# /user/{id}
resource "aws_api_gateway_resource" "example_user_id" {
  parent_id = "${aws_api_gateway_resource.example_user.id}"
  path_part = "{id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

# {{{ GET /user/{id}

resource "aws_api_gateway_method" "example_user_id_get" {
  authorization = "NONE"
  http_method = "GET"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

resource "aws_api_gateway_integration" "example_user_id_get" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  integration_http_method = "POST"
  request_templates = {
    "application/json" = <<EOF
{
  "id": "$input.params('id')",
  "_method": "GET"
}
EOF
  }
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  type = "AWS"
  uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${var.aws_region}:${var.aws_account_id}:function:${aws_lambda_function.example.function_name}/invocations"
}

resource "aws_api_gateway_method_response" "example_user_id_get_200" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "example_user_id_get_400" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "400"
}

resource "aws_api_gateway_method_response" "example_user_id_get_500" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "500"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_200" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_200.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_400" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "400: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_400.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_500" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "500: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_500.status_code}"
}

# }}} GET /user/{id}

# {{{ POST /user/{id}

resource "aws_api_gateway_method" "example_user_id_post" {
  authorization = "NONE"
  http_method = "POST"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

resource "aws_api_gateway_integration" "example_user_id_post" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  integration_http_method = "POST"
  request_templates = {
    "application/json" = <<EOF
{
  "id": "$input.params('id')",
  "name": "$input.path('$.name')",
  "_method": "POST"
}
EOF
  }
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  type = "AWS"
  uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${var.aws_region}:${var.aws_account_id}:function:${aws_lambda_function.example.function_name}/invocations"
}

resource "aws_api_gateway_method_response" "example_user_id_post_200" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "example_user_id_post_400" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "400"
}

resource "aws_api_gateway_method_response" "example_user_id_post_500" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "500"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_200" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_200.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_400" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "400: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_400.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_500" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "500: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_500.status_code}"
}

# }}} POST /user/{id}

かう成ります。

request_template 等の記法は VTL (Velocity Template Language) です。オブジェクトに対しては Java のメソッドが其の儘呼べます。

reourse に {id} として設定してあると、method は URL に書かれたパラメーターを自動で integration 迄渡して呉れます。さうでないパラメーターは、GET のクエリーも POST の body もヘッダーも全て method と integration に明示しなければなりません。其れと API GatewayJSON を解するのですが、JSON しか解さない爲、POST 等の body が application/json の場合は素通しできますが、application/x-www-form-urlencoded で來る場合はすざましい request_template を書かねばなりません。今の所は。

レスポンスも全てを設定しなければいけません。Lambda から正常終了で返された JSON を其の儘返すだけならば、200 番の method_response と 200 番のデフォルト integration_response を設定するだけで濟みます。其の場合エラーはスタックトレース迄含めて 500 番でユーザーに返されます。エラーを返すには、Exception の message に正規表現を掛けて、ステータスコードや body やヘッダーをマップしてやります。正規表現が掛けられる樣に Lambda 側で message を整形しておかねばなりません。頑張る。今の所は。

バックエンドから切り離された APIゲートウェイとしては必要な設定ばかりではあります。

此れで API Gateway の設定は一段落する。rest_api を prod ステージにデプロイしてやると、 https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/user/1 等としてアクセス出來ます。

テスト等のやり方は全てすっ飛ばしてゐますが今回の話題ではないからです。未だ前説です。今回の話題迄到達してゐません。

後はドメインを設定します。Terraform は API Gateway のカスタムドメインに対応してゐず、やらうとすると大掛かりに成るから AWS コンソールからやってもよいかもしれません。今の所は。API Gateway のカスタムドメインの頁から証明書を設定してやり、その後 Route53 から其所に向けたレコードを作成します。

f:id:Kureduki_Maari:20160726121836p:plain

そしてゼーレヴェはついに大洋を渡り丘へと登る。

API Gateway の設定と Lambda のコードを staging と production に分ける

本題です。

先ず Lambda のバージョンを管理しませう。Lambda の alias と publish-version を組み合はせます。

publish-version は Lambda 函數のコードや設定のスナップショットを作りバージョンを附ける機能です。バージョンは publish-version を行う度に連番が自動で振られます。Lambda 函數には$LATESTと云ふ特別なバージョンが初めから在ります。此れは常に、アップロードした最新のコードを指します。バージョンを指定せず呼ぶとLambdaは$LATEST を呼びます。

AWS CLI からは aws lambda publish-version コマンドでやれます。aws lambda publish-version help が man です。

alias は Lambda 函數の或るバージョンに名前を附ける機能です。此れで staging には\$LATEST を當て、prod には publish-version した或るバージョンを當てると、staging で最新の設定やコードを検証した後 prod に適用する事が出來ます。

先に作った Lambda 函數の名は example でした。其々の alias は example:staging と example:prod として呼べます。

Terraform で alias を作成し、alias が指すバージョンを指定します。aws_lambda_permission は其々の alias に対して作ってやる必要が在ります。var.example_prod_function_version と云ふ變數で prod のバージョンを設定出來るやうにしておきます。

variable "example_prod_function_version" {
  default = "1"
}

resource "aws_lambda_function" "example" {
  description = "Example."
  filename = "example.zip"
  function_name = "example"
  handler = "main.lambda_handler"
  memory_size = 128
  role = "${aws_iam_role.lambda_example_exec.arn}"
  runtime = "python2.7"
  source_code_hash = "${base64sha256(file("example.zip"))}"
  timeout = "${var.timeout}"
}

resource "aws_lambda_alias" "example_staging" {
  depends_on = ["aws_lambda_function.example"]
  description = "staging"
  function_name = "${aws_lambda_function.example.function_name}"
  function_version = "$LATEST"
  name = "staging"
}

resource "aws_lambda_alias" "prod" {
  depends_on = ["aws_lambda_function.example"]
  description = "production"
  function_name = "${aws_lambda_function.example.function_name}"
  function_version = "${var.example_prod_function_version}"
  name = "prod"
}

resource "aws_lambda_permission" "example_staging_apigateway" {
  action = "lambda:InvokeFunction"
  depends_on = ["aws_lambda_function.example"]
  function_name = "${aws_lambda_function.example.function_name}"
  principal = "apigateway.amazonaws.com"
  qualifier = "staging"
  statement_id = "example_staging_apigateway"
}

resource "aws_lambda_permission" "example_prod_apigateway" {
  action = "lambda:InvokeFunction"
  depends_on = ["aws_lambda_function.example"]
  function_name = "${aws_lambda_function.example.function_name}"
  principal = "apigateway.amazonaws.com"
  qualifier = "prod"
  statement_id = "example_prod_apigateway"
}

次に API Gateway を staging と prod に分け、此れを Lambda の alias と結び附けます。API Gateway で使ふ機能はステージ (deployment) とステージ變數です。

ステージは API Gateway を公開する機能であり、より正しくは API Gateway の設定のスナップショットを公開する機能です。staging と prod の二つのステージを作り、staging にデプロイして検証してから prod にデプロイすればバージョンを管理できます。

ステージ變數はステージ毎に管理される環境變數のやうなものです。此れを使って Lambda 函數の alias を呼び分け、又 Lambda 函數内で処理を分岐します。

deployment を二つ作り、其々に stage と云ふステージ變數を定義します。alias を出し分けるには、integration にて呼ぶ Lambda 函數の uri に alias 名をくっ附けておきます。ステージ變數を lambda 函數に迄渡してやるには、integrarion の request_template でマップしてやります。

resource "aws_api_gateway_rest_api" "example" {
  description = "Dummy endpoint for example."
  name = "example"
}

resource "aws_api_gateway_deployment" "example_staging" {
  depends_on = [
    "aws_api_gateway_integration.example_user_id_get",
    "aws_api_gateway_integration.example_user_id_post"
  ]
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  stage_description = "staging"
  stage_name = "staging"
  variables = {
    stage = "staging"
  }
}

resource "aws_api_gateway_deployment" "example_prod" {
  depends_on = [
    "aws_api_gateway_integration.example_user_id_get",
    "aws_api_gateway_integration.example_user_id_post"
  ]
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  stage_description = "production"
  stage_name = "prod"
  variables = {
    stage = "prod"
  }
}

# /user
resource "aws_api_gateway_resource" "example_user" {
  parent_id = "${aws_api_gateway_rest_api.example.root_resource_id}"
  path_part = "user"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

# /user/{id}
resource "aws_api_gateway_resource" "example_user_id" {
  parent_id = "${aws_api_gateway_resource.example_user.id}"
  path_part = "{id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

# {{{ GET /user/{id}

resource "aws_api_gateway_method" "example_user_id_get" {
  authorization = "NONE"
  http_method = "GET"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

resource "aws_api_gateway_integration" "example_user_id_get" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  integration_http_method = "POST"
  request_templates = {
    "application/json" = <<EOF
{
  "id": "$input.params('id')",
  "stage": "$stageVariables.stage",
  "_method": "GET"
}
EOF
  }
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  type = "AWS"
  uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${var.aws_region}:${var.aws_account_id}:function:${aws_lambda_function.example.function_name}:$${stageVariables.stage}/invocations"
}

resource "aws_api_gateway_method_response" "example_user_id_get_200" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "example_user_id_get_400" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "400"
}

resource "aws_api_gateway_method_response" "example_user_id_get_500" {
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "500"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_200" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_200.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_400" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "400: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_400.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_get_500" {
  depends_on = ["aws_api_gateway_integration.example_user_id_get"]
  http_method = "${aws_api_gateway_method.example_user_id_get.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "500: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_get_500.status_code}"
}

# }}} GET /user/{id}

# {{{ POST /user/{id}

resource "aws_api_gateway_method" "example_user_id_post" {
  authorization = "NONE"
  http_method = "POST"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
}

resource "aws_api_gateway_integration" "example_user_id_post" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  integration_http_method = "POST"
  request_templates = {
    "application/json" = <<EOF
{
  "id": "$input.params('id')",
  "name": "$input.path('$.name')",
  "stage": "$stageVariables.stage",
  "_method": "POST"
}
EOF
  }
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  type = "AWS"
  uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${var.aws_region}:${var.aws_account_id}:function:${aws_lambda_function.example.function_name}:$${stageVariables.stage}/invocations"
}

resource "aws_api_gateway_method_response" "example_user_id_post_200" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "example_user_id_post_400" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "400"
}

resource "aws_api_gateway_method_response" "example_user_id_post_500" {
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "500"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_200" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_200.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_400" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "400: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_400.status_code}"
}

resource "aws_api_gateway_integration_response" "example_user_id_post_500" {
  depends_on = ["aws_api_gateway_integration.example_user_id_post"]
  http_method = "${aws_api_gateway_method.example_user_id_post.http_method}"
  resource_id = "${aws_api_gateway_resource.example_user_id.id}"
  response_templates = {
    "application/json" = <<EOF
{
  "error": "$input.path('$.errorMessage').substring(5)"
}
EOF
  }
  rest_api_id = "${aws_api_gateway_rest_api.example.id}"
  selection_pattern = "500: .+"
  status_code = "${aws_api_gateway_method_response.example_user_id_post_500.status_code}"
}

# }}} POST /user/{id}

此の内で、

resource "aws_api_gateway_integration" "example_user_id_get" {
  uri = "arn:aws:apigateway:${var.aws_region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${var.aws_region}:${var.aws_account_id}:function:${aws_lambda_function.example.function_name}:$${stageVariables.stage}/invocations"
}

${aws_lambda_function.example.function_name}:$${stageVariables.stage} と云ふ部分が呼び分けてゐる所で、此れは Terraform 実行後の AWS 上で example:${stageVariables.stage} と成ります。

又、

resource "aws_api_gateway_integration" "example_user_id_get" {
  request_templates = {
    "application/json" = <<EOF
{
  "stage": "$stageVariables.stage"
}
EOF
  }
}

と integration でマップしてやったので、Lambda 函數には event に stage と云ふキーで "staging" か "prod" が渡されます。讀み書きする DynamoDB のテーブルを切り替へる等色々出來るでせう。

import boto3
import logging
import re
import traceback

logger = logging.getLogger()
logger.setLevel(logging.INFO)

class EventValidationException(Exception):
    def __str__(self):
        return "400: %s" % self.message

def get_user(event):
    for key in ["id", "stage"]:
      if key not in event:
          raise EventValidationException("`id` should be required.")
    if event["stage"] == "prod":
        table_name = "example_user"
    else:
        table_name = "s-example_user"
    # 色々やる
    return {
        "id": id,
        "name": name
    }

def post_user(event):
    for key in ["id", "name", "stage"]:
      if key not in event:
          raise EventValidationException("`%s` should be required." % key)
    if not re.match(r"\A[0-9A-Za-z]{8,64}\Z", event["id"]):
        raise EventValidationException("`id` should match [0-9A-Za-z]{8,64}")
    if event["stage"] == "prod":
        table_name = "example_user"
    else:
        table_name = "s-example_user"
    # 色々やる
    return {
        "id": id,
        "name": name
    }

def lambda_handler(event, context):
    try:
        logger.info(event)
        if event["_method"] == "GET":
            get_user(event)
        elif event["_method"] == "POST":
            post_user(event)
        else:
            raise NotImplementedError()
    except Exception as e:
        logger.error("%s\n%s" % (e, traceback.format_exc()))
        m = re.match(r"\A\d{3}: ", e.__str__())
        if (not m) or (m and m.group(0)[0:3] not in ["400", "500"]):
            e = Exception("500: %s" % e)
        raise e

構成は此んな感じに成ってゐます。

f:id:Kureduki_Maari:20160726121849p:plain

更新の手順は以下のやうに成る筈です。Terraform と AWS CLI を前提とする手順ではありますが。

最初のデプロイ:

  1. aws_lambda_alias.prod の function_version を "\$LATEST" にしておく。Lambda 函數作成前で、publish_version もしてゐずバージョンが未だ無い爲。
  2. Lambda と API Gateway をデプロイ。
  3. API Gateway を staging ステージにデプロイ。
  4. staging で検証。
  5. Lambda を publish_version して、Version をメモする。
  6. aws_lambda_alias.prod の function_version を 上でメモしたバージョンに書き換へる。
  7. Lambda をデプロイ。
  8. API Gateway を prod ステージにデプロイ。

API Gateway の設定を更新した場合:

  1. API Gateway をデプロイ。
  2. API Gateway を staging ステージにデプロイ。
  3. staging で検証。
  4. API Gateway を prod ステージにデプロイ。

Lambda の設定やコードを変更した場合:

  1. Lambda をデプロイ。
  2. staging で検証。
  3. Lambda を publish_version して、Version をメモする。
  4. aws_lambda_alias.prod の function_version を 上でメモしたバージョンに書き換へる。
  5. Lambda をデプロイ。

ハイ。

OSXでGitの罠を回避しつつ更新されたPNGを圧縮するスクリプト

画像の圧縮自体はImageOptimと云ふプログラムをコマンドで呼んでゐる丈だ。お終い。
HomebrewではCaskroom/cask/imageoptimに在る。

そこでGitの話に成る。
利用シーンはご想像にお任せするが、gitでpullした後に、其のpullで更新されたPNGファイルをlosslessで最適化する。最適化するコードだけ抜き出すと以下に成る。

#!/usr/bin/env ruby
require 'shellwords'

targets = ...
system "ImageOptim #{Shellwords.shelljoin(targets)}"

targetsに指定すべきファイルを一覧するにはだうしたらいいだらうか。pullした時に更新したコミット番号がわかるので、diffをとり、削除された差分ではないファイルを一覧し、PNGを抽出する。簡単なシェルのスクリプトだ。git diff --name-statusを使へる。

#!/usr/bin/env ruby
require 'shellwords'

revisions = ARGV[1] || (print 'revisions> '; gets.strip)
targets = `git diff --name-status #{revisions}`.
  each_line.
  collect(&:strip).
  reject{|line| line.start_with?('D') }.
  collect{|line| line.sub(/\A\S+\s+/, '') }.
  select{|line| line.end_with?('.png') }
system "ImageOptim #{Shellwords.shelljoin(targets)}"

此れをImageOptimと云ふ名前で保存しておけば、./ImageOptim -- 64e88f6..2be0234等と呼び出せる。
餘談だが此の偽のコミット番号はruby -rsecurerandom -e"puts SecureRandom.uuid[0..6]"で作れる。
上で標準ライブラリのShellwordsを使ってゐるのは、空白を含んだファイル名が沢山転がってゐるからだ。そもそも此れが、bashスクリプトではなく面倒に成ってRubyを書いた理由だった。
次に現れたのは日本語ファイル名だった。OSXのgitはとうに日本語のファイル名を扱へるやうにはなってゐるが、親切にもエスケープして表示してくれる。\001\002…のやうな。
面倒だ。調べると此のエスケープをオフにするオプションがある。git config core.quotepathだ。此れをfalseにすればUnicodeのまま印字される。常にオフにするのも氣がひけるので、スクリプトの終了時に元の設定に戻すコードを書いた。Ruby的なブロック引數だ。

#!/usr/bin/env ruby
require 'shellwords'

def git_quotepath?
  case `git config --local core.quotepath`
  when ""        then nil
  when "false\n" then false
  when "true\n"  then true
  end
end

def set_git_quotepath config
  case config
  when nil
    `git config --local --unset core.quotepath`
  when false
    `git config --local core.quotepath false`
  when true
    `git config --local core.quotepath true`
  end
end

def without_git_quotepath &block
  config = git_quotepath?
  set_git_quotepath false
  block.call
ensure
  set_git_quotepath config
end

revisions = ARGV[1] || (print 'revisions> '; gets.strip)
targets = without_git_quotepath{ `git diff --name-status #{revisions}` }.
  each_line.
  collect(&:strip).
  reject{|line| line.start_with?('D') }.
  collect{|line| line.sub(/\A\S+\s+/, '') }.
  select{|line| line.end_with?('.png') }
system "ImageOptim #{Shellwords.shelljoin(targets)}"

# vim: set ft=ruby:

疲れた。

astyleを使ひdotnet/CodeFormatterがコンパイルできない環境でC#をlintする

題名で此の記事の内容は終はる。
私はVim (NeoVim) 使ひなので、IDEは殺す。IDEのコードフォーマッターはコマンドラインから呼び出せない限り殺す。Unity附属のMonoDevelopは殺す。
C#にはCodeFormatterと云ふ公式のコードフォーマッターが在る。此れを使ひたかったのだが、OSXコンパイルできなかった。
仕方が無いので、astyleと云ふコードフォーマッターを探してきた。

#!/bin/bash -eux
CodeFormatter="astyle --indent=tab=8 --indent=force-tab=4 --indent-cases --indent-namespaces --min-conditional-indent=0 --pad-oper --pad-header --delete-empty-lines --add-one-line-brackets --keep-one-line-blocks --lineend=linux --formatted"
find Assets/Scripts/ -name '*.cs' -exec $CodeFormatter {} \;
find Assets/Editor/ -name '*.cs' -exec $CodeFormatter {} \;
find Assets/ -name '*.cs.orig' | xargs rm
# vim:set ft=sh:

用は足せる。此れをCodeFormatterと云ふファイル名で保存して、./CodeFormatterとして使ってゐる。悲しい。
EditorConfigと組み合はせて使ってゐる。

root = true

[*.cs]
charset = utf-8
end_of_line = lf
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true