年中アイス

いろいろつらつら

rnzoo 0.5.0開発中

EC2関連のコマンドをGoで実装したrnzooですが、0.5.0に向けて追加実装を色々やってます。 2017/03/23時点で0.5.0-dev5をリリースしています。devバージョンを使いたい場合は、以下で入ります。

brew tap reiki4040/rnzoo
brew install --devel rnzoo

0.5.0のゴール

大きくは2つです。片手落ちだった機能の拡充と、安全面への配慮。

EC2の作成から削除までライフサイクルを一通りできるようにする

新規作成 -> 停止 -> 起動 -> 停止 -> タイプ変更 -> 起動 -> 削除みたいな流れです。これでAWSコンソール開かなくても起動したりとかの操作は一通りできます。*1

確認をつけてより安全に

使ってもらっている同僚から、確認オプションが欲しいという要望もあり、危なそうなのはデフォルト確認して、確認なしオプションを追加し、動いてもまぁいいかなというのは、デフォルト確認なし、確認するオプションを追加しました。確認もinstance idでは判断できないので、Nameタグを使うようにして、よりわかりやすくしました。

0.5.0-dev5(2017/3/23時点)のアップデート内容

機能追加

変更

  • 実行前に確認オプションの追加(デフォルトは確認なし)
  • 確認時にinstance idではなくNameタグを表示するように
  • attach-eip --movemove-eipサブコマンドとして独立
  • terminate, move-eip, detach-eipはデフォルト確認ありで確認しないオプションを追加
  • サブコマンドの名称からec2を除いた短縮名を追加

internal

  • 使用ライブラリの最新追いつき(aws-sdk-goを最新にしてIPv6対応)
  • release scriptの整備(tag打ったら、リリースして、homebrewのプルリクまで作る)
  • homebrewのdevel対応の為に、バージョン表記からvを削除

リリース予定

アップデート的にはこの辺りにして、3末あたりに0.5.0としてリリースする予定です。次は0.6.0で、SecurityGroup関連と、内部のリファクタあたりをやろうかと。SecurityGroup自体はpiculetでCodenizeするのがいいと思っているので、あくまでEC2への付与、除去と、一時的なルール変更の位置付けになる予定です。

*1:EC2新規作成時のオプションは多すぎるので自分で使うもののみ。他は必要になったら実装する予定。

rnzooにinstance type変更機能を追加しました

rnzooのdevelopバージョンにec2typeというインスタンスタイプを変更するサブコマンドを追加しました。

# すでに入れている場合はtap不要
brew tap reiki4040/rnzoo

# developバージョンの指定でインストール
brew install rnzoo —devel

AWSコンソールからやると1インスタンスずつしかタイプ変更できず、たまに複数やりたくなるので追加しました。複数選ぶときはctrl+spaceです。また、変更したらどうせ起動するというのもあり、変更後に起動させるオプションも-startとしてつけています。

rnzoo ec2typeで、停止中のインスタンスが選択でき、続けてタイプの選択ができます。未指定の場合、以下の小さめ-標準ぐらいのタイプのみが選択できます。

  • t2.nano
  • t2.micro
  • t2.small
  • t2.medium
  • t2.large
  • c4.large
  • m4.large
  • r4.large

最初全部のタイプ選択できてもいいかなと思いましたが、並び順が面倒なのと、xlargeとか高いのは使わないし、間違って起動することを防ぐために除外しました。未指定の選択肢には出てきませんが、-tオプションは自由に指定できます。起動したい場合は、-t c4.2xlargeといった形で変更できます。新しいインスタンスタイプが増えた時に、更新しなくてもとりあえず、-tで対応できるというのもあります。

起動中のインスタンスを停止して変更も考えたんですが、停止まで待つ必要があり、CLIだと数分待てないかなーというのでやめました。多分やろうと思えばec2stop呼んで、ec2listでstoppedになるまで、定期ポーリングして待つとかでできそうですが、実装はしてません。

また、今回から、次のマイナーバージョンのdevelopブランチを作って、そこに機能追加していき、適宜(自分が使いたい時に)developバージョンとしてリリースしていこうとしています。そしてまとまったら正式版?*1としてリリース予定。この辺りはgit ブランチと開発バージョンなどの練習も兼ねてます。

*1:stableとは違うからなんて表現したやら

Pull Requestを自動で作成する

rnssh, rnzooの新しいバージョンを出そうとするときに、別リポジトリになっているhomebrew-rnssh, homebrew-rnzooを編集してpushするのが、地味に面倒だと思うようになり、他の自動化でも使えるかなと考えて、Pull Requestの自動作成をやってみるようにしてみました。

こんな感じで動かします。今回はversionとsha256 hashを渡してます。

bash genpr.sh 0.0.0 ab01cd23ef45ab01cd23ef45ab01cd23ef45ab01cd23ef45

コードはgistに。rnzooのhomebrewのバージョンアップ対応。

手順としてはすごく単純で、以下を行っているだけ。

  • tmpディレクトリにcloneしてくる
  • sedで該当ファイルを編集する
  • git commitする
  • git pushする
  • hubコマンドでPull Requestを作成する

実際にできたプルリク(テストなのでクローズしてます)

sedで編集している部分は、方法は何でもよく、コードで書き換えられるなら、他の用途にも使えそうです。AWSのCodenizeに使ってるroadworker(Route53)やmiam(IAM)などの定義追加や変更は、この応用でPull Requestを生成できそうな雰囲気。

hubコマンドは、githubCLIで操作できるもので、githubのreleaseも作れるようなので、あとはそこをスクリプトにすれば、リリースが全部自動化できます。最後のhomebrewの反映だけプルリクをマージのワンクッション。*1

rnsshとrnzooのリリース自動化状況

参考

*1:今回の場合は別にPull Requestでなくてmasterにそのままpushするでもいいんですが、練習と個人的に最後の一押しは手でいいかなというところ。

golangのHTTPサーバを構成しているもの

golangのHTTPサーバは、少量のコードで動くものを作ることができます。内部的には、net/httpパッケージのServerHandlerで構成されます。

  • Serverは名前の通り、Serveするもので、HTTPをどのネットワークソースで提供するかを定義するものです。
  • Handlerは、HTTPリクエストを実際にどう処理するかを抽象化したinterfaceで、ServeHTTP(w http.ResponseWriter req *http.Request)を提供しています。

しかし、これらは簡易に使う場合、見えなくなっています。どうしてそうなっているかを理解することで、幾つかのテクニックを知ることができます。

コードが短くなる過程

よく例として出てくるHTTPサーバの実装例は以下のコードです。

通常版

package main

import (
    "net/http"
)

func main() {
    http.HandleFunc("/index", func(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Hello world"))
    })
    http.ListenAndServe(":3000", nil)
}

GET /indexでHello worldを返します

curl http://localhost:3000/index

Hello world

ここにはServerもHandlerも登場しません。これを省略せずに書くと、おおよそ以下のコードになります。

冗長版

package main

import (
    "net/http"
)

func main() {
    m := http.NewServeMux()
    h := new(HelloHandler)
    m.Handle("/index", h)

    s := http.Server{
        Addr:    ":3000",
        Handler: m,
    }
    s.ListenAndServe()
}

type HelloHandler struct{}

func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    w.Write([]byte("Hello world"))
}

通常版と比べると長いですね。この冗長版を以下のような3つのテクニックで短く簡単な通常版にしていきます。

その1 構造体作ってServeHTTP()作る必要をなくす

冗長版では、HelloHandler構造体を定義して、そこにHandlerinterfaceであるServeHTTP(w http.ResponseWriter req *http.Request)を実装しています。

機能を作るたびにServeHTTP()のための構造体を作るのは面倒です。そこで、ServeHTTP()と同じ関数型のaliasであるHandlerFuncが用意されています。

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

HandlerFunc型にServeHTTP()を作り、内部的に自身の関数を呼び出しています。

これによって、2つのメリットが生まれています。

  • わざわざ構造体を定義しなくても良くなった
  • ServeHTTPという関数名に縛られなくなった

それをコードに適用すると

package main

import (
    "net/http"
)

func main() {
    m := http.NewServeMux()

    // HandlerFunc型にキャストして渡す
    m.Handle("/index", http.HandlerFunc(index))

    s := http.Server{
        Addr:    ":3000",
        Handler: m,
    }
    s.ListenAndServe()
}

// ただの関数に
func index(w http.ResponseWriter, req *http.Request) {
    w.Write([]byte("Hello world"))
}

func(http.ResponseWriter, *http.Request)という関数を好きな名前で作り、HandlerFuncにキャストするだけで良くなります。さらに、このキャスト自体も意識しなくても良いです。詳しくは次の節で。

その2 ServeMuxは使うので予め用意する

ServeMuxは、HTTPリクエストに対しての処理をPathごとに登録することができる構造体です。ServeMuxを使わなくても、HTTPリクエストを処理することはできますが、Pathごとの振り分けは余程のことがない限り必ず使うため、省略できるようになっています。

冗長版では、http.NewServeMux()して、PathのパターンとHelloHandler構造体を渡していました。これを短くするために、net/httpは予めパッケージの変数にDefaultServeMuxという変数を持っています。DefaultServeMuxに対する登録を行うことができる関数も用意されていて、そのうちの一つがhttp.HandleFunc(pattern string, func(http.ResponseWriter, *http.Request))です。第一引数のpatternでPathを、そして第二引数はHandlerFuncにキャスト可能な関数型です。前述のHandlerFuncへのキャストを意識しなくて良い理由は、このhttp.HandleFunc()が内部でキャストしてくれるからです。

これによって2つのメリットが生まれています。

  • ServeMuxを自分でnewしなくて良くなった
  • HandlerFuncにキャストせずに、Pathと決まった引数の関数を実装して渡すだけで良くなった

再びコードに適用します。

package main

import (
    "net/http"
)

func main() {
    // ServeMuxは作らず、HandlerFuncへのキャストもしなくていい
    http.HandleFunc("/index", index)

    s := http.Server{
        Addr: ":3000",
        // packageで用意されているDefaultServeMuxを使う
        Handler: http.DefaultServeMux,
    }
    s.ListenAndServe()
}

func index(w http.ResponseWriter, req *http.Request) {
    w.Write([]byte("Hello world"))
}

また少し短くなりました。

その3 Serverも必ず使うので、1発に

Serverは、HTTPをどうServeするかを定義します。基本的にはどのネットワークソースでHTTPを提供するのかと、そのHTTPをどう処理するかのHandlerがあれば動きます。

冗長版では、Serverをnewしてport 3000番をAddrとHandlerを入れて、ListenAndServe()しています。多くの場合、これだけで済むので、その2つだけで渡せば、内部的にServerを動かしてくれるhttp.ListenAndServe(adds string, handler Handler) errorが提供されています。

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

それを適用(+無名関数化)すると、最初に書いた通常版のコードになりました。

package main

import (
    "net/http"
)

func main() {
    // index()は無名関数に
    http.HandleFunc("/index", func(w http.ResponseWriter, req *http.Request) {
        w.Write([]byte("Hello world"))
    })
    // Serverは作らなくても、DefaultServeMuxで起動
    http.ListenAndServe(":3000", nil)
}

通常版では、http.ListenAndServe(adds string, handler Handler) errorの第一引数に、port番号を表す文字列を渡し、第二引数はnilです。nilを渡すとServerの内部で、前述のパッケージ変数のDefaultServeMuxが使われます。*1自分でHandlerを実装して、このListenAndServe()の第二引数に入れてを呼ぶことも可能です。その場合でもServerの作成は省略できます。

ちなみに、Serverの作成を省略しないケースはというと、Serverには他にも各種Timeoutや最大リクエストサイズなど、HTTPを受け付ける上での制約を課すことができます。それを使う場合はServerを自分でnewして、対象の変数に値を入れます。ここ参照*2

まとめ

以上で、冗長版が通常版に省略されていくテクニックを知ることができました。いずれも、ほとんどやることが定型化しているのであれば、できるだけ手間が少なくなるように設計されています。逆に、細かくやりたい場合は、省略せずに書くことで実現できます。

ライブラリを書いたりすると、このバランスが難しかったりしますが、net/httpパッケージで使われているやり方は参考になります。

参考

*1:その2でServerにhttp.DefaultServeMuxを渡していますが、Addrだけで動きます(Handlerは指定しなかったらnilなので)

*2:多くの場合、そういう制約は前段にnginxなどを置いて対応するかと思うので、省略形が提供されているのかなと思います。内部のAPIなどでnginxを前段に置くのは手間だけど、制限したい場合などはこれを使います。

homebrewのformulaフォーマットをチェックする

ふとQiita見てたら、homebrewで配布するという記事に、auditでフォーマットチェックするといいよってコメントがついていました。 goでcliのコマンドを作ってhomebrewで使えるようにしてみた - Qiita

rnzooもrnsshも数年前にpecoを参考に作っただけで、チェックも知らなかったので実行してみました。

homebrew-rnzooのformulaファイルに試してみました。

brew audit --strict --online rnzoo.rb

まず、チェック用に、rubocop のインストールが行われました。ログは記録忘れ。brewで入るようで、ローカルのgem listには出てこないです。結果は、以下の通り。

rnzoo:
  * A `test do` test block should be added
  * `require "formula"` is now unnecessary
  * C: 5: col 12: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.
  * C: 10: col 12: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.
  * C: 14: col 17: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.
  * W: 19: col 5: Useless assignment to variable - `msg`.
Error: 6 problems in 1 formula
  • test doはテスト用のブロックを入れるということで、特にないのでとりあえずブロックを作るだけ。
  • require "formula"はもういらないらしい(古いやつみたい)
  • Cの奴は、\使わないなら、"使うようにということで"に統一
  • Wの奴は、無駄な変数msgに割り当ててるとのことで、変数は除去

test do適当に追加したら、cavertstest doより前じゃないといけないとのこと。

* `caveats method` (line 26) should be put before `test block`

対応したコミットはこちら

セルフチェックできる仕組みいいですね。 homebrewの記事、新規に書いてアップデートしないとかなー。 reiki4040.hatenablog.com

参考

rnzoo 0.4.1をリリースしました

去年ふっとgolang 1.7に上げといたrnzooですが、先日v0.4.1をリリースしました。アップデートはhomebrewなら以下で。

brew update
brew upgrade rnzoo

内容は、rnzoo ec2listに、-t, -tsvオプションを追加しました。rnzoo ec2listは、ec2のインスタンスリストを出力しますが、区切りがタブではなく、見た目重視のスペースになっていて、シェルスクリプトとの連携具合が良くなかったので、TSVで出すようにするオプションを追加しました。

rnzoo ec2list -tsv | grep "stopped" | cut -f1とかで必要なカラムを取り出すことができます。

AWS API Gateway+LambdaでSlackにメッセージをPOSTする(後編)

前編中編にて、AWS API Gateway + Lambdaを使って、post-slack WebAPIを作り、テスト実行まで行いました。後編では、API keyの設定と実際に使えるようにデプロイをしていきます。

Slack Webhook URLは、それを知っていれば誰でも使うことができます。せっかくAPI Gatewayを使うので、API keyを使って制限をかけます。*1Usage PlanとAPI keyを使うと、アクセス自体の制限以外に、使用量の制限、スロットリングをかけることができます。

API GatewayのStageとUsagePlanとAPI key

今回の部分に関して、それぞれ用語とその関連を先に頭に入れると理解しやすいので、先にその説明をします。

Stage

API GatewayのStageは、いわゆる開発(dev)、検証(staging)、本番(prod)といった、動作環境を指します。これらを作ることで、一般的な開発フローと同じく、まず開発環境にデプロイしてーという流れと同じことができます。*2

Usage Plan

Webサービスなどでの、Free PlanやStandard Plan、Unlimited Planみたいなものです。スロットリングや使用量制限を定義します。Usage PlanはStageと紐付ける必要があり、Stageがないと作れません。

API key

対象ごとに発行する認可用のkeyです。API keyは、発行は単体でできますが、Usage Planと紐付けないと、実際にAPIで使うことはできません。

post-slackでの設定

前述の説明で、およそそれぞれの関連性がわかったかと思います。ここからは実際にそれを設定していきます。この部分がAWSコンソール上から作成しようとすると、先に作る必要があるものがあったりなどハマりがちなので、スムーズに行く順番で行っていきます。

API keyの必須設定と、API GatewayのStageの作成(=デプロイ)

まず引っかかりやすいのが、Stageを作成するには、そのWebAPIを1回はデプロイする必要があることです。左メニューから、Stageを選択して、[Create]で進むと、必須項目のDeploymentで選択できるものがなく、「どうやって環境作るんだろう・・・」と感じてしまいます。*3

デプロイするものができるまでStageは不要というのもわかるんですが、API keyで制限をかけるには、Usage Planが必要で、Usage Planの作成にはStageが必要でなので、ハマります。

今回は、API key必須の設定だけしてデプロイすることでAPI keyでの制限を有効にして進めていきます。

post-slack APIのResource -> / POSTを選択します。前回も使ったマス目の並ぶ画面です。Method Requestを選択します。

f:id:reiki4040:20170207233007p:plain:w400

API Key Requiredをtrueに変更します。これだけです。Authorizationというのもありますが、こちらは今回使いません。

f:id:reiki4040:20170207233027p:plain:w400

API key必須の設定ができたので、デプロイしてStageを作ります。[Action]-> Deploy APIを選択します。

f:id:reiki4040:20170207233254p:plain:w400

Deploy APIというダイアログが出てくるので、Deployment Stageで[New Stage]を選びます。そうするとフォームが出てくるので、devというStageにします。Deploymentの所は、デプロイ(リリース)の内容を入れます。今回は適当にfirst deployにしてます。*4 [Deploy]を押すとデプロイされます。

f:id:reiki4040:20170207233137p:plain:w400

URLが割り当てられます。これを後で使うので、コピー等で取っておいてください。 f:id:reiki4040:20170207233356p:plain

WebAPIのURLは以下のフォーマットです。

https://<ランダム>.execute-api.ap-northeast-1.amazonaws.com/<Stage名>

早速curlで試してみます。--dump-header -は、レスポンスヘッダの出力を標準出力に出すオプションです。

curl -XPOST -H 'Content-Type: application/json'  https://<あなたのURL> -d '{"message":"hi"}' --dump-header -

HTTP/1.1 403 Forbidden
Content-Type: application/json
Content-Length: 23
Connection: keep-alive
Date: Tue, 07 Feb 2017 13:39:33 GMT
...AWS独自ヘッダーかとか色々...

{"message":"Forbidden"}

はい。403の権限がないエラーになります。API keyがないからです。

Usage Planの作成とAPI keyの作成

次にUsagePlanを作ります。左メニューのUsagePlanを選択し、[Create]で作り始めます。

f:id:reiki4040:20170211150120p:plain:w300

ここではdevという名称で、Planを作ります。スロットリングは適当に10/secぐらい、burstも10にしておきます。使用制限は1,000/dayぐらいにしてみます。この辺の値は適当なので、適宜どうぞ。入れたら[Next]で次へ。

f:id:reiki4040:20170211150144p:plain:w400 f:id:reiki4040:20170211150153p:plain:w500

どのAPIのStageに適用するかを設定するので、[Add API Stage]を選択し、 f:id:reiki4040:20170211150253p:plain:w400

post-slack APIと先ほど作ったdev Stageを指定して、チェックを選択します。 f:id:reiki4040:20170211150358p:plain:w400

紐付けられたら、[Next]で進みます。 f:id:reiki4040:20170211150404p:plain:w400

次はAPI keyの設定に進みます。[Create API keyand Add to Usage Plan]を選択し、新しくAPI keyを発行します。

f:id:reiki4040:20170211150755p:plain:w400

Nameはnotification-programにします。ここは好きなもので構いません。keyは、Auto Generateでランダムに生成します。入れたら[Save]します。 f:id:reiki4040:20170211150907p:plain:w400

Usage PlanのAPI key一覧に戻るので、notification-programというkeyがあることを確認して、[Done]で完了します。 f:id:reiki4040:20170211151738p:plain:w400

作成後、API keyタブから、notification-programを選択すると、API keyの詳細を見ることができます。

f:id:reiki4040:20170211231517p:plain:w500

API keyのshowを選択すると、値を見ることができます。これをリクエストヘッダに入れて使います。 f:id:reiki4040:20170211151950p:plain

これでpost-slack APIにdev PlanとAPI keyが出来ました。

API keyを使ってcurlからslackへPOST

さていよいよ完成したので、curlを使ってリクエストしてみます。API keyはx-api-keyというリクエストヘッダで送ります。

curl -XPOST -H 'Content-Type: application/json' -H 'x-api-key: <あなたのAPI key>' https://<あなたのAPI URL> -d '{"message":"post slack message from my machine via API Gateway/Lambda!"}' --dump-header -

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 17
Connection: keep-alive
Date: Tue, 07 Feb 2017 14:05:37 GMT
...AWS独自ヘッダーかとか色々...

"posted to slack"

いやーレスポンスがひどい。でもSlackには表示されました! f:id:reiki4040:20170211152540p:plain:w500

これで、HTTPS+API keyでslackへのPOSTができるようになりました。後はスクリプトから叩いたり、他の人に渡す時は別のAPI keyを発行して渡して使うことができます。

参考

*1:もちろん、元のSlack Webhook URL自体へのアクセスを制限できるわけではありません

*2:加えて、簡単にStageを追加できるので、開発フロー上の環境だけでなく、個別用途での環境も作ることができます。

*3:触ってから説明書読むタイプにはハマるポイント

*4:本来はバージョン番号とかそういうのを入れる場所なのかなと。後で別のStageを作る時は、このDeploymentから選択することができます。