自前メールホスティングの行く先 (2)

前回の続きです。

まずはOffice 365へ移行できるかを検討

徐に “office 365 imap 移行” と雑に検索。

さすがにちゃんと手順はありますね…顧客を引っぱるには必須ですからね…。 IMAPからの移行というよりかは、IMAPをソースにした定期的な一方向同期のようです。

  • 旧ホストにO365への転送設定を入れる
  • IMAP同期する
  • 最後にMXを向ける (いきなり向けても転送設定はどのみち新しいMXに切り替わるまでの間必要)

で自分以外のメールボックスはさくっと移行できそうです。 問題は…自分のメールボックスですね。30GBある。

とりあえず、雑に何通あるか数えてみました。なんでかというと、O365が提供している同期機能にはメールの数に制限があるためです。

ユーザーのメールボックスから移行できる項目数は最大 500,000 です (メールは新しいものから順に移行されます)。

で、

find Maildir/ -type f | wc -l`
1906345

あ…うん…190万通ね…そんな気はしてたよ…。15年分あるからね。SpamAssassinがスパムと判定した数を数えてみよう。

find .Junk -type f | wc -l
195059

1割か…消しても焼け石に水ですね(もちろん移行前に消すけど)。 O365の用意する方法では難しそうなので自分でなんとかする必要がありそう。

たしかimapsyncというOSSがあったはず。 (昔ローカルのdovecotとsyncしてオフラインでメール読む環境作った時に使った)

で、これはOSSというかライセンスがちょっと特殊ですね…

そして、すごく詳しいFAQがあった。さすがに商売としてやっているだけある。

EC2のAmazon Linux向けの手順が一番簡単そう。

当面の方針はEC2のスポットインスタンスを使ってimapsyncを動作させて自前メールサーバからOffice 365(Exchang Online)へ移行かな……

今後

  • MXのTTLを短くしとく (2分くらい?)
  • Azure AD作って無料トライアル中にimapsyncを検証する

約90万通あるINBOXが移行できればたぶん問題ないので本移行の作業に入る。

  • 自前メールシステムからO365へ全転送
  • 自分以外のメールボックスをIMAP同期
  • クライアントをO365へ向ける
  • 自分のメールボックスをimapsyncで移行
  • MXを切り替える
  • 自前メールシステムをバックアップ
  • 自前メールシステムを破棄

お楽しみに!(めんどくさいけど)

自前メールホスティングの行く先 (1)

最近まったく記事を書いていませんが、いろいろ考えてはいます。 年単位で考えているのがプライベートで運用しているメールサービスの今後です。 ブログのリハビリも兼ねて書き出してみました。

~~~

現在の構成はさくらのVPS上のMXが1台で、そのなかでPostfix + Dovecot (IMAP) + LDAPでバーチャルドメインを使用し複数ドメインを収容しています。 もともと家族用に提供していましたが、利用者は私を入れて2名に減少しています。 自宅サーバ時代から数えてすでに15年以上提供していますが、当時IMAPを使うには自前でホスティングするしか選択肢がなかったはずです。

WikipediaによるとGmailは2004年ベータ版開始(招待制)、そしてIMAP対応と独自ドメインが使えるサービス(Google Apps; 現在のG Suite)が2007年でした。

1年ほど前まではMXが2台あり、LDAPもレプリケーションしていましたが、 バックアップ側のOSが老朽化してしまったのを機に潔く1台構成にしました。 しかし、現在運用中のプライマリもDebianのWheezyなので来年の5月までとなっており、対応が必要となってきました。

仕事柄、まっさきに考えるのは自前を諦めてどこかのサービスへ移管する案

  • G Suite (30GB 6000円/年/ユーザー)
    • ユーザーによっては30GB付近にいる(というか私)。追加料金をみると20GB追加で350円。
    • 仕事でも使っているので慣れている。
    • プライベートでのアカウントではすてに1TBのドライブ容量を買っているのでちょっと納得がいかない…
  • Office 365 (50GB 6480円/年/ユーザー)
    • G Suiteより月額で40円高いが、メールボックスの容量は足りているので追加の必要はなし。
    • 金額面でもG Suiteより安い。
    • しかし、私はGmailのほうが慣れているので、たぶん全転送する
    • 仕事ではあんまり使えていないでAzureADとかの勉強にいいかも (←前向き)
  • さくらのメールボックス (10GB 1029円/年)
    • 安いが容量が圧倒的に足りないし増やせない…
  • Amazon WorkMail (50GB $60/年/ユーザー)
    • 誰か使っている人いるのだろうか…?ユーザ管理にディレクトリが必要そうなので実際には数人レベルで使うものじゃなさそう。

4つ上げたところで、もしここから選ぶならOffice 365になりそうですね。

このまま自前でいく案

金銭面ではMXが1台なのは困るのでもう1台増やしたい。そうすると、月1000円* 2 で年間24000円、上でいう4ユーザー分に相当しますね。 容量は100GBあるので実際には8ユーザー程度くらい?ユーザー数的には10名越えるくらいなら細々と自前でもいいのかもしれません。

技術的な面では、まずOSに依存してしまうとディストリビューションのライフサイクルに強制的に引っ張られてしまうので PostfixとDovecotがいくら枯れていても作業が発生してしまいます。また、その作業自体もさくらのVPSだと安全にというかゆっくり作業ができません (スナップショットを用意するとか、インスタンスを別に用意して最後にIPアドレスを付け替えるなど)。 プライベートだと片手間に検証しつつ………になるので別インスタンスでこつこつ作業して最後に切り替えたい。 ここはコンテナで解決できます。

もう1つ厄介なのが、複数MX構成時のバーチャルアカウントの同期です。LDAPのレプリケーションはもう御免です。2人しかいないのにオーバーキルです。 今はLDAP上のパスワードを変更する昔書いたRailsアプリを必要に応じて使用。

実のところこの問題が解決できずコンテナ化が進んでいません。要件はパスワードの変更が自分でできて、アカウントの同期が簡単にできることです。

案はいくつかあります。

1つ目はコンテナの内部で動かすのであればシステムアカウント (/etc/passwd) でも問題ないので passwdとshadowを生成するWeb I/Fとそれをバラまく仕組みを自分で用意する。Goで書いてAmazon API Gateway -> Lambda -> S3 SSE-KMSに書いて、コンテナ起動時に取得する、など。 ちょっとダルい。

2つ目はサーバレスSTNS。API Gateway + DynamoDBで構成するSTNSをpam経由で使う。過去に検証したところdovecotで使うとstnsがcgoエラーで死ぬので断念しました。 そのまま使うのは厳しいと判断。ただ、Go + cgoでpamモジュールを書くのはよいと感じました。 もし自分がやるならpamモジュール内部ではローカル上のhttpサーバにunix socket経由で問合せるに留めて、以後バックエンドはユーザースペース側に別途用意する設計にするかな…。 ただ、この場合でもWeb I/Fは必要。

3つ目はAmazon Cognit User PoolsまたはAmazon Cloud DirectoryをバックエンドにしたpamモジュールとそれらをいじくるWeb I/Fを書く。 技術的には面白いし本命感ありますが、ユーザー2名なのにそこまで感がハンパない。

4つ目は……自前LDAPの代わりにAzureADを使う。だったら黙ってOffice 365使うよね…

結局のところ、どの案も手を動かす必要がありますね。

それで

私は自前メールサーバの辞め時を探しているのかもしれません。 今のところ、Office 365が最有力候補ですね。自分でも驚いていますが…。 進捗があったらまた続きを。

俺のaws-sdk-goラッパーライブラリ3兄弟

去る2014年12月にStripeによるstripe/aws-goが公開され、 今年2015年1月にAWSがstripe/aws-goを拾い上げ、awslabs/aws-sdk-goとなり (ちなみにライセンスも元のMITからApacheライセンスになった)、そして その半年後の2015年6月にDeveloper Previewながらもついにaws/aws-sdk-goになりました。

前書きが長くなりましたが、ようやく自分のプロジェクトでもgoamzからaws-sdk-goへの移行を 完了させました。その過程で3つのラッパーライブラリを公開したので紹介します。

aws-go-sqs

https://github.com/nabeken/aws-go-sqs

これは以前書いたものです。stripe/aws-go自体に書いたものを 最新の状態へ追随させました。バッチ処理もちゃんと扱えるので便利だと思います。

aws-go-s3

https://github.com/nabeken/aws-go-s3

新作です。aws-go-sqsと同様にfunction optionパターンを適用し、リクエスト用の構造体を それっぽく操作できます。

http.Request.BodyをそのままS3にアップロードする際、SDKが io.ReadSeeker を 要求しているため、今のところ一時ファイルへ書き出してから改めてS3へアップロードする方法 しかありません(署名とかマルチパートアップロードのため?)。

aws-go-s3の今の実装では素朴に一旦 io.ReadAll したものを一時ファイルへ書き出し、それをSDKへ渡す 方式を採用しています。 今後の改善案としては、書き込みと読み出しを同時にできるようにしつつ、io.Seeker もいい感じに扱う ようにしたいところです。

aws-go-dynamodb

https://github.com/nabeken/aws-go-dynamodb

こちらも新作です。同じくfunction optionパターンでやってみました。

Expression APIを使うとクエリは文字列で表現できるようになったのでAPIクライアント的には楽になりました(意味不明なJSONを組み立てる必要がなくなった)。

DynamoDBで面倒なのはGoの構造体のmarshal, unmarshalです。 goamzではencoding/jsonを流用したmarshaler, unmarshalerがありましたが、 aws-sdk-goではdynamodbattributeパッケージがそれに該当します。

dynamodbattributeパッケージには一つ落とし穴があり、AttributeTypeにSet(StringSet/NumberSet/BinarySet)を使っている場合はこのパッケージでは正しく処理できません。これはオブジェクトがDocument TypeのListとMapで表現されているためです。移行時には注意する必要があります。

aws-go-dynamodbはitem.Unmarshalerインターフェースを用意し、それが実装されていればdynamodbattributeを使わずに このインターフェース経由でunmarshalするようにしています。 実装がなければ dynamodbattribute パッケージを使用します。

aws-sdk-go雑感

  • 1年も掛からずに一気に整備された。AWSの本気を感じた。
  • APIのアップデートがあってもすぐに使用可能になってよい
  • 各サービスのパッケージにはそれぞれAPI部分のインターフェースが自動生成されているのでそれを使うことでテスト時にモックへ差し替えられる
  • このライブラリはプリミティブなものなのでやはりラッパーは自分で書く必要がある

簡単に既存の監視プラグインにPROXY Protocol機能を追加できるツール mikoi をリリースしました

https://github.com/nabeken/mikoi

1つ前の記事でgo-check-smtpを作った話をしましたが、 もう1つ必要な機能がありました。 それがPROXY Protocol対応です。

PROXY Protocolについて

PROXY Protocolはロードバランサーなどのリバースプロキシ環境でオリジナルのピア情報をアップストリームへ伝送するためのL4(TCP)で動作するプロトコルです。 HTTPにおける X-Forwarded-For のL4版です。

http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt

もともとはHAProxyに実装されていた機能ですが、仕様が文書化されていることもあり ELBやPostfixにも実装されています。

例えば、Goでネットワークサーバを書いた場合、go-proxyprotoパッケージを使うことで簡単にPROXY Protocol対応ができます。

PROXY Protocolに対応したアプリケーションはすべてのR/Wの前にまずPROXY Protocolヘッダーを受信し、 オリジナルのピア情報を再構成してからアプリケーション固有の通信を開始する必要があります。

つまり、クライアントがPROXY Protocolヘッダーを送信しないかぎり アプリケーションの通信が開始されません。

この挙動は監視を考える時に問題となります。基本的な監視ではPROXY Protocolを実装した ロードバランサー(たとえばHAProxyやELB)経由でサービスポートを監視することになりますが、 この場合、全サーバに監視用のトラフィックが流れるかはプロキシサーバまかせになります (〜ベースロードバランシング的な)。

さすがにこれはちょっと雑すぎるので各サーバ上で監視エージェントを動かす(sensu/nagios-nrpe)ことになりますが、この時も監視プラグイン(SMTPならcheck_smtp)はPROXY Protocolをしゃべる必要があります。

解決案としては

  1. アプリケーションの設定を変更してPROXY Protocol対応ポートと通常ポートの2つのポートをlisten
  2. ローカルに監視用(あるいは流用)のhaproxyを追加し、それ経由で監視
  3. check_smtpなどのプラグインを改変してPROXY Protocol対応を追加

あたりが思いつきます。

1はアプリケーション固有ですが、実現可能であれば一番手軽な案です。 Postfixであればmaster.cfに追加することで実現可能です。

2は流用可能であれば現実的な解決案だと思われます。 流用できない場合はhaproxyのメンテが増えるので1よりかは複雑になりますが、開発は不要です。 HAProxyのPROXY Protocol対応は1.5からなので割と最近のディストリビューションでない限り、 ソースコードからビルドする必要があります。

3はPROXY Protocol対応したいプラグインすべてに改変が必要です。 nagios-pluginsの場合、ディストリビューション側の パッケージでサクっと入れられる利点を失ないます。

mikoiの提供する解決案

mikoiはephemeralなポート上で動作するPROXY Protocol対応プロキシサーバ機能と それを利用するための監視プラグインに対するラッパーとして動作します。

プロキシとして動作するため既存の監視プラグインへの改変は不要です。 また、監視のためだけのアプリケーションの設定変更やhaproxyの導入も不要です。

もちろんGoで書いたのでバイナリを配置するだけで動きます。

        +----------+
  +---> |  plugin  | forked by mikoi
  |     +----------+
  |       /|\   |
  |        |    |
  |        |   \|/
  |      +---------+            +----------+
  |      |         | <--------- |          |
  +----- |  mikoi  |            |  server  | (proxy protocol enabled)
         |         | ---------> |          |
         +---------+ w/ header  +----------+

例えば、nagios-pluginsに付属の check_smtp にPROXY Protocol対応を追加したい場合:

$ mikoi \
  -P \
  -H smtp.example.com \
  -p 25 \
  -- /usr/lib/nagios/plugins/check_smtp -H 127.0.0.1 -p {} -w 0.5 -c 1.0

{}の部分はmikoiがephemeralポート上に起動させたプロキシサーバのポート番号に置換されます。 プロキシサーバを起動したあと、もともとのプラグインを起動します。 監視トラフィックはmikoiの提供するプロキシ経由で対象サーバへ送られます。

Postfixで試してみる

PROXY Protocolを有効にしたPostfixをmikoi経由のcheck_smtpで監視してみます。 PostfixはDockerで用意します。

git clone git://github.com/nabeken/mikoi.git
cd mikoi/postfix-example
docker build -t local/mikoi-postfix-example .
docker run -it -p 10025:25 local/mikoi-postfix-example

Postfixのコンテナを起動させるとmaillogが流れてくるので別のシェルでプラグインを実行してみます。

/usr/lib/nagios/plugins/check_smtp -H 127.0.0.1 -p 10025
recv() failed

また、maillogにはwarningが出ているはずです。

Jun 14 08:07:32 c00a65e5cc25 postfix/smtpd[161]: warning: haproxy read: timeout error

SMTPでは接続するとサーバ側がgreetingを送ってくるのでクライアントはそれを待ちます。 一方、サーバ側ではPROXY Protocolヘッダーがクライアントから来るのを待つため、 お見合い状態になります。

mikoiを使ってみましょう。check_smtpに与えていた監視対象のアドレスとポートはmikoiへ渡します。 そのかわりに check_smtp には127.0.0.1と {} を与えます。 -PはPROXY Protocol機能を有効にするフラグです。

curl \
  -L https://github.com/nabeken/mikoi/releases/download/v20150613191746/mikoi_linux_amd64.tar.gz \
  | tar -C /usr/local/bin --strip-components=1 -zxvf - mikoi_linux_amd64/mikoi

mikoi \
  -P \
  -H 127.0.0.1 \
  -p 10025 \
  -- /usr/lib/nagios/plugins/check_smtp -H 127.0.0.1 -p {}
SMTP OK - 0.002 sec. response time|time=0.002467s;;;0.000000

うまくいきました。

はたしてどれだけの人がこれを必要としているのかは謎ですが、プログラムの構造としては ひとつのプログラムでサーバの実行とコマンドの実行をする必要があるのでGoっぽさが出たと思います。

機会があればmikoiちゃんをどうぞ。

Goでcheck_smtpを書き直した go-check-smtp をリリースしました

nagios-pluginsに含まれている check_smtp でSMTPコマンドの結果込みで監視できるように Goで書き直してリリースしました。

https://github.com/nabeken/go-check-smtp

check_smtpの -C と -R が動いていない…?

MAIL コマンドと RCPT コマンドの実行結果を含めて監視する要件がありました。 つまり、コマンドの戻り値が200番台以外であればWARNINGあるいはCRITICALにする必要があります。

check_smtpのmanを見ると まさにこのためのオプションとして -C-R が用意されています。しかし、STARTTLSと同時 に使うとこのオプションは使えないことが判明しました。

今回の監視対象はSTARTTLSが必須なため、既存のプラグインは使えませんでした。

監視プラグインとGo

いろいろ理由は付けましたが、要はGoでプラグインを書く口実が欲しかった。 Sensuを触っているとRubyインタプリタが毎分forkしているのを見て微妙な気持ちになりませんか? Goと監視プラグインの相性はとてもよいはずです。

そこで、 go-check-smtpを書きました。 オリジナルの check_smtp にあった-Cと-Rは微妙なのでなくしました。代わりにMAILとRCPTコマンドをきちんとチェックするようにしました。こんな感じ。

$ go-check-smtp \
  -S \
  -F localhost \
  -H 127.0.0.1 \
  -p 10025 \
  -w 1s \
  -c 2s \
  -f sender@example.com \
  -r recipient@example.com
SMTP CRITICAL: RCPT command was not accepted: 554 5.7.1 <recipient@example.com>: Relay access denied

細かい点としては時間間隔の指定がGoのtime.ParseDurationを使うようになるので書きやすくなっています(これは後述するgo-flagsの仕事)。

Travis CIではGithub Releasesに成果物を簡単にデプロイできます

https://github.com/nabeken/go-check-smtp/blob/master/.travis.yml

手元でtagを切り、Githubへpushすると自動的にリリースされます。便利。

go-flagsとnagiosplugin

今回のプラグイン作成ではgo-flagsパッケージを使い、 コマンドライン引数をパースしました。カジュアルに書いてもちゃんと動くのがよいです。

nagios pluginsのAPIについては nagiospluginパッケージを手元にフォークしたバージョンを使いました。 フォークしたのはチェック名をプラグインの実行結果に反映させたかったためです。

<オリジナル版>
OK: 11.733648ms response time

<フォーク版>
SMTP OK: 11.733648ms response time

プラグインは複数のメトリクスから最終的な結果を返す必要があるため、状態を管理する 必要があります。プラグインを使うとその部分を自分で何度も書く必要がないため、便利です。

go-check-smtpがいい感じなので今後新規にプラグインを書く場合は Go + go-flags + nagiosplugin + TravisCIでサクっと書きたいと思います。

Docker 1.6とdevicemapperの話

Ubuntu 14.04(LTS) + get.docker.io版Docker 1.6で devicemapperを使うとコンテナの起動や削除に時々失敗する現象が発生しました。 (devicemapperがデフォルト。。。)

最近、AWS OpsWorksで アプリケーションをDockerで配布仕組みを実装していて、これが原因でデプロイが失敗 することがありました(幸いまだステージング)。

エラー内容

https://github.com/docker/docker/issues/4036 から引用すると、以下のようなエラーが出力されているとこの問題に該当しています。

Error mounting ‘/dev/mapper/docker-8:1-4980769-56bee8c4da5bd5641fc42405c742083b418ca14ddfb4a3e632955e236e23c284’ on ‘/var/lib/docker/devicemapper/mnt/56bee8c4da5bd5641fc42405c742083b418ca14ddfb4a3e632955e236e23c284’: no such file or directory

さらに、docker infoを実行することで問題のある環境かどうかを判断できます。

docker info | grep -i udev
 Udev Sync Supported: false
 WARNING: No swap limit support

ここで、 Udev Sync Supported がfalseなら該当しています。

対応方法

上記の#4036を読むと、 devicemapperのコードを静的リンクしていると発生するようです。

そこで、DockerをOS側のdevicemapperのライブラリとリンクする ようにビルド(つまり普通にビルドする)するとよさそうです。

workaroundとしてAUFSを使う人もいるようですが、upstream(kernel)サポートが入らないこともあり、 現状では非推奨です(使えるのはUbuntuくらい?)。boot2dockerはまだAUFSのようです。

参考: http://www.projectatomic.io/docs/filesystems/

先日リリースされたUbuntu 15.04に含まれるDocker(1.5)を使う場合は Ubuntu Sync Supported がtrueになっているためこの問題には該当しません。 また、おそらく次回リリース以降はget.docker.io版も動的リンク版になりそうなので、 1.7以降では改善されているかもしれません。

参考: https://github.com/docker/docker/issues/4036#issuecomment-76723060

参考: https://github.com/docker/docker/pull/11412 これによると、1.7以降静的リンクされたDockerでdevicemapperが原則利用できなくなります。

残念ながら、OpsWorksはLTS(14.04など)のみ対応しているのでここでは自分でビルド する方法を紹介します。

Docker自体がDocker内でビルドされているので、この仕組みを利用します。 幸い、このコンテナはUbuntu 14.04をベースにしているので、 私の場合は devicemapperをソースではなく、パッケージ版に修正 するだけで大丈夫です。

この修正ブランチを使ってビルドする方法:

git clone git://github.com/docker/docker.git && cd docker
git fetch git://github.com/nabeken/docker.git issues-4036-dynbinary:issues-4036-dynbinary
git checkout issues-4036-dynbinary

sudo docker build -t docker .

sudo docker run --privileged --name docker -e AUTO_GOPATH=1 -e DOCKER_BUILDTAGS=btrfs_noversion docker ./hack/make.sh dynbinary

sudo docker cp docker:/go/src/github.com/docker/docker/bundles/1.6.0/dynbinary/docker-1.6.0 .
sudo docker cp docker:/go/src/github.com/docker/docker/bundles/1.6.0/dynbinary/dockerinit-1.6.0 .

sudo ./docker-1.6.0 info

ここではまだサーバ側を入れ替えていないため、 Udev Sync Supported はfalseです。

ビルドしたバイナリは最終的にはAMIに入れてしまうのでバイナリを直接差し替えます。

sudo service docker stop
sudo cp /usr/bin/docker{,.orig}
sudo cp ./docker-1.6.0 /usr/bin/docker
sudo cp ./dockerinit-1.6.0 /usr/bin/dockerinit
sudo service docker start
sudo docker info | grep -i udev
sudo: unable to resolve host ip-10-15-15-4
 Udev Sync Supported: true
 WARNING: No swap limit support

Udev Sync Supported: true なのが確認できました。 ビルドには時間が掛かるので2つのバイナリを適当な場所に配置し、packer等でAMIに焼きましょう。

おまけ

https://github.com/nabeken/docker/releases/tag/v1.6.0-issues-4036-dynbinary

(Use at your own risk)

PackerでのイメージビルドをCircleCIで実行する

PackerでEC2のAMIをビルドするのはともかく、どこで誰がビルドするのかは悩ましいポイントです。 今回はみんな大好きCircleCIでAMIのビルドとAMI IDの管理する方法を考えてみました。

流れ

  1. GithubのPRを出す
  2. CirleCIで packer validate を実行する
  3. テストにパスしたら実際にPRをマージ
  4. CircleCIが packer build を実行する
  5. ビルドに成功したら作成されたAMI IDをリポジトリ内の images.yml にAMI IDを追記し、Githubへpushする

マージする度にビルドを実行する必要はないため、 作成したAMIのIDを images.yml に保存し、次回以降はすでにIDの記載がある場合は packer build を実行しないようにしています(そのIDが最新かどうかは確認しない)。

テンプレートを編集したあと、同時にビルドを実行したい場合はID部分を削除することで ビルドを実行するようにします。

今回はRubyでこの処理を自動化する packer build のラッパーを書きました。

実装例

https://github.com/nabeken/circleci-packer-example

ディレクトリ構成

https://github.com/chef/bento 同様に packer ディレクトリ以下にテンプレートとスクリプト を用意しました。

CI用のスクリプトは ci ディレクトリ以下へ用意しています。

circle.yml

特に特別なことはしていませんが、 dependenciesセクションでpackerのバイナリをキャッシュしています。

testは packer/*.json に対して packer validate を実行しています(ci/packer-validate.sh)。

deploymentセクションでは2つのスクリプトを実行しています。

packer-ami.rb

今回のメインです。

packerでAMIをビルドするとログの最後にAMI IDが出力されます。 packer-ami.rbpacker build を実行し、 ビルドが成功した場合のみログからAMI IDを取り出し、images.yml を更新するラッパースクリプトです。

例: 引数にはテンプレート名(ファイル名から .json を取ったもの)を与えて実行します。

$ ./ci/packer-ami.rb ec2-ubuntu-14.04-docker
// ...通常のPackerのログ...
==> Builds finished. The artifacts of successful builds are:
--> amazon-ebs: AMIs were created:
ap-northeast-1: ami-9201f992

$ cat images.yml
---
ec2-ubuntu-14.04-docker: ami-9201f992

push_to_github.sh

images.yml に差分が発生している場合はマージ先へpushするだけのスクリプトです。

作業例

AMIにGoをインストールしてみます。作業前の images.yml は上の状態です。

まず、PRを作ります。 ここではテンプレートでAMI名を修正(重複しているとエラーになる)し、Goをインストールする処理を追加しています。 最後にビルドをトリガーするため、 images.yml からIDを削除しました。

CircleCIでテスト が通ったのを確認し、マージします。

https://circleci.com/gh/nabeken/circleci-packer-example/4 でマージ後のdeploymentとしてビルドの実行と images.ymlの更新が確認できます。

今後の課題とか

Dockerfileを使ってDockerコンテナをビルドしているとあるプロジェクトでは、 タグにGitのコミットIDを使うことで、CIする時にタグがDockerHubに存在しなければ自動的にリビルドし、 pushする運用をしています。こうすることでチーム内で誰かがビルド職人になることを防いでいます。

同じようなことを今回にも適用するとすると、AMIのタグにGitのコミットIDなどから生成した識別子を付与すれば images.yml なしに 運用できそうです。

あと、複数のリージョンを扱う場合ももうすこし工夫が必要だと思われますが、現状は必要ないのでこのままになっています。

アドベントカレンダーのその後、またはGo言語向けAWS SQSライブラリaws-go-sqsを書いた話

2014年の会社のアドベントカレンダーでGoとAWSの現状について書きました。 あれから動きがあったのでアップデートとaws-go-sqsの話を書きます。

goamzとgo-amz

goamzのアップストリームであるCanonicalがリポジトリをlaunchpadからGithubへ移動させたようです。

名前が……。Issue #11を読む限り、 単純なフォーク間のマージはせず、変更を精査し適宜取り込んでいくポリシーのようです。 私もそのほうがよいと思います。

aws-go

小さな修正からXMLでマップを扱うための修正までいくつかPRを出して、すべてマージされました

AWSではXMLでmapを表現することがよく出てきます。GoでXMLはGoの鬼門のひとつで、 修正前はstruct内のmapが正しくunmarshalできませんでした。

mapをunmarshalするには自分でxml.Unmarshalerインターフェースを実装する必要があります。

aws-goの場合はJSONのAPI定義からUnmarshalerを自動生成すればOKですね。 で、どうやって実装すれば…(Goの標準ライブラリみてもわからん…)

@ono_matope 氏のすばらしい記事が大変参考になりました。

最終的に https://github.com/stripe/aws-go/pull/60 になりました。

aws-go-sqs

crowdmob/goamz/sqsにSQS実装があります。 しかし、APIがプリミティブでもなければ使いやすいわけでもなく、ただ単にその時の都合に合せた感じ になっていて辛い感じです。

バッチ処理もちゃんと動くようにしたかったと考えていたところにaws-goが出てきたので、 それをべースにしたライブラリを書きました。

GoDocのExampleにバッチ処理の例を書いたのでここにも載せておきます。

// MessageAttributes
attrs := map[string]interface{}{
    "ATTR1": "STRING!!",
}

// Create messages for batch operation
batchMessages := []queue.BatchMessage{
    queue.BatchMessage{
        Body: "success",
    },
    queue.BatchMessage{
        Body:    "failed",
        Options: []option.SendMessageRequest{option.MessageAttributes(attrs)},
    },
}

err = q.SendMessageBatch(batchMessages...)
if err != nil {
    batchErrors, ok := queue.IsBatchError(err)
    if !ok {
        log.Fatal(err)
    }
    for _, e := range batchErrors {
        if e.SenderFault {
            // Continue if the failure is on the client side.
            log.Print(e)
            continue
        }
        // Retry if the failure is on the server side
        // You can use e.Index to identify the message
        // failedMessage := batchMessages[e.Index]
    }
}

バッチエラーの扱いはもうすこしいい方法があるかもしれません。

工夫した点

ラップするためだけにaws-go-sqs側に劣化structを持たせたくありませんでした。 そこで、

で紹介されている functional options パターンを適用してみました。

つまり、aws-go側のプリミティブなリクエストstructの中身を更新する部分をaws-go-sqsが提供するようにしました。ラップするためだけに便利structを作る必要がなくなりました。

例えば、 https://github.com/nabeken/aws-go-sqs/blob/master/queue/option/option.go#L20 で、 SQSでメッセージを受け取る時に最大メッセージ数やVisibility Timeoutを指定するoptionを用意しています。

ReceiveMessageは functional optionsをvariadicに受け取れるので、以下のように書けます。

messages, err := s.queue.ReceiveMessage(
    option.MaxNumberOfMessages(5),
    option.UseAllAttribute(),
)
if err != nil {
    log.Fatal(err)
}
  • リクエストstruct全体を渡す必要はなくなる
  • 必要なfunctional optionを渡すだけで済む
  • aws-go側に新しいメンバーが追加されてもfunctional optionsを用意すれば動く(aws-go-sqsになければ自分でも書ける)

また、variadicな引数は0個を表現できます。 つまり、

messages, err := s.queue.ReceiveMessage()
if err != nil {
    log.Fatal(err)
}

とも書けます。

もう1つはデバッグしやすくする工夫です。 aws-go自体にまだ不安定な要素があるので、正しいリクエストを送っているか?正しくレスポンスをunmarshalしているか?を検証する必要があります。

aws-go-sqsでは http://motemen.hatenablog.com/entry/2014/12/02/go-loghttp を参考に go-loghttp を デバッグ時のみ仕込み、かつ、リクエスト、レスポンスボディも出力するようにして組込みました。

go-loghttpのおかげでめっちゃデバッグが捗りました。

さいごに

aws-go-sqsの紹介とそこでの工夫について書きました。 GoでWorker書くのすごくマッチしていると思うので、 SQSを使うする機会があればぜひお試しください。PR歓迎です。