@ijin

[Michael H. Oshita]

AWS GameDay Japan 2016を開催してきた

過去に何回も参加・開催両方を経験した事があるAWS GameDayに、またしても運営側として関わりました。

お題は基本的に去年のre:Inventでやったのを若干チューニングしたやつ。 詳細は今後また別のところで開催される可能性があるので、その時の参加者の為に伏せておきます。

当日

競技中はそれぞれのチームのスコアがリアルタイムで見れるダッシュボードがあって白熱した様子が伝わってました。

これはこれで楽しいんですが、何かが足りない感じ。。。






そう、グラフです!!






参加チームの白熱したバトル(順位の入れ替え等)を時系列で表示し、戦いの軌跡をビジュアライズするアレです。ISUCONでもよく見慣れたあの遷移するグラフがなかったのです!

開始してから気づきました。。

なので作りました

Graphing premiere

ダッシュボードの作りを見てみると jQueryReact.js を使っている模様。ならば、どこかでendpointを叩いてjsonを取得しているはずなので、いろいろ探したところ、それらしいのがありました。

1
2
3
4
5
6
7
8
9
10
11
12
13
  getScores: function() {
    $.ajax({
      url: 'https://xxxxxxxxx.execute-api.us-west-2.amazonaws.com/prod/scores',
      dataType: 'json',
      cache: false,
      success: function(data) {
        this.setState({data: data});
      }.bind(this),
      error: function(xhr, status, err) {
        console.error("scores", status, err.toString());
      }.bind(this)
    });
  },

こうなれば話は簡単で後は返却されるjsonを解析して、定期的にcallしてplotしていけば良いだけです。

データ送信

グラフを描画するサーバを用意するのはしんどいので、カスタムメトリックスが作成できる監視サービスを使いました。 最初はMackerelをと思ったけど、独自グラフを一般公開する設定がなさそうだったのでDatadogを採用。

Datadogでカスタムメトリックスを送るにはAPIを直接叩くよりは、StatsD経由で送信した方が楽なのでrubyでサクっと適当に記述。

gameday.rb
1
2
3
4
5
6
statsd.batch do |s|
  s.gauge('gameday2016', scores[0]['Profit']*100, :tags => ["team:" + scores[0]['Team']])
  s.gauge('gameday2016', scores[1]['Profit']*100, :tags => ["team:" + scores[1]['Team']])
  s.gauge('gameday2016', scores[2]['Profit']*100, :tags => ["team:" + scores[2]['Team']])
  # etc
end

グラフ描画

公開ダッシュボードを作成するには TimeBoard ではなく ScreenBoard を選択し、後は必要そうなグラフを追加していくだけ。

グラフ自体はGUIで作っても良いし、jsonで記述可能なので結構柔軟で素敵です。

profits.json
1
2
3
4
5
6
7
8
9
10
11
{
  "viz": "timeseries",
  "requests": [
    {
      "q": "avg:gameday2016{*} by {team}",
      "aggregator": "avg",
      "conditional_formats": [],
      "type": "line"
    }
  ]
}
ranking.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
  "viz": "toplist",
  "requests": [
    {
      "q": "top(avg:gameday2016{*} by {team}, 20, 'last', 'desc')",
      "style": {
        "palette": "dog_classic"
      },
      "conditional_formats": [
        {
          "palette": "white_on_green",
          "comparator": ">=",
          "value": 0
        },
        {
          "palette": "white_on_red",
          "comparator": "<",
          "value": 0
        }
      ]
    }
  ]
}

できあがったグラフはこんな感じ。 高負荷発生等のイベント時の対応との比較もできて見やすいと思います。

これで各チームのポジション等を伝えやすくなりました。

というわけで優勝したチーム初老丸、おめでとうございます!

終わりに

今回は競技の途中から実装しちゃったので次回は最初から用意しておきたいと思います。

(※)実は去年のISUCONの時も似たような事をやってましたね。。

DockerCon 2016に参加してきた

先日、DockerCon 2016に参加してきたので、そのメモ。

To DockerCon you go!

ちなみに行く事になった経緯は後日のJAWS-UGおコンテナ支部 #5でLTしてきました。

パッション大事!

以下、トゥギャッター風に。

Day 0

Pre-Registration & Welcome Party

場所はシアトルのWashington State Convention Center。何気にこの地に訪れるのは初。

Welcome to DockerCon 2016!

企業ブース

Welcome Party

Bar

Day 1

Highlights

  • Docker 1.12 (orchestration built-in)
    • swarm mode
    • serice api
    • cryptographic node identity
    • build-in routing mesh
  • Experimental Distributed Application Bundles (DAB)
  • Docker for AWS & Azure (beta)
  • Docker for Mac & Windows (public beta)

ぞろぞろ

Sessions

  • The Golden Ticket: Docker and High Security Microservices
  • Microservices + Events + Docker = A Perfect Trio
  • Thinking Inside the Container: A Continuous Delivery Story
  • Docker in Production, Look No Hands!

The Golden Ticket: Docker and High Security Microservices

Microservices + Events + Docker = A Perfect Trio

Thinking Inside the Container: A Continuous Delivery Story

DockerCon party @ Space Needle

最後はシアトルの名所(?)、Space Needleでの貸切パーティー

中でもワイワイ

Day 2

Highlights

  • Docker Datacenter
  • AWS Quickstart
  • Azure Marcketplace Templates
  • Docker Store (private beta)

個人的には、とある企業でのDockerのユースケースを演じる漫才が面白かった。

AzureのCTO

ADPのCTO

Sessions

  • Making Friendly Microservices
  • Sharding Containers: Make Go Apps Computer-Friendly Again
  • Closing General Session: Moby Dock’s Cool Hacks

Making Friendly Microservices

Sharding Containers: Make Go Apps Computer-Friendly Again

Closing General Session: Moby Dock’s Cool Hacks

Serverless Dockerが面白かった。

Docker on Drones

終わりに

世界各国のエンジニアと話せて、Dockerに対する熱量や動向を肌感覚で感じとれて非常に有意義な旅でした。以前のChefConfHashiConfもそうだけど、気になる技術やトレンドがあればTechカンファレンスに直接乗り込んで見るのが一番効率的なので、今後も継続してどこかに参加するようにしたいです。

Thank you!

TerraformでAPI Gatewway

つい先日、Terraformでずっと気になっていたAmazon API Gatewayのselection_patternpull requestがmergeされました。

今まではAPI GWをInfrastructure As Codeで構築するにあたって複数のintegration responseパターンを返却できないのがネックだったのが、これでようやく解決。途中までTerraformで作って、その後に以下のようにawscliで追加するというちょっと煩わしい手順でした。

1
2
3
REST_ID=$(aws apigateway get-rest-apis --query 'items[?name==`my_api`].id' --output text)
RESOURCE_ID=$(aws apigateway get-resources --rest-api-id $REST_ID --query 'items[?path==`/my_path`].id' --output text)
aws apigateway put-integration-response --rest-api-id $REST_ID --resource-id $RESOURCE_ID --http-method GET --status-code 400 --response-templates '{"application/json": "$input.path('$').errorMessage"}' --selection-pattern "[^0-9](.|\n)*" 

というわけで、早速実験。お題は以前紹介したElastic Beanstalk ssh用のAPI GWで。

Terraform version

まずは、masterにmergeされた開発版Terraformのビルド。やり方はこちら

1
2
$ terraform version
Terraform v0.6.16-dev - 5cd27c2

Terraform file

API GWのterraform化はこんな感じで。

(*) permissionはlambda作成後に許可

Terraform plan/apply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
$ terraform plan
Refreshing Terraform state prior to plan...


The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ aws_api_gateway_deployment.eb_deployment
    rest_api_id: "" => "${aws_api_gateway_rest_api.EB.id}"
    stage_name:  "" => "prod"

+ aws_api_gateway_integration.ip_get
    http_method:                        "" => "GET"
    integration_http_method:            "" => "POST"
    request_templates.#:                "" => "1"
    request_templates.application/json: "" => "{ \"env_name\": \"$input.params('env_name')\" }"
    resource_id:                        "" => "${aws_api_gateway_resource.ip.id}"
    rest_api_id:                        "" => "${aws_api_gateway_rest_api.EB.id}"
    type:                               "" => "AWS"
    uri:                                "" => "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:123456789012:function:eb_ip/invocations"

+ aws_api_gateway_integration.server_num_get
    http_method:                        "" => "GET"
    integration_http_method:            "" => "POST"
    request_templates.#:                "" => "1"
    request_templates.application/json: "" => "{\n \"env_name\": \"$input.params('env_name')\",\n \"server_num\": \"$input.params('server_num')\" \n}"
    resource_id:                        "" => "${aws_api_gateway_resource.server_num.id}"
    rest_api_id:                        "" => "${aws_api_gateway_rest_api.EB.id}"
    type:                               "" => "AWS"
    uri:                                "" => "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:123456789012:function:eb_ip/invocations"

+ aws_api_gateway_integration_response.ip_get_200
    http_method:                         "" => "GET"
    resource_id:                         "" => "${aws_api_gateway_resource.ip.id}"
    response_templates.#:                "" => "1"
    response_templates.application/json: "" => "$input.path('$')"
    rest_api_id:                         "" => "${aws_api_gateway_rest_api.EB.id}"
    status_code:                         "" => "200"

+ aws_api_gateway_integration_response.ip_get_400
    http_method:                         "" => "GET"
    resource_id:                         "" => "${aws_api_gateway_resource.ip.id}"
    response_templates.#:                "" => "1"
    response_templates.application/json: "" => "$input.path('$').errorMessage"
    rest_api_id:                         "" => "${aws_api_gateway_rest_api.EB.id}"
    selection_pattern:                   "" => "[^0-9](.|\n)*"
    status_code:                         "" => "400"

+ aws_api_gateway_integration_response.server_num_get_200
    http_method:                         "" => "GET"
    resource_id:                         "" => "${aws_api_gateway_resource.server_num.id}"
    response_templates.#:                "" => "1"
    response_templates.application/json: "" => "$input.path('$')"
    rest_api_id:                         "" => "${aws_api_gateway_rest_api.EB.id}"
    status_code:                         "" => "200"

+ aws_api_gateway_integration_response.server_num_get_400
    http_method:                         "" => "GET"
    resource_id:                         "" => "${aws_api_gateway_resource.server_num.id}"
    response_templates.#:                "" => "1"
    response_templates.application/json: "" => "$input.path('$').errorMessage"
    rest_api_id:                         "" => "${aws_api_gateway_rest_api.EB.id}"
    selection_pattern:                   "" => "[^0-9](.|\n)*"
    status_code:                         "" => "400"

+ aws_api_gateway_method.ip_get
    api_key_required: "" => "0"
    authorization:    "" => "NONE"
    http_method:      "" => "GET"
    resource_id:      "" => "${aws_api_gateway_resource.ip.id}"
    rest_api_id:      "" => "${aws_api_gateway_rest_api.EB.id}"

+ aws_api_gateway_method.server_num_get
    api_key_required: "" => "0"
    authorization:    "" => "NONE"
    http_method:      "" => "GET"
    api_key_required: "" => "0"
    authorization:    "" => "NONE"
    http_method:      "" => "GET"
    resource_id:      "" => "${aws_api_gateway_resource.server_num.id}"
    rest_api_id:      "" => "${aws_api_gateway_rest_api.EB.id}"

+ aws_api_gateway_method_response.ip_200
    http_method: "" => "GET"
    resource_id: "" => "${aws_api_gateway_resource.ip.id}"
    rest_api_id: "" => "${aws_api_gateway_rest_api.EB.id}"
    status_code: "" => "200"

+ aws_api_gateway_method_response.ip_400
    http_method: "" => "GET"
    resource_id: "" => "${aws_api_gateway_resource.ip.id}"
    rest_api_id: "" => "${aws_api_gateway_rest_api.EB.id}"
    status_code: "" => "400"

+ aws_api_gateway_method_response.server_num_200
    http_method: "" => "GET"
    resource_id: "" => "${aws_api_gateway_resource.server_num.id}"
    rest_api_id: "" => "${aws_api_gateway_rest_api.EB.id}"
    status_code: "" => "200"

+ aws_api_gateway_method_response.server_num_400
    http_method: "" => "GET"
    resource_id: "" => "${aws_api_gateway_resource.server_num.id}"
    rest_api_id: "" => "${aws_api_gateway_rest_api.EB.id}"
    status_code: "" => "400"

+ aws_api_gateway_resource.eb
    parent_id:   "" => "${aws_api_gateway_rest_api.EB.root_resource_id}"
    path:        "" => "<computed>"
    path_part:   "" => "eb"
    rest_api_id: "" => "${aws_api_gateway_rest_api.EB.id}"

+ aws_api_gateway_resource.env_name
    parent_id:   "" => "${aws_api_gateway_resource.eb.id}"
    path:        "" => "<computed>"
    path_part:   "" => "{env_name}"
    rest_api_id: "" => "${aws_api_gateway_rest_api.EB.id}"

+ aws_api_gateway_resource.ip
    parent_id:   "" => "${aws_api_gateway_resource.env_name.id}"
    path:        "" => "<computed>"
    path_part:   "" => "ip"
    rest_api_id: "" => "${aws_api_gateway_rest_api.EB.id}"

+ aws_api_gateway_resource.server_num
    parent_id:   "" => "${aws_api_gateway_resource.ip.id}"
    path:        "" => "<computed>"
    path_part:   "" => "{server_num}"
    rest_api_id: "" => "${aws_api_gateway_rest_api.EB.id}"

+ aws_api_gateway_rest_api.EB
    description:      "" => "get EB info"
    name:             "" => "EB"
    root_resource_id: "" => "<computed>"


Plan: 18 to add, 0 to change, 0 to destroy.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
$ terraform apply
aws_api_gateway_rest_api.EB: Creating...
  description:      "" => "get EB info"
  name:             "" => "EB"
  root_resource_id: "" => "<computed>"
aws_api_gateway_rest_api.EB: Creation complete
aws_api_gateway_resource.eb: Creating...
  parent_id:   "" => "k9x3d7qlhd"
  path:        "" => "<computed>"
  path_part:   "" => "eb"
  rest_api_id: "" => "mdsyn3w42a"
aws_api_gateway_resource.eb: Creation complete
aws_api_gateway_resource.env_name: Creating...
  parent_id:   "" => "nr2lkm"
  path:        "" => "<computed>"
  path_part:   "" => "{env_name}"
  rest_api_id: "" => "mdsyn3w42a"
aws_api_gateway_resource.env_name: Creation complete
aws_api_gateway_resource.ip: Creating...
  parent_id:   "" => "g29h7n"
  path:        "" => "<computed>"
  path_part:   "" => "ip"
  rest_api_id: "" => "mdsyn3w42a"
aws_api_gateway_resource.ip: Creation complete
aws_api_gateway_resource.server_num: Creating...
  parent_id:   "" => "sthj28"
  path:        "" => "<computed>"
  path_part:   "" => "{server_num}"
  rest_api_id: "" => "mdsyn3w42a"
aws_api_gateway_method.ip_get: Creating...
  api_key_required: "" => "0"
  authorization:    "" => "NONE"
  http_method:      "" => "GET"
  resource_id:      "" => "sthj28"
  rest_api_id:      "" => "mdsyn3w42a"
aws_api_gateway_method.ip_get: Creation complete
aws_api_gateway_method_response.ip_200: Creating...
  http_method: "" => "GET"
  resource_id: "" => "sthj28"
  rest_api_id: "" => "mdsyn3w42a"
  status_code: "" => "200"
aws_api_gateway_integration.ip_get: Creating...
  http_method:                        "" => "GET"
  integration_http_method:            "" => "POST"
  request_templates.#:                "" => "1"
  request_templates.application/json: "" => "{ \"env_name\": \"$input.params('env_name')\" }"
  resource_id:                        "" => "sthj28"
  rest_api_id:                        "" => "mdsyn3w42a"
  type:                               "" => "AWS"
  uri:                                "" => "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:123456789012:function:eb_ip/invocations"
aws_api_gateway_resource.server_num: Creation complete
aws_api_gateway_method.server_num_get: Creating...
  api_key_required: "" => "0"
  authorization:    "" => "NONE"
  http_method:      "" => "GET"
  resource_id:      "" => "9w68fs"
  rest_api_id:      "" => "mdsyn3w42a"
aws_api_gateway_method_response.ip_200: Creation complete
aws_api_gateway_method_response.ip_400: Creating...
  http_method: "" => "GET"
  resource_id: "" => "sthj28"
  rest_api_id: "" => "mdsyn3w42a"
  status_code: "" => "400"
aws_api_gateway_integration_response.ip_get_200: Creating...
  http_method:                         "" => "GET"
  resource_id:                         "" => "sthj28"
  response_templates.#:                "" => "1"
  response_templates.application/json: "" => "$input.path('$')"
  rest_api_id:                         "" => "mdsyn3w42a"
  status_code:                         "" => "200"
aws_api_gateway_integration.ip_get: Creation complete
aws_api_gateway_method.server_num_get: Creation complete
aws_api_gateway_method_response.server_num_200: Creating...
  http_method: "" => "GET"
  resource_id: "" => "9w68fs"
  rest_api_id: "" => "mdsyn3w42a"
  status_code: "" => "200"
aws_api_gateway_integration.server_num_get: Creating...
  http_method:                        "" => "GET"
  integration_http_method:            "" => "POST"
  request_templates.#:                "" => "1"
  request_templates.application/json: "" => "{\n \"env_name\": \"$input.params('env_name')\",\n \"server_num\": \"$input.params('server_num')\" \n}"
  resource_id:                        "" => "9w68fs"
  rest_api_id:                        "" => "mdsyn3w42a"
  type:                               "" => "AWS"
  uri:                                "" => "arn:aws:apigateway:ap-northeast-1:lambda:path/2015-03-31/functions/arn:aws:lambda:ap-northeast-1:123456789012:function:eb_ip/invocations"
aws_api_gateway_integration_response.ip_get_200: Creation complete
aws_api_gateway_method_response.ip_400: Creation complete
aws_api_gateway_integration_response.ip_get_400: Creating...
  http_method:                         "" => "GET"
  resource_id:                         "" => "sthj28"
  response_templates.#:                "" => "1"
  response_templates.application/json: "" => "$input.path('$').errorMessage"
  rest_api_id:                         "" => "mdsyn3w42a"
  selection_pattern:                   "" => "[^0-9](.|\n)*"
  status_code:                         "" => "400"
aws_api_gateway_integration.server_num_get: Creation complete
aws_api_gateway_method_response.server_num_200: Creation complete
aws_api_gateway_method_response.server_num_400: Creating...
  http_method: "" => "GET"
  resource_id: "" => "9w68fs"
  rest_api_id: "" => "mdsyn3w42a"
  status_code: "" => "400"
aws_api_gateway_method_response.server_num_400: Creation complete
aws_api_gateway_integration_response.ip_get_400: Creation complete
aws_api_gateway_integration_response.server_num_get_200: Creating...
  http_method:                         "" => "GET"
  resource_id:                         "" => "9w68fs"
  response_templates.#:                "" => "1"
  response_templates.application/json: "" => "$input.path('$')"
  rest_api_id:                         "" => "mdsyn3w42a"
  status_code:                         "" => "200"
aws_api_gateway_integration_response.server_num_get_200: Creation complete
aws_api_gateway_integration_response.server_num_get_400: Creating...
  http_method:                         "" => "GET"
  resource_id:                         "" => "9w68fs"
  response_templates.#:                "" => "1"
  response_templates.application/json: "" => "$input.path('$').errorMessage"
  rest_api_id:                         "" => "mdsyn3w42a"
  selection_pattern:                   "" => "[^0-9](.|\n)*"
  status_code:                         "" => "400"
aws_api_gateway_integration_response.server_num_get_400: Creation complete
aws_api_gateway_deployment.eb_deployment: Creating...
  rest_api_id: "" => "mdsyn3w42a"
  stage_name:  "" => "prod"
aws_api_gateway_deployment.eb_deployment: Creation complete

Apply complete! Resources: 18 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

できた!

考察

  • 依存関係

リソースを作成するのに並列処理が出来なかったり、依存関係がまだうまく対応できてないので、depends_onを駆使する必要があるのが若干まだ面倒。ない場合は、BadRequestException: Unable to complete operation due to concurrent modification. Please try again laterBadRequestException: No integration defined for method status code: 400等のエラーが発生する。

  • Integration/Method Response Headers

CORS等の設定するする際にはAccess-Control-Allow-Origin等のヘッダーをMethodやIntegrationのResponse Headerに設定をする必要があるけど、.の扱い問題で未対応(#2143)。それまではawscliで以下のようにすると事で回避。

1
aws apigateway update-integration-response --rest-api-id $rest_id --resource-id $appo_resource_id --http-method OPTIONS --status-code 200 --patch-operations op=add,path="/responseParameters/method.response.header.Access-Control-Allow-Headers",value="\"'Content-Type,X-Amz-Date,Authorization,X-Api-Key, Access-Control-Allow-Origin, x-amz-security-token'\""

Issueはこの前上げたので(#6092)、ウォッチしておくと良い。

  • Infrastructure as Code

API Gatewayはresource, method, integration, method responseintegration response等を記述しないといけないので、どうしてもコードが多くになってしまう事からSwaggerでやった方が楽だったりするかも。ただ、その場合はInfrastructure as YAMLになってしまうけど。。また、YAMLは整形してからimportする必要があったりするので、その辺は諸々トレードオフかなぁ。

Terraformで特定のブランチを使う

HashiCorpのTerraformの開発は非常に活発であり日々進化していますが、リリースまでに待てない実装中や実験的な機能を使いたい場合は、自分で特定のブランチを変更・ビルドして使えます。

リポジトリ取得

1
2
3
$ go get github.com/hashicorp/terraform
$ cd $GOPATH/src/github.com/hashicorp/terraform
$ git checkout new-aws-feature

commit hash付与

どのコミットリビジョンを使っているか簡単に分かるように、versionにgitのcommit hashを指定。

1
sed -i -e "s/dev/dev - `git rev-parse --short HEAD`/" terraform/version.go

ビルド

make devすると全pluginやproviderがコンパイルされ、非常に時間がかかる(特に最近は対応範囲の広がりが著しく、生成されるバイナリの総容量が増加傾向)。 よって、特定のproviderを指定して時間短縮する。以下はaws providerで作業している場合。

1
2
3
4
5
6
7
$ make core-dev plugin-dev PLUGIN=provider-aws
==> Checking that code complies with gofmt requirements...
/Users/ijin/golang/bin/stringer
go generate $(go list ./... | grep -v /vendor/)
go install github.com/hashicorp/terraform
go install github.com/hashicorp/terraform/builtin/bins/provider-aws
mv /Users/ijin/golang/bin/provider-aws /Users/ijin/golang/bin/terraform-provider-aws

確認

1
2
$ terraform version
Terraform v0.6.15-dev - 123abcd

Happy Terraforming!

LambdaでSSHやGitを使ってみよう

AWS Lambda上でsshgitを使えたら便利!思ったけど、そもそもバイナリ自体が入ってない上にパッケージのインストールが出来ないので回避方法を悩んでいたところ、幸いそれぞれPython nativeの実装があったので先人の肩に乗っかる事で事無きを得ました。

SSH

SSHはParamikoというライブラリを利用。s3上にSSE-KMSで暗号化されたキーファイルをダウンロードし、それを使ってサーバと認証しログイン。

注意点

  • Lambdaの実行環境にはデフォルトでParamikoが入ってないので、deployment packageに含める必要がある
  • Mac上ではpythonライブラリの互換性が衝突する為、Linux(EC2等)上でpackageを作成する必要がある
  • 最近発表されたVPC対応を使ってprivate networkで通信をしたい場合、外部への経路はそのままでは不可なので、s3の場合はVPC endpointを作成するかNATが必要になってくる。詳しくはこの記事を。

Git

Gitはdulwichというライブラリを利用。sshプロトコルの場合、デフォルトではシステム上のsshが利用されるのでPython版のParamikoSSHVendorクラスを使えばすんなりいくと思いきや、キーファイル指定が出来なかったのでそこを少し改変。また、Lambda上での特殊な環境の為かsys.stderr のencode周りがうまく検出されなかったので、dulwich.porcelain.push methodも若干修正。

Paramikoを使っているので、上記同様パッケージ作成はLinux上で。

以下はprivate repositoryをclone後、別branchに新規ファイルを追加後にcommitし、githubへpushする例。

それでは、良いLambdaライフを!

Lambdaの容量を監視しよう

2016/1/14現在、AWS Lambdaにはなんとリージョン毎!にアップロードできるパッケージの合計サイズがたったの1.5GBという悲しい制限があります。特にlibraryを同包したり、versioningを使ったりしてCIをガンガン回してると、結構すぐこの上限に達してしまいがちです。そこで、Lambdaの総容量はAWSコンソール上には表示されるものの、トラッキングし辛いので監視する仕組みを作ってみました。

仕組み

LambdaのScheduled Eventsを使って、ListFunctionsListVersionsByFunction APIを叩いて、個別functionのCodeSizeをサマって、PutMetricDataでCloudWatchに投げて、Alarm設定してるだけ。

(*) 2016/1/26 追記:@marcy_teruiさんからのご指摘でVersionsの容量計算が抜けてました。ありがとうございます。

といっても、今後別アカウントでいちいち設定(IAM role&policy、Lambda、SNS、CloudWatch)するのも非常に面倒くさいので、今回はCloudFormation Designerを使って、ほぼ一発で環境を再現できるようにしました。

CloudFormation

ボタン作ってみた。

Stack Creation

Designerではこんな感じ。心なしか、jsonの苦痛が多少楽になったような。。後、Propertyの補完機能は良いけどショートカットがCmd+SpaceなのでSpotlightさんがぁ。

s3からtemplateを指定。 template urlはhttps://s3-ap-northeast-1.amazonaws.com/ijin/aws/lambda/check_lambda_capacity/check_lambda_capacity.template

Parameterとしては以下が指定可能

  • アラート閾値(Byte単位)
  • SNS topic(空の場合は、自動作成される)

Stackを作成すると、諸々のリソースが数分で出来上がり。

Manual Labor

本当は以上で終了!にしたいところですが、LambdaのScheduled Eventsの設定はAWSコンソールからのみしか出来ないという情けない残念な状態2016/1/14現在)なので、ここからポチポチ設定作業。。(API重視の開発姿勢はどこ行ったんだろう)

(*) 2016/1/26 追記:この記事の翌日に発表された CloudWatch Eventsscheduling 機能 によって出来るようになりました。なんというタイミング。

最小頻度が5分毎

Graph

これで、グラフが取れて閾値を超えたらアラートが飛ぶようになる

Code

出来上がったCloudFormation templateコードはこちら。AWS::Lambda::Functionがlambdaのresource担当だけど、templateにfunctionをインラインで埋め込めるのは現時点ではnodejsのみなので仕方なくzipしたpythonコードをs3にアップして参照するようにしてる。

元気があれば、そのうちnode版も書こうかな。。

GitHubのリポジトリはこちらから

バリ島のHubudでコワークしてきた

東京で借りているマンションのエレベータが総入れ替えで1週間完全停止する事になったので、ふらっとインドネシアのバリ島に来てみました。 仕事柄リモートワークがしやすいで、ついでにバリ島のウブドというエリアにある噂のコワーキングスペース「Hubud」で作業してみることに。

場所

インドネシアのバリ島は日本からやや西にほぼ南下しただけなので、時差は1時間程度。


山岳エリアにあるウブドという地域。



そこにあるMonkey Forestというジャングルの近く。猿がそこら中に。

Hubud

HUB in UBUDなのでHubudという名称。

$60〜の月額プランを支払う事によって、館内で使える時間数が決まる。平日は24時間アクセス可能で、土日は22時ぐらいに閉店。(2015年12月現在)

今回は$60(+$3の処理手数料)を事前でPaypalで払って、25時間プランに加入。

ちなみに、bitcoinでも支払い可能。

中にはbitcoin自販機も!

館内には24時間いつでも入れるので時間のトラッキングはどうなってるかというと、恐らく登録時にノートのwifiを設定したので、そのmac addressで識別していると推定。

内装

リラックスした感じでアットホームな雰囲気。

フリーデスクで館内でも中庭でも作業可能。

お気に入りの作業場所。但し、夜になると蚊が出てくるので蚊よけスプレー必須。

2階にはゆったりソファも。

free coffee!

通信環境

宿のwifiや現地のキャリアで買えるSIMカードによる通信品質はそれほど良くなく、時間と場所によってはパケットロスが大量に発生。

しかし、Hubudに引いている回線は帯域が太く、レイテンシーもほとんど感じられずに超快適!ストレスなく通信ができるので日本にいるのとさほど変わらないぐらいの感触。

まとめ

たまには作業場所を替えて仕事して見るのも一興です。異なる環境に身を置くことによって、いつもとは違った感じの集中力が発揮でき、短時間で没頭してproductivityが向上する場面がいくつかありました。(昼間の熱い時間はプールに入って涼んで、夕方から夜まで作業するスタイルが結構効率的でした)また、いろんな国やバックグラウンドの人がいるので会話してみると良い刺激にもなるので是非一度体験してみてはいかがでしょうか。

LambdaのログをSlackで見よう

今年もやるよ!AWS Lambda縛り Advent Calendar 2015の10日分です。

背景

AWS Lambdaで開発してるとちょこちょこ実行ログを見たりします。cliであれば、@sgwr_dtsさんのlambchoptail的に使えて素敵なんだけど、後で見返したり、検索したりするので、最近ではログをSlackに通知するようにしているのでその紹介を。

イメージはこんな感じ。

ログはCloudWatch logsに溜まるのでsubscriptionさえ出来れば、別にソースはLambdaじゃなくても良いんですけどね。

Lambda

CloudWatch logsのイベントをparseして、日付の色付けやタイムゾーン変換等ちょっこっと加工してメッセージと共に指定の#channelに飛ばすようにし、別のSlack通知用lambda functionをinvokeしているだけですね。 (Slack用のlambdaは以前のエントリを参照)

何故Slackの部分を別functionにしてるかというと、最小単位の機能の切り出しによるportabilityとcross-account間のinvokeが可能となるreusabilityからです。

紐付け

cloudwatch logsにlambda呼び出しの権限設定

1
2
3
4
aws --region ap-northeast-1 lambda add-permission --function-name "cloudwatch_logs" \
  --statement-id "logs-my_lambda" --principal "logs.ap-northeast-1.amazonaws.com" \
  --action "lambda:InvokeFunction" --source-account "123456789012" --source-arn \
  "arn:aws:logs:ap-northeast-1:123456789012:log-group:/aws/lambda/my_lambda:*"

subscription filterの作成

1
2
3
4
aws --region ap-northeast-1 logs put-subscription-filter \
  --log-group-name "/aws/lambda/my_lambda" --filter-name logs-my_lambda \
  --filter-pattern ""
  --destination-arn arn:aws:lambda:ap-northeast-1:123456789012:function:cloudwatch_logs`

実行

これでlambda functionが実行されると、Slackにログが通知されます。

うん、見やすい。

問題点

これでログが飛ぶようになったのは良いですが、Lambdaが実行されてからSlackへの通知まで10数秒とやや長めの時間がかかってしまうのが現在の難点です。(Immediate Feedbackはないものの履歴や検索用途には十分だけど)

調べてみるとLambdaからCloudWatch logsへの書き出しが一番時間がかかっているのが判明。Logsへ書き出されたら、そこからの処理時間は1〜2秒とちょっと前に発表されたCloudWatch logsのnear realtime processingの通りなので、早くLambda -> CloudWatch logsも同じような処理時間を実現して欲しいものです。

まあ、別にCloudWatch logsにさえ入れば早いので、lambda logに限らずいろいろ応用できそうですが。

では、Happy Lambda Life!

参考

Elastic Beanstalkへの簡単ssh

AWS Elastic Beanstalk上で管理されているinstanceにsshするにはeb sshコマンドを使えば割りと簡単に接続できますが、アクセスキーの設定が必要で、開発者が多い場合にそれを発行してばら撒いて管理するのはかなり面倒です(IAM user/roleの割当にしても)。特に極稀にしか接続する必要がない場合。

そこで、API GatewayとLambdaでinstanceのpublic ipを返却するエンドポイントを作り、SSH configを設定すれば誰でもアクセスできるようにしてみました。

前提条件

  • instanceにはログインユーザーのssh keyが登録積み(.ebextensions等で)
  • ssh localhost可能である
  • Elastic Beanstalkのenvironment名は数値で終わらない
  • Auto ScalingのTermination PolicyがNewestInstance

Lambda

やってる事は単純で自動的に付与されるelasticbeanstalk:environment-nameタグのついたinstanceのipを(パラメータに応じて)返却するだけ。複数ある場合は、起動した順で。

せっかくなので新しくサポートされたPythonで記述。

当然、LambdaのIAM roleでec2:Describe*のAction許可も必要。

Swagger

API Gatewayのresourceをコンソールでポチポチ作るのがイヤだったのでAPIの状態をSwaggerで定義してみた。awscliは生REST APIが辛い

編集しながらドキュメントも見れるonline editorが結構便利。

API Gateway

Swagger上でYAMLで作成した定義書はjsonとしてexportし、aws-apigateway-importerでAPIのresourceやmethodやらを生成する。

import処理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$ ./aws-api-import.sh -c swagger.json
2015-11-04 02:23:18,507 INFO - Attempting to create API from Swagger definition. Swagger file: swagger.json
reading from swagger.json
2015-11-04 02:23:18,649 INFO - Parsed Swagger with 2 paths
2015-11-04 02:23:18,655 INFO - Creating API with name EB API
2015-11-04 02:23:19,417 INFO - Removing default model Error
2015-11-04 02:23:19,634 INFO - Removing default model Empty
2015-11-04 02:23:19,872 INFO - Creating model for api id nx3r6d6yhh with name IP
2015-11-04 02:23:20,114 INFO - Generated json-schema for model IP: {"type":"object","properties":{"ip":{"type":"string","description":"IP address."}},"definitions":{}}
2015-11-04 02:23:20,329 INFO - Creating model for api id nx3r6d6yhh with name Error
2015-11-04 02:23:20,338 INFO - Generated json-schema for model Error: {"type":"object","properties":{"code":{"type":"integer","format":"int32"},"message":{"type":"string"},"fields":{"type":"string"}},"definitions":{}}
2015-11-04 02:23:20,770 INFO - Creating resource 'eb' on bd5pqsq37h
2015-11-04 02:23:21,420 INFO - Creating resource '{env_name}' on 8rjsbt
2015-11-04 02:23:22,079 INFO - Creating resource 'ip' on 4cg26s
2015-11-04 02:23:22,931 INFO - Creating method response for api nx3r6d6yhh and method GET and status 200
2015-11-04 02:23:23,144 INFO - Creating new model referenced from response: IPaddresses
2015-11-04 02:23:23,144 INFO - Creating model for api id nx3r6d6yhh with name IPaddresses
2015-11-04 02:23:23,153 INFO - Generated json-schema for model IPaddresses: {"type":"string","definitions":{}}
2015-11-04 02:23:23,770 WARN - Default response not supported, skipping
2015-11-04 02:23:23,772 INFO - Creating method parameter for api nx3r6d6yhh and method GET with name method.request.path.env_name
2015-11-04 02:23:23,998 INFO - Creating method for api id nx3r6d6yhh and resource id md6qcm with method get
2015-11-04 02:23:24,822 INFO - Creating resource '{server_num}' on qmd6mc
2015-11-04 02:23:25,689 INFO - Creating method response for api nx3r6d6yhh and method GET and status 200
2015-11-04 02:23:25,896 INFO - Found reference to existing model IPaddresses
2015-11-04 02:23:26,306 WARN - Default response not supported, skipping
2015-11-04 02:23:26,306 INFO - Creating method parameter for api nx3r6d6yhh and method GET with name method.request.path.env_name
2015-11-04 02:23:26,525 INFO - Creating method parameter for api nx3r6d6yhh and method GET with name method.request.path.server_number
2015-11-04 02:23:26,764 INFO - Creating method for api id nx3r6d6yhh and resource id 5udxeo with method get

これでAPI Gateway上に階層が出来上がり。

後は各GET methodでLambdaへの関連付けをし、Integration RequestIntegration Responsemapping templateでパラメータやレスポンスを設定。(多分、この手順もswaggerで定義できるとは思うけど)

integration request

integration response

参考:

endpoint

Deployすれば、以下のAPIでipが取得可能。

/$API_ENDPOINT/prod/eb/$EB_ENV_NAME/ip/[n]

instance ip一覧

1
2
3
$ curl -s https://altvsxa2kj.execute-api.ap-northeast-1.amazonaws.com/prod/eb/my-ebenv-production/ip
54.189.149.5
54.189.168.83

特定のinstance ip

1
2
$ curl -s https://altvsxa2kj.execute-api.ap-northeast-1.amazonaws.com/prod/eb/my-ebenv-production/ip/1
54.189.149.5

ipの部分だけ、PATHじゃなくquery paramterにした方がAPI Gatewayの設定がシンプルだったけど、少しでもRESTfulにしたかったもので。(まあ、そもそもjson返してないけど)

SSH Config

さて、ここからがキモです。

~/.ssh/config

1
2
3
4
Host my-ebenv-production*
  User ec2-user
  StrictHostKeyChecking no
  ProxyCommand ssh -A localhost -W `N=$(grep -o '[0-9]\+$' <<< %h || echo 1); E=$(perl -pe 's/\d+$//' <<< %h); curl -s https://altvsxa2kj.execute-api.ap-northeast-1.amazonaws.com/prod/eb/$E/ip/$N`:%p

上記の設定でssh $EB_ENV_NAME[n]で指定したサーバに接続できるようになります。

仕組みとしては、ホスト名をparseしAPI Gatewayに適切なリクエストを行い、返却されるpublic ipにローカル端末自身を踏み台としてsshするProxyCommandの設定です。

例えば、2番目(に起動した)のサーバに接続する場合は

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ssh my-ebenv-production2
Last login: Wed Nov  4 01:19:14 2015 from ndx6-ppp413.tokyo.sannet.ne.jp
 _____ _           _   _      ____                       _        _ _
| ____| | __ _ ___| |_(_) ___| __ )  ___  __ _ _ __  ___| |_ __ _| | | __
|  _| | |/ _` / __| __| |/ __|  _ \ / _ \/ _` | '_ \/ __| __/ _` | | |/ /
| |___| | (_| \__ \ |_| | (__| |_) |  __/ (_| | | | \__ \ || (_| | |   <
|_____|_|\__,_|___/\__|_|\___|____/ \___|\__,_|_| |_|___/\__\__,_|_|_|\_\
                                       Amazon Linux AMI

This EC2 instance is managed by AWS Elastic Beanstalk. Changes made via SSH 
WILL BE LOST if the instance is replaced by auto-scaling. For more information 
on customizing your Elastic Beanstalk environment, see our documentation here: 
http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/customize-containers-ec2.html

1番目の場合は明示的に指定する必要はありません。

1
$ ssh my-ebenv-production

また、Auto Scalingでinstanceが増減せずに1台のみの場合はもっとシンプルに記述できます。

1
  ProxyCommand ssh -A localhost -W `curl -s https://altvsxa2kj.execute-api.ap-northeast-1.amazonaws.com/prod/eb/$E/ip`:%p

簡単!

でも、よくよく考えたら普通のAuto Scalingの場合でも利用できる事に気付きました。

まとめ

  • Swaggerなかなか面白い
  • API GatewayとLambda便利
  • Lambda pythonも良い。けど、rubyサポートも早う。。

AWS re:Invent 2015に参加してきた

今年で3度目の参加となるAWS re:Invent。 忘れない内に記録を残しておきます。

Day 0

Game Day

Unicornを貸し出すサービスを展開する仮想のスタートアップ企業にDevOpsチームとして最近入社したという設定。前任者が退職しており、資料が少ない中でサービスオープンに立ち会いつつ、様々な困難に直面するというフルデイ・イベント。 今までのGame Dayと違って面白いのはパフォーマンス・チューニングをしつつも、コストも意識しながらチーム間でスコアを競争するところ。アプリは触れないので、ISUCONよりは昔やったチューニンガソンに近い感じ。

スコアは累積の損益。アーキテクチャによっては利益が出たり損失が出たりする。例えば、多くのリクエストが処理できると利益は増すが、AWSのリソースが多いと費用が掛かって損失になりうる。 当然最初は各チームは赤字から始まり、時間とともに積算した利益によって黒転して行く様が目新しかった。

結果、48チーム中で6位。(上位2チームはチートで失格となったので実質は4位

ちなみに最速レスポンスタイムはうちのチームが叩きだした。

詳細は今度日本で開催されるかも知れないので控えておくが、非常に楽しめたので次回は運営側に回って手伝おうかと思います!

Day 1

Keynote

Andy Jessy副社長による発表。今年のテーマは「自由

  • Amazon QuickSight
  • Amazon Kinesis Firehose
  • Amazon Snowball
  • MariaDB for RDS
  • AWS Database Migration Service
  • AWS Schema Conversion Tool
  • AWS Config Rules
  • Amazon Inspector

Oracleからの自由、解放!

WRK306 - AWS Professional Services Architecting Workshop

実在した、ある企業のクラウド移行案件。RFP的なモノがあり、アーキテクチャをチーム内で議論し、最後にそれぞれ各チームが発表していく流れ。 かつてjawsugで主催を手伝ったワールドカフェ形式とほぼ同じだったので、チームメンバーを先導してCacooでさくさく構成図を起こしていく。 他のチームが模造紙にラフスケッチで発表する中、我らは綺麗に正本して、プロジェクターで登壇しながら発表。

最後に実際にどう移行したかというAWSチームからの回答。まず、移行フェーズを段階的に分け、最初はシステムをほぼそのままクラウド上に乗せた後に部分的に最適化してコンポーネントを置き換えて行ったという話。最後にLambdaになっていた部分があったのが興味深かった。

早く新サービスに対応したAWS Simple Iconsのアップデートが待たれるところ。

今回提案した内容。

WRK305 - Zombie Apocalypse Survival: Building Serverless Microservices

Zombie Apocalypseが起こって、人類存亡の危機!途中まで実装されたチャットルームの機能を拡張・実装して危機を救え!というシナリオの元、LambdaとAPI Gatewayとjavascript sdkで実装されたサーバーレスアーキテクチャのワークショップ。

機能拡張の為に実装が必要なので、設計しながらチーム内で作業分担し、コードをせっせと書いていく。 ゾンビ出現のアラート通知、ヒートマップ描画、アンデッドカウンター、緊急食料倉庫の位置情報配信等、面白い機能要求が盛り沢山。

ささっとSlack部屋を作り、githubでコードを共有しながらのチームワーク作業。多分、うちらのチームが一番多く実装できた感触。

このワークショップはかなりの人気で、開始30分前にすでに長蛇の列が。 運良くぎりぎり最後の参加者として入れたけど、皆どれだけゾンビが好きなんだ。。

Day 2

Keynote

Wernerl Vogels CTOの発表。

  • Amazon Kinesis Analytics
  • X1 instance (100 cores, 1TB RAM)
  • t2.nano instance
  • Amazon EC2 Container Registry
  • Lambda
    • VPC support
    • Long running Functions (300s)
    • Scheduled Functions
    • Custom Retry Logic
    • Python
  • AWS IOT

前日にAndyが7つの自由を語って、当日はWernerが7つの法則を語る。

WRK308 - AWS + ASK: Teaching Amazon Echo New Skills

Amazon Echoを使った、Alexaのプログラミングワークショップ。音声によって、Echo経由でLambdaイベントを呼び出し、Alexaサービスと連携するカスタマイズしたスキルセットを実装して行く。

例えば、Alexaに好きな色を覚えさせて、後ほど聞くと答えてくれる機能とか。全てボイスコントロール。吉田さんの英語でも通じたので、かなり優秀。

新品のEchoを開封して使ったので、最後に貰える物かとささやかに期待したものの、$15のAWSクーポン配布のみ。さすがケチFrugalなAmazonさん。

re:Play

EDMの若きプリンスDJ、Zeddをシークレットゲストとして呼んでのアフター。

もう完全にWernerのパーティー。

Zeldaのremixが良かった。

終わりに

結局セッションは一つも出なかったです。まあ、ビデオやスライドは公開されるので内容自体は後で把握可能なので別にいいかな。授業を聞きに来た分けでもないし。 それよりも、現地に来ているエンジニアと交流したり、実装まで含んだハンズオンのワークショップをやった方が楽しいし、糧となる。後はトレンドを肌感覚として体感するには良い場所なので行った事ない人には是非オススメしておきたい。