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

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

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

音樂は SoundCloud に公開中です。

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

Programming は GitHub で開發中です。

Terraformのmoduleを書く

AWS の設定を Terraform で行ってゐる。マネージメントコンソールからからやると簡單な設定であっても AWS CLI や Terraform からやらうとすると大變なものに成る場合も在る。コンソールでは一つに纏められてゐるものが個々別のオブジェクトとして其々操作しなければならないものや、裏で自動で作られるものも明示して作らねばならないものが在り、設定が膨れる。大變だ。一回だけなら膨れても何でも問題に成らないのだが、似たオブジェクトを沢山作らねばならぬと成れば、大變だ。否、似たものであれば纏められる筈である。Terraform にはモジュール機能が在る。

以前 API Gateway と Lambda を連携させて Terraform から設定た (API Gateway と Lambda の組み合はせでリリースするバージョンを制御する)。

此所で作ったものは、

有り體に此れ丈が API を作る度に必要に成る。死にたく成る。抽象化して呉れ。

書いてゐると判るが、以下の纏まりは重複する。Lambda は aws_lambda_function 毎に、

  • aws_lambda_function
  • aws_lambda_alias staging と prod
  • aws_lambda_permission staging と prod

が。API Gateway では aws_api_gateway_method 毎に、

が繰り返す。此れ等を其々モジュールに纏めてみる。

出來上がったものが以下に在るのでコードを読めば解ると云ふ話も在る。→ https://github.com/ne-sachirou/c4se-infra/tree/31bcdcaf8bc11c210163087c01b4087115d9eccd/terraform/modules

Terraform モジュールの構成

Terraform のモジュールは、Terraform のコードを分離する機能だ。モジュールを呼び出す時變數を與へる事が出來る。即ち、變數を切り替へる丈で重複させられるコードは、同じモジュールで扱へる。引數を與へ Terraform のコードを吐く函數の樣なものだ。

terraform-community-modulesに例が在る。

モジュールのファイルは、入力・本体・出力に分けて以下の樣にされる事が多い。

  • variables.tf モジュールで使ふ變數は全て此所で定義する
  • main.tf
  • outputs.tf モジュールの外から見える値は全て output 記述で書かねばならない。其れを全て此所に書く

例として以下の樣に使へる "a" と云ふモジュールを書く。

module "a_of_sample" {
  source = "./a"
  var1 = "sample"
}

resource b {
  attr = "${module.a_of_sample.out1}"
}

./a ディレクトリに三っつのファイルを作る。

# a/variables.tf
variable "var1" {}
# a/main.tf
resource some_resource {
  attr = "${var.var1}"
}
# a/outputs.tf
output "out1" {
  value = "${var.var1}"
}

ファイルの分け方は通常の Terraform と同じく任意なので、もっと細かく分けてもよい。variables.tf と outputs.tf は此う置いておくのが他人から見易いであらう。

モジュールを使った Terraform を実行する時には、plan を作る前に terraform get を行なふ。

terraform get
terraform plan --out=terraform.tfplan
terraform apply terraform.tfplan

GitHub 上等に在るモジュールを使ってゐれば .terraform/ に clone され、ローカルにモジュールを作ってあれば .terraform/ 下にシンボリックリンクが張られる。.terraform/ は.gitignore する。

モジュールに限らず Terraform では条件分岐は未だ出來ない。ループは少しは出來る。以下で實例を下[もと]に詳細を見てゆく。

API Gateway 向け Lambda のモジュール

  • aws_lambda_function
  • aws_lambda_alias staging と prod
  • aws_lambda_permission staging と prod

を纏めてみる。繰り返すコードをただ抽出するだけで出來る。

引數は、

variable "description" {}

variable "filename" {}

variable "function_name" {}

variable "handler" {}

variable "memory_size" {}

variable "prod_function_version" {
  default = "$LATEST"
}

variable "role" {}

variable "runtime" {}

variable "timeout" {}

でよいであらう。出力は、他から Lambda を呼び出すのに function_name が要るから、

output "function_name" {
  value = "${var.function_name}"
}

と成らう。するとメインは此う成る。

resource "aws_lambda_function" "function" {
  description = "${var.description}"
  filename = "${var.filename}"
  function_name = "${var.function_name}"
  handler = "${var.handler}"
  memory_size = "${var.memory_size}"
  role = "${var.role}"
  runtime = "${var.runtime}"
  source_code_hash = "${base64sha256(file(var.filename))}"
  timeout = "${var.timeout}"
}

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

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

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

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

此れ等を ./modules/aws_lambda_function_for_apigateway に保存しやう。呼び出しは此う成る。殆ど aws_lambda_function の内容だけで済む。

module "aws_lambda_function_for_apigateway_sample" {
  source = "./modules/aws_lambda_function_for_apigateway"
  description = "Sample."
  filename = "./sample.zip"
  function_name = "sample"
  handler = "handler.handler"
  memory_size = 128
  prod_function_version = "1"
  role = "${aws_iam_role.sample.arn}"
  runtime = "python2.7"
  timeout = 3
}

Lambda コードの ./sample.zip や Lambda 実行權限の aws_iam_role.sample.arn は今迄通り用意する必要が在る。本番デプロイして publish version した度に prod_function_version を上げてゆく。

Lambda 向け API Gateway のメソッドのモジュール

さて、先の Lambda を呼び出す API Gateway を作ってみる。

は今迄通り作っておく。aws_api_gateway_resource.sample_sample/sample を指すとする。

をモジュールにしやう。

モジュールの前に、aws_api_gateway_method_response と aws_api_gateway_integration_response をエラーのステータスコードの個數分作るのはループするのが有効である。ループは、洞の resource にも在る count 属性に依って行なへる。例へば、

resource a "a_with_count" {
  count = 2
  attr = "${count.index}"
}

は、

resource a "a_with_count_0" {
  attr = 0
}

resource a "a_with_count_1" {
  attr = 1
}

と、resource 名を除き等しい。又モジュールに配列は渡せないから、文字列分割函數 split 函數を使って、

variable "status_codes" {
  default = "400,500"
}

resource a "a_with_status_codes" {
  count = "${length(split(",", var.status_codes))}"
  status_code = "${element(split(",", var.status_codes), count.index)}"
}

とループ出來る。何とかして呉れ。

ループも出來る樣に成ったのでモジュール化する。マッピングも全て與へなければならないので引數はいささか多い。成るべく省略出來るやう default を定義しておく。

variable "authorization" {
  default = "NONE"
}

variable "aws_region" {}

variable "aws_account_id" {}

variable "error_status_codes" {}

variable "function_name" {}

variable "http_method" {}

variable "integration_request_parameters_in_json" {
  default = "{}"
}

variable "integration_request_templates" {
  default = <<EOF
{ "stage": "$stageVariables.stage" }
EOF
}

variable "integration_response_parameters_in_json" {
  default = "{}"
}

variable "integration_response_templates" {
  default = "$input.json('$')"
}

variable "method_request_parameters_in_json" {
  default = "{}"
}

variable "method_response_parameters_in_json" {
  default = "{}"
}

variable "resource_id" {}

variable "rest_api_id" {}

此れでもう大体見える。出力 (output) は不要だらう。メインは大体長い。長いからモジュール化したので仕方が無い。

resource "aws_api_gateway_method" "method" {
  authorization = "${var.authorization}"
  http_method = "${var.http_method}"
  request_parameters_in_json = "${var.method_request_parameters_in_json}"
  resource_id = "${var.resource_id}"
  rest_api_id = "${var.rest_api_id}"
}

resource "aws_api_gateway_integration" "integration" {
  depends_on = ["aws_api_gateway_method.method"]
  http_method = "${var.http_method}"
  integration_http_method = "POST"
  request_parameters_in_json = "${var.integration_request_parameters_in_json}"
  request_templates = {
    "application/json" = "${var.integration_request_templates}"
  }
  resource_id = "${var.resource_id}"
  rest_api_id = "${var.rest_api_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:${var.function_name}:$${stageVariables.stage}/invocations"
}

resource "aws_api_gateway_method_response" "method_response_200" {
  depends_on = ["aws_api_gateway_method.method"]
  http_method = "${var.http_method}"
  resource_id = "${var.resource_id}"
  response_parameters_in_json = "${var.method_response_parameters_in_json}"
  rest_api_id = "${var.rest_api_id}"
  status_code = "200"
}

resource "aws_api_gateway_integration_response" "integration_response_200" {
  depends_on = [
    "aws_api_gateway_integration.integration",
    "aws_api_gateway_method_response.method_response_200"
  ]
  http_method = "${var.http_method}"
  resource_id = "${var.resource_id}"
  response_parameters_in_json = "${var.integration_response_parameters_in_json}"
  response_templates = {
    "application/json" = "${var.integration_response_templates}"
  }
  rest_api_id = "${var.rest_api_id}"
  status_code = "200"
}

resource "aws_api_gateway_method_response" "method_response_error" {
  count = "${length(split(",", var.error_status_codes))}"
  depends_on = ["aws_api_gateway_method.method"]
  http_method = "${var.http_method}"
  resource_id = "${var.resource_id}"
  rest_api_id = "${var.rest_api_id}"
  status_code = "${element(split(",", var.error_status_codes), count.index)}"
}

resource "aws_api_gateway_integration_response" "integration_response_error" {
  count = "${length(split(",", var.error_status_codes))}"
  depends_on = ["aws_api_gateway_integration.integration"]
  http_method = "${var.http_method}"
  resource_id = "${var.resource_id}"
  response_templates = {
    "application/json" = <<EOF
{ "error": "$input.path('$.errorMessage').substring(5)" }
EOF
  }
  rest_api_id = "${var.rest_api_id}"
  selection_pattern = "${element(split(",", var.error_status_codes), count.index)}: .+"
  status_code = "${element(split(",", var.error_status_codes), count.index)}"
}

aws_api_gateway_method_response.method_response_erroraws_api_gateway_integration_response.integration_response_error が count を使ってゐる。エラーメッセージは、「500: Some error.」等 \d{3}: が先頭に附くのを前提した。Terraform はモジュールに配列や連想配列を渡せないので此うしないとモジュールに出來ない。

此れ等を ./modules/aws_api_gateway_method_for_lambda_function に保存する。呼び出し例として GET /sample を作ってみる。

module "aws_api_gateway_method_research_random_id_get" {
  source = "./modules/aws_api_gateway_method_for_lambda_function"
  aws_region = "${var.aws_region}"
  aws_account_id = "${var.aws_account_id}"
  error_status_codes = "400,500"
  function_name = "${module.aws_lambda_function_for_apigateway_sample.function_name}"
  http_method = "GET"
  resource_id = "${aws_api_gateway_resource.sample_sample.id}"
  rest_api_id = "${aws_api_gateway_rest_api.smaple.id}"
}

マッピングが default で定義したもので濟めば此の樣に短い。

やりましたね!

餘談

Lambda と API GatewayServerless Framworkに載せ替へたので上記モジュールは不要と化しましたがわたしは元氣です。

Switch API Gateway from Terraform to Serverless Framwork.

DynamoDBで楽観的lockを行なふ

DynamoDB にはトランザクションは無いしロック等無い。RDS 使へ。好い加減にしろ。

然し DynamoDB には魅力が在るし、少々トランザクション出來なからうが此れを使ひたいといふ欲求の在る場面もある。楽観的ロック位いは出來ないだらうか。

楽観的ロックと云へば私にとっては ActiveRecord の lock_version だ。UPDATE items SET name = "New Name", lock_version = 43 WHERE id = 1 AND lock_version = 42 等のやうに、比較と更新をアトミックに行なへれば此れは實裝出來る。

DynamoDB ではテーブルを跨がなければ、比較と更新がアトミックに出來る。從ってテーブルを跨がない楽観的ロックは實裝出來る。詰りテーブルを跨がないトランザクションは實裝出來る。

以下の DynamoDB テーブルが在るとする。id, name, lock_version を持たせやう。

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

記法は Terraform。

PutItem 時に condition-expression を附けると、condition-expression の結果が僞である時にエラーを起こし更新せぬやうに出來る。

cf. 条件式を使用した条件付きの書き込みの実行 - Amazon DynamoDB

Python でやると以下の如し。行が無い爲 lock_version 列も無い時か、或いは lock_version 列が變更されてゐなければ、PutItem を實行する。

import boto3

table = boto3.resource("dynamodb").Table("item")


class Item(object):
    def __init__(id, **props):
        self.id = id
        self.name = props.get("name", None)
        self.lock_version = props.get("lock_version", 0)


def get_item(id):
    item = Item(id=id)
    res = table.get_item(
        Key={"id": id}
    )
    if "Item" in res:
        item.name = res["Item"]["name"]
        item.lock_version = res["Item"]["lock_version"]
    return item


def put_item(item):
    lock_version_attr = boto3.dynamodb.conditions.Attr("lock_version")
    table.put_item(
        Item={
            "id": item.id,
            "name": item.name,
            "lock_version": item.lock_version + 1
        },
        ConditionExpression=lock_version_attr.not_exists().__or__(lock_version_attr.eq(item.lock_version))
    )
    item.lock_version += 1

if __name__ == "__main__":
    item = get_item("mOmonga")
    item.name = "New name"
    put_item(item)

此の ConditionExpression=lock_version_attr.not_exists().__or__(lock_version_attr.eq(item.lock_version)) が条件式を組み立ててゐる。

もっと綺麗な組み立て方をしたい。

Pythonのunittestを書かう。其れを自動実行しやう (rake test + guardを置き換へやう)

Python アプリケーション毎に環境を分ける (pyenv + virtualenv (or venv) + pip)の續きのやうなもの。

Ruby Python
Lint Rubocop flake8
テスト RSpec unittest
モック RSpec mock (or unittest.mock)
時間に依存したテスト Timecop (or ActiveSupport::Testing::TimeHelpers) freezegun
テストランナー Rspec unittest
スクランナー Rake Invoke
ファイル監視 Guard watchdog + Invoke

Lint

追記 20161107 flake8-pep257 は flake8-docstrings に置き換へられた。

PEP8 と PEP257 と云ふコーディングスタイル標準が在る。flake8 を使へば此れに從ってゐるかテスト出來る。

flake8 3.0.2 : Python Package Index

インストールには此う。

pip install pep8 flake8 flake8-pep257

其の儘実行するには問題が在る。PEP257 の D203 と D211 と云ふ規則は互いに矛盾してゐる。洞ちらかに従へばよいので、洞ちらかを無視させる。又一行の長さが 80 文字はいささか短いので、160 文字にしてやらう。

D203: 1 blank line required before class docstring

D211: No blank lines allowed before class docstring

.flake8 と云ふファイルをプロジェクト root に作ってやる。

[flake8]
ignore = D203
max-line-length = 160

flake8 を実行するには、flake8 でよい。

因みに以下のコードは docstring や空行等で PEP8 に従はない。地の文が在るから。

テスト & テストランナー

テスト記述とテストランナーには Python 附属の unittest を使ふ。

2 系 25.3. unittest — ユニットテストフレームワーク — Python 2.7.x ドキュメント

3 系 26.4. unittest — ユニットテストフレームワーク — Python 3.5.1 ドキュメント

定番っぽいのと云へばpytesttoxなのだらうが、附属のものを使ってみる。後で tox にするかも……。

標準のやり方が見當らなかったので當てずっぽうである。tests ディレクトリを作りテストコードを置く。

app.py
lib.py
tests
  ├ test_app.py
  └ test_lib.py

以下のコードだとしやう。

# app.py

def succ(n):
    return n + 1

テストコードは此う成るだらうか。

# tests/test_app.py

from app import succ
try:
    import unittest2 as unittest
except ImportError:
    import unittest

class TestSucc(unittest.TestCase):
    def test_succ(self):
        self.assertEqual(43, succ(42))

變哲も無い。unittest ライブラリのテストディスカバリを使って実行する。

python -m unittest discover -s tests

此れで tests ディレクトリ下から unittest.TestCase を繼承したテストケースを全て実行出來る。

モック

26.5. unittest.mock — モックオブジェクトライブラリ — Python 3.5.1 ドキュメント

Python3 だと附属の unittest の一部と成ってゐるらしい。Python2 には無いので、pip install mock する。

patch でモックして、呼び出しを assert してみる。次のコードをテストする。

# app.py

import lib

class SampleException(Exception):
    pass

class Sample(object):
    def __init__(base):
        self.base = base

    def is_even(n):
        return n % 2 == 0

    def sample(self, n):
        if self.is_even(n):
            raise SampleException("%d is even." % n)
        if lib.is_prime(n):
            return 1
        else:
            return n - 1

機能紹介を意図とした、無理矢理なテストコードを書かう。

# tests/test_app.py

from app import (
    Sample,
    SampleException
)
try:
    import unittest2 as unittest
except ImportError:
    import unittest
try:
    import unittest.mock
except ImportError:
    import mock

class TestSample(unittest.TestCase):
    def setUp(self):
        self.fourtytwo = Sample(42)

    def test_init(self):
        self.assertEqual(Sample(1).base, 1)

    def test_is_even(self):
        self.assertTrue(Sample(0).is_even())
        self.assertFalse(Sample(1).is_even())
        self.assertTrue(Sample(2).is_even())
        self.assertFalse(Sample(3).is_even())

    def test_sample_is_even(self):
        with\
                mock.patch("lib.is_prime") as is_prime,
                mock.patch.object(self.fourtytwo, "is_even", return_value=True) as is_even:
            with self.assertRaises(SampleException):
                self.fourtytwo.sample(2)
            is_prime.assert_not_called()
            is_even.assert_called_once_with(2)

    def test_sample_is_prime(self):
        with\
                mock.patch("lib.is_prime", return_value=True),
                mock.patch.object(self.fourtytwo, "is_even", return_value=False):
            self.assertEqual(self.fourtytwo.sample(2), 1)

    def test_sample_is_not_prime(self):
        with\
                mock.patch("lib.is_prime", return_value=False),
                mock.patch.object(self.fourtytwo, "is_even", return_value=False):
            self.assertEqual(self.fourtytwo.sample(2), 1)

patch, patch.object, return_value, assert_called_once_with, assert_not_called を使ってみた。

時間に依存したテスト

時間を計算する式を書いてはテストとして元も子もない時や、閏日等で現在時刻を偽装したい時が在る。又テスト中に時間が経過しては assertEqual に失敗する場合も在るだらう。現在時刻をモックしたいのだ。此れが freezegun である。

spulec/freezegun: Let your Python tests travel through time

from datetime import datetime
from freezegun import freeze_time
from pytz import timezone
try:
    import unittest2 as unittest
except ImportError:
    import unittest

class TestExample(unittest.TestCase):
    @freeze_time(datetime.now())
    def test_freeze(self):
        self.assertEqual(datetime.now(), datetime.now())

    @freeze_time(datetime(2016, 4, 1, 7, tzinfo=timezone("Asia/Tokyo")))
    def test_freeze(self):
        self.assertEqual(
            datetime.now(timezone("Asia/Tokyo")),
            datetime(2016, 4, 1, 7, tzinfo=timezone("Asia/Tokyo"))
        )

スクランナー

Invoke と云ふ便利なツールが在る。未だ正式版ではないが、Fabricと云ふデプロイツール (Ruby で謂ふ CapistranoMina) にも採用されてゐる。

Welcome to Invoke! — Invoke documentation

pip install invoke する。tasks.py と云ふファイルを作ると Invoke が其れをタスク定義ファイルとして扱ふ。

from invoke import run, task

@task
def test(context):
    try:
        run("flake8 *.py tests")
        run("python -m unittest discover -s tests")
    except Exception:
        pass

此れで inv test 或いは invoke test で flake8 と unittest を実行出來る。

ファイル監視

Node.js 界隈には有りふれてゐる。ファイルを変更したら、其れに應じてテストを実行しビルドする仕組みが欲しい。watchdog と云ふライブラリで簡單に実装出來る。

watchdog 0.8.3 : Python Package Index

pip install watchdog する。inv watch で、ファイルを更新する度にテストを実行するやうにしてみる。

from invoke import run, task
import time
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer

class WatcherHandler(FileSystemEventHandler):
    def __init__(self, context):
        super(FileSystemEventHandler, self).__init__()
        self.context = context

    def on_modified(self, event):
        test(self.context)

@task
def test(context):
    try:
        run("flake8 *.py tests")
        run("python -m unittest discover -s tests")
    except Exception:
        pass

@task
def watch(context):
    observer = Observer()
    observer.schedule(WatcherHandler(context), ".", recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()

大体わからう。

ハイ。

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. 殺したサーバーを一覧する