続 カッコの付け方

AWSを始めとしたクラウドコンピューティング全般と、唯一神emacsにおける()の付け方についてだらだら書きます

AWS NLB 初心者向け説明 (特にHealthCheck)

続々とELBに機能が追加されていますが、今更ですがNLBについて書きます。 今更NLBの記事を書くことに意味があるのか?と自問しましたが、一応 (初心者向け) と (HealthCheck) という切り口で、なるべく優しく書こうと思います。

NLBについて

先に言っておきたいこととして、NLBははっきり言って玄人好みで初見殺しです。いや、死ぬことは無いかも、そもそも思った通りに動かせない。

先にこの記事よりも遥かにレベルの高いものをエントリしておきます。下記を理解出来るレベルであればこのエントリは用済みです。

kanny.hateblo.jp

dev.classmethod.jp

dev.classmethod.jp

HealthCheckについて

早速本題です。 HealthCheck と ELB(TargetGroup)はセットで切り離せない存在ですが、NLBにSecurityGroupを当てることができません。

今までなら、ELBに X というSGを付けたら、バックエンドのサーバには X からのインバウンド 例えば TCP:80 とかを許可していました。SG設定できないなら、どうすんの? 答えは NLBの属するSubnetのIPレンジに対して許可するとなります。しかも上記の記事をちゃんと読んだ人ならわかるはずですが、NLBはバックエンドとの通信でAZを跨ぎません。よって、NLBが属するAZと同じAZにあるバックエンドに対して、設定すれば良いとなります。ここはAccess.logを見ても確認できます。

NLBの解説

なるべく簡単に

所謂リバースプロキシ型ではない

ALB, ELB (Classic ELB) は所謂リバースプロキシで、ロードバランサで一旦全てのリクエストを受けきって、ロードバランサのSourceIPでバックエンドにリクエストを飛ばします。当然レスポンスが戻って来るので、それを受け止めてリクエスタ(エンドユーザーのブラウザ等)に返します。雑に書くと、結構忙しい。だから暖気とか必要。 もっというと 複雑なこともできる となります。複雑なこととは SSL Terminate, Stickey Sessionなどです。

対して NLB はリバースプロキシではないです。厳密にはDSRとも違う?らしいですが、ロードバランサが一旦リクエストを受ける... の動きがなく、いきなりバックエンドのインスタンスにリクエストが来ます(来たように見えますが正しい)そして、ロードバランサを介さずレスポンスが戻ります。これまた雑に書くと、結構ヒマ、だけど複雑なことは無理

GCEの場合

敢えてGCEと比較します。 GCEが最初にだした、ネットワークロードバランサ(元々は単にローバランサ)はDSR方式です。ウチのLBは暖気要らずやで!どやぁ と言っていたのはこのためです。 これは私の感覚ですが何度も出している言葉ですが、 Google は IP, AWS は DNS という冗長のための仕組みの思想があるように思います。最初にDSRを作ったのもこの設計思想が強く反映されているように思います。しかしDSR型は機能が盛れない、SSL Terminateとかです。そこで HTTP(S) ロードバランサというリバースプロキシ型 + 地域分散型のLBを出しました。

話をHealthCheckに戻します。 GCEはHealthCheck、どうやってるのか? 答えは LB自身がヘルスチェックをせずに特定のIPアドレスからヘルスチェックが飛んでくる、です。この特定のIPとは、プロジェクト(AWSで言うところのアカウント)が異なっても同じです。なので、ヘルスチェックだけを通す・殺すは一応可能です。

NLBのHealthCheckは誰がやんの?

誰がHealthCheckするのかは、他のLBと変わらずNLB自身がやります。 で、SGが指定できない。なので、今のところバックエンド側のinbound SG ルールを絞りたいとなったら、(個人的にはこれはやだなというのも含みますが)

  • VPC の CIDRで妥協 (AWSドキュメントでおすすめとか言ってる)
  • Subnet の CIDRで行く (AZ跨がない考慮を入れる・入れないで2段階)
  • NLBだけを特定のSubnetに押し込めてそのCIDR (Subnetだらけになる、ただでさえ嫌いなのに)
  • 固定かどうか分からないが、アクセスログに出てくるIPだけ許可する。(変わらない保証はない)

今のところ、これぐらいです。

おまけ1. 従来のPrivate Subnetにバックエンドを引っ込めるは出来るのか?

結論から言うとできます。 この構成は CLB,ALBともよくやっていると思います。バックエンドにグローバルIPアドレスを持たせない。DSRは自力でクライアントにデータを届けなくてはならないので一見無理そうですが、できます、できました。 必要なものは

  • NLB は当然 Public Subnetにおく
  • バックエンドは Private Subnet & グローバルIP無し(あっても意味ない)
  • バックエンドが立つ Subnet は NAT-GatewayとかでOutboundが出来る
  • バックエンドにSG inbound TCP:80 0.0.0.0/0 とかを付ける

グローバルIPが無いインスタンスinbound TCP:80 0.0.0.0/0 を付けるんです!気になるのは、NAT経由で戻る点ですが、帯域的にもまあ問題ないかと。そもそもこの構成で本当に行くなら、全AZにNAT-GW必須と言えると思います。理由は?冒頭のエントリ見直してみてください。

おまけ2. ECSと組み合わせ可能?

可能です。ただし、普通にPublic Subnetにおいてしまうと、ポート全開で丸裸にせざるを得ないので、Privateに引っ込めるのはかなりいい手だと思います。NLBもターゲットグループを参照するので、普通にECSと連携できます、Drainingはできませんが、そもそもDSR方式(じゃないけど)なら原理的にはそんなもん必要ないです。

まとめ

  • healthcheck は VPC か Subnet のCIDRぐらいしか無い
  • NLB + Privateに引っ込んだバックエンドの構成は可能
  • ECS + NLB も一応イケる Privateに引っ込めるのがおすすめ

healthcheck はそのうち綺麗な方法がでるかも。一応、healthcheckにしか使わないよ!とUIで言って予防線張っても、ユーザーは NLBのSGに 0.0.0.0/0 とか絶対付けちゃうだろうし、NLBでIP制限出来るとか考えちゃうだろうから、NLBはSG無しとしたのは良い手だと思う。ただ、仕組み的にはhealthcheckだけSGは付けられるはず、だってhealthcheckするのは結局NLBだしローカルIPあるってことは、ENIがあるってこと、SGはインスタンスに付くのではなく、ENIに付くものなので仕組み上は絶対出来るはず。

結局あんまり初心者むけではなくなったような。。。

CloudWatchLogs データを入れてからのおたのしみ(概要編)

リリースから随分日が経っていますが、改めてCloudWatchLogsの良さを書きます。今回はLogs Agentや取り込みの話しは触れません。Logsに取り込んだは良いけど、どうしよう にフォーカスします。

その前にCWLogsの概要

長いのでCWLogsと略記。 CWLogsは文字通り、システムログやアプリケーションログ、Windowsはイベントログも含むを、CloudWatchLogs(AWS側の基版と言い換え可能)に投げます。CloudWatchはカスタムメトリックスといって、ユーザーが任意のメトリックスを送信することが出来ますが、CWLogsはそれのLog版となります。Logですので、メトリックス値(数値)ではなく、ログそのもの=文字列です。

ログをCWLogsに投げる処理は、CWL-Agent等を使います。前述の通りCWLogsに投げるまでの話しはこの記事では触れません。

CWLogsの代替

ここまでの 「Logを何処か一箇所に投げる」という機能だけみれば、fluentdや、同じくAWSkinesis、kafkaなどでも実現出来ます。逆に言うと、「Logを何処か一箇所にに投げる」という目的だけで使ってしまうと、ある程度の量を越えると割高で、kinesisの方がお得です。

これより、CWLogsの利点です。

用語説明

その前に用語説明を

ロググループ 後述のログストリームを束ねるものです。一般的にログの種類でロググループを分けます。
e.g. /var/log/messages や /var/log/http/access_log 毎に1ロググループ

ログストリーム 必ずどこかのロググループに所属します。一般的にはホスト毎に1ストリームとします。そうしない設計も可能ですが、その場合ログの中にホスト名を書くなど考慮が必要です。(どのホストのログか見かけが付けられないので)

The Log group has some Log streams .
A Log stream belong to one Log group.

CWLogsの利点

  1. マネジメントコンソールからログの検索
  2. メトリックスフィルタにより、ログをメトリックス(数値)化、そこからClowdWatch アラート発報
  3. ロググループをサブスクライブして、Lambdaでリアルタイム処理

マネジメントコンソールからログの検索

ロググループ単位で検索可能です。つまり複数のホストのログをまとめて、時間と検索文字列を指定して探すことができます。触ればわかる機能ですので、詳しく説明しませんが。これはこれで便利だと思います。

メトリックスフィルタについて

CWLogsがリリースされたすぐに使えた機能です。先の説明どうりですが、Log中の特定の値を含んだものをカウントし、Logを数値化します。 これを ClowdWatchで閾値を指定、アラーム発報とします。メトリックスフィルタは、ロググループ単位で指定します。

メトリックスフィルタの特徴

Pros
- 1つのロググループに対して、複数のメトリックスフィルタを指定可能
- フィルタルールは結構柔軟。複合条件に似たような記載も可能(後述)
- 検出・通知までプログラミングは一切不要

Cons
- ログ -> 数値化し、その数値に対してアラート発報をするだけなので、ログのアラート発報の原因となったレコード自体 はわからない

簡単に指定出来る点は有利ですが、ログそのものはアラートに含まれないという欠点があります。
つまり、
アラート飛んできたら、どのログ(種類)でアラートを検出したかや、発生の時間帯はわかるが、
どのノードで発生した?/一部なの?全体なの?
ログの中身は?
と言った利用者が本当に欲しいものはそのアラートだけでは得らず、わざわざマネージメントコンソールにログインしてログ検索する必要があります。
それでも最低限の監視、何かが起こっているの検知にはなると思います。

ロググループのサブスクライブの特徴

CWLogs出たてのころはlambdaは存在しなかったため、この機能は後発で実装されました。この機能もロググループ単位で指定出来ます。

Pros
- 生のログを処理出来るので、ログの内容をアラートに込めることが出来る
- フィルタルールだけでなく、lambdaで処理できるので、自由自在

Cons
- lambda必須 = プログラミング必須
- 1つのロググループに対して、1つののサブスクライバしか指定できない

メトリックスフィルタで出来ないことは実現できるのが強力です。が、ロググループ1つに付き1つのサブスクライバとなるので、この点は注意です。

1つの記事にまとめるつもりでしたが、結構な長編になってしまいました。 次回はメトリックスフィルタ編、次にサブスクライブ編と続けます。

AWS EBS SSD (gp2 general purpose) バーストクレジット計算式

「EBS (gp2) のクレジットの回復レートってどんなの?」の質問に即答できなかったので、書いておきます。 結論、数式としては

クレジット 0 -> Fullまでの時間 = 5,400,000(IO) ÷ ベースラインIOPS

となります。

概要と用語

簡潔に

  • EBSは 3000 IOPSまでのバーストクレジットがある
  • 1GBあたり 3 IOPSのベースラインIOPS ただし、 33.333GB ?(つまり 100IOPS以下)の場合でも最低100IOPS
  • バーストの継続時間は、 3000IOPS と、ベースラインのIOPSの差に反比例する 言い換えると ベースラインが 3000IOPSに近ければ長く、遠ければそれだけ短い
  • 1TB超の場合はベースラインが 3000IOPSとなるので、そもそもバーストしない

上記は理解できていると思います。 用語も改めて

  • ベースラインIOPS = バーストなしで利用可能なIOPS上限
  • バーストクレジット = ベースラインを越えるIOPSを利用した時に消費され、ベースライン以下のIOPSしか消費していなければ貯まる

公式ドキュメント

http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html

http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/images/gp2-burst-bucket.png

解説もなにもありません、この絵の通りです。 EBSはOS起動時にIOPSが派手に発生することを見込んで、CPUクレジットとは異なり、いきなり 5,400,000 クレジットから始まります。このクレジットを消費することによって、バーストします。

Volume size (GiB) Baseline performance (IOPS) Maximum burst duration @ 3,000 IOPS (seconds) Seconds to fill empty credit balance
1 100 1862 54,000
100 300 2,000 18,000
214 (Min. size for max. throughput) 642 2,290 8,412
250 750 2,400 7,200
500 1,500 3,600 3,600
750 2,250 7,200 2,400
1,000 3,000 N/A* N/A*
3,334 (Min. size for max. IOPS) 10,000 N/A* N/A*
16,384 (16 TiB, max. volume size) 10,000 N/A* N/A*

上記抜粋表のの 1GB を見て下さい 単純な算数ですが 54,000 * 1000 = 5,400,000 となります。 100GBの場合、1GBの3倍のベースラインですので、54,000 / 3 = 18,000 となっています。

数学的な説明

5,400,000 という数字ですが、単位として示すなら 5,400,000 IO (英語的にはIOsが正しいですが、紛らわしいので IOとします) と読み替えられます。算数の 速さ・時間・距離(地方によっては、は・じ・き と言うらしい)の関係のように、単位変換を式で表すと

まずIOPSは文字通り1秒あたりのIO数ですので IOPS = IO / 1sec とします。そのあと、
Sec = 5,400,000 IO ÷ BaseLine IO / 1 sec 変換して
Sec = 5,400,000 IO × 1 sec / BaseLine IO

5,400,000 の IO と 掛け算の分母である BaseLine の IOが相殺されて
Sec = 5,400,000 / BaseLine
となります。

AmazonLinux や CentOS6 で upstart root以外で実行

もうオワコンである upstart で結構苦戦しました。やりたい事は単純に root以外のユーザーで実行したいということだけ

upstart ?

sysVと systemd の間にあったデーモン管理Tool(というか機構?)です。こいつは sysV とは違って systemdと共存出来ないので、オワコンです。が、残念なことにAmazonLinuxはこれに依存しているサービスが沢山あります。 AmazonLinuxのイケてないところは後述します。

やりたい事

root以外でデーモン起動

setuid / setgid があるじゃん!

残念ながら、 Cent6(RHEL6)をベースにしている AmazonLinuxは古すぎてsetuid/setgid に対応していません。その気になれば差し替えも出来るでしょうが、upstart のような根幹に関わる部分を差し替えるのは難しいはずです。少なくとも再起動は伴うと思いますので、やりません

方法

ubuntu - Running upstart jobs as unprivileged users - Super User

色々ありますが、単に su/sudo でやる方法でも、私の目的のスクリプト(中でデーモンプロセスが動く)は上手く動かなかった。挙動としてはsu コマンドの部分のみプロセスを見ているような動き。 コマンドその物に chmod コマンドで setuidもありですが、今回はシェルスクリプトなので不可能。シェルスクリプトで setuidが使えない理由はセキュリティー上の問題です。結構重要なことですが、詳しくはググって下さい。

結論 setuidgid コマンドを使う

daemon-tools の一部である setuidgid コマンドを使います。古いツールですが、パッチさえ当てればビルド出来ます。

ビルド

kaztr.hatenadiary.com

抜粋

wget http://cr.yp.to/daemontools/daemontools-0.76.tar.gz
tar zxvf daemontools-0.76.tar.gz
admin/daemontools-0.76
wget http://qmail.org/moni.csi.hu/pub/glibc-2.3.1/daemontools-0.76.errno.patch
patch -p1 < daemontools-0.76.errno.patch 
package/compile

使う

upstart のサービス毎の設定ファイルにて

exec setuidgid <ユーザ名> xxxx

AmazonLinuxのイケてないところ

他所のディストリビューションが 全て systemd に行ったのに、未だに sysV upstartなのは、centos6 をベースにしたから。 AmazonLinuxはkernelだけは上げていますが、grub, init系など は centos6 の古典時代のまま。 さらに悪い事に、ECS Agent 等は upstart で動かしているなど、この古い基版の上にAWS謹製のものを作っています。 思うに、今更 死んだ upstart に触りくないし、触ってほしくない。

なんでスパッと切り替えられないかの最大の要因は たぶんAmazonLinuxはローリング・リリースしているから。つまり yum updateだけでメジャーバージョンアップ完了。当然カーネルは無理だが、その他のものはこれで問題ない。しかし、 grub, init系を弄るとなると、結構怖い変更かつ、OS再起動は必須になるはず。だからやらなかったのだとおもうし出来る気がしない。

なので、次のバージョンから 最低でもベースを Cent7 か何かに切り替えた AmazonLinuxと、今のAmazonLinuxの2本立てとなるのが嬉しい。でもなんとなくそうしない気がする、2本立てのリソース裂きを嫌って。

まとめ

upstart root以外ユーザーなら setuidgidが良さげ、

AmazonLinuxへ一言 さっさと systemd化してくれ。。

Google API Client Library 入門

今まで、Cloud Monitoring のカスタムメトリックスを打つために、サンプルを改造する程度の理解しか無かったのですが、ある程度自分の頭で整理出来たので残します。GCP(Google)におけるAPIクライアントライブラリについて、入門からAWSとの違いについて。

Google API Client Library概要

Google(GCP) の各種サービスのAPIを呼び出すためのライブラリです。 ここで言うGCPには、GAE, GCEは勿論、G-Suite(旧Apps) や Mapsも含みます。

主に何をやってくれる?

まず、基本的な部分の説明ですが、APIを呼び出すためには

  1. 認証 (ヘッダなどに込める)
  2. https による RESTfulな操作

が必要です。といいますか、これだけあれば他は不要です。 つまり、AWSだろうがGoogleだろうが、 curl(にかぎらず httpsが喋れるクライアント) があれば、あとは認証情報さえどうにかすれば完了です。 Google API Client Library (API Clientと略) は、シンプルに 上記機能を提供します。特に面倒な 1. 認証 が大きな役目であり、それ以外の機能はとても 薄い実装 といえます。

理論よりさきに実装

書いていて先にコードを書いたほうがいいと思ったので晒します。(文章の流れは無視)

import httplib2
from apiclient import discovery
from oauth2client.client import GoogleCredentials

credentials = GoogleCredentials.get_application_default()
service = discovery.build('monitoring', 'v3', credentials=credentials)
req = service.projects().timeSeries().list(
        name=project_resource,
        filter=filter_strings,
        interval_endTime=end_time.isoformat(),
        interval_startTime=start_time.isoformat(),
        fields="timeSeries.points.value"
        )
ret = req.execute()

project_resource など、service.projects().timeSeries().list() の中の引数は実際のコードの抜粋なので参考程度にしてください。これはmonitoring.projects.timeSeries.listのコールとなります。

https://developers.google.com/apis-explorer/#search/monitoring.projects.timeseries.list/m/monitoring/v3/monitoring.projects.timeSeries.list

コード解説

credentials = GoogleCredentials.get_application_default() 特に触れなくていいいと思います、
service = discovery.build('monitoring', 'v3', credentials=credentials) これも問題無いでしょう、
req = service.projects().timeSeries().list(... がなんとも気持ち悪いコードだと思います(AWS脳には)。なんでわざわざメソッドになるの?ということですが、ここが Client APIの肝です。IPythonなどのREPLを使えばわかりますが、
service.projects(). まで打って 補完しても候補が出てきません。メソッドなので当たり前といえばそうですが、service.projects() を評価して初めて timeSeries リソース(クラス)が存在することが分かりのです。試しに().のチェーンをやめて、1つずつ変数に込めて見て下さい。
つまり、動的にAPIコールのためのクラスを生成しているのです。

また、メソッドの引数(APIパラメータと引数の名前マップ) を確認したいなら

 .__DOC__

を参照すればわかります。IPythonなら ? でも見えます。

Google API Client Library概要 再び

厚い実装・薄い実装

やり玉にAWSを上げると、AWSSDKは非常にぶ厚い実装です。上記で述べた認証は勿論、各種サービス毎に、1API毎に細かくclass-methodを実装し、各サービスを網羅しています。

それに対して GoogleAPI Client は API Clientのソースコード自体に、各サービスが持っている APIを細かく実装するということは行っておらず、classやresourceをダイナミックに取得・生成することにより、ものすごくコンパクトな実装になっています。

  1. 認証についてはどちらも同じ
  2. API操作部分については、 AWSが泥臭く全サービス毎に実装しているのに対して、GoogleAPI clientの中で自動生成している

優劣は一旦置いておいて、
AWS SDKAWS CLI とほぼ同じ領域をカバーする実装であるのに対し、
gcloudコマンド と Google API Client は 全くカバーする範囲が異なります。

gcloud に近い実装としては、 API Client ではなく Google Cloud Client Library というものが対応しています。

github.com

ただしこちらはまだ開発中であるためか、GCE等のサービスには対応していなかったりします。とりあえずAWS SDKに近いのはこちらで、AWS脳のひとはすんなり入れると思います。

薄い実装の利点

APIを作る側(この場合はGoogle)にとってはとてもメリットが大きいです。ここでもAWS SDKと比較しますが、APIの数だけSDKのclass-methodとして実装しているので、APIの仕様ドキュメントは勿論、AWS SDKでもほぼ同じ内容のドキュメントを用意、さらに各種言語毎に作成することになり、かなりのリソースを割くことになります。無論、潜在的なバグも多く含むことになります。
対して、API Clientのような実装の場合、各APIのドキュメントはありますが、API Client側にひとつひとつの対応class-methodに対応したドキュメントを用意する必要がありません。出来ないといったほうが正しいかもしれません。APIs Explorer を使えばOAuth2 で実際に打てるので、四の五の言わずに打って試せ!そしてそれをコードにしろ! というスタンスかもしれません。

また、API本体に仕様変更があっても、API Client側の実装はまず変更不要となるはずです。(使う側は堪ったもんではないですが)

この考え方・実装は、自分がAPIの提供側となった時、とても参考になると思う。

薄い実装の欠点

一言で言うと取っ付きにくい
私個人の感覚ですが、AWS SDKを使ったことがある人で、APIなんて curlさえあれば呼べると分かっていない人(つまりSDKなんて必須でないことをわかっている人以外)、SDKとは丁寧なドキュメントがあるのが正しいと思っている人は、まずそう感じると思います。それをGoogle自身も感じているため Google Cloud Client Library を作っているのだと思います。私も、取っ付きにくいと感じました。

また、根本的な話ですが、GCPは結構頻繁にAPIを弄ります。破壊的な変更は v1,v2 などと旧バージョンを残した上で、エンドポイントが変わる(増える)ことがほとんどですが、パラメータが増える等の対応があった場合、確かに API Client自体は影響受けないですが、API使う側の実装は当然変更となります。ただし、この点は 厚い実装 でも同じですし、厚い実装 と違って新機能のためにわざわざ API Clientのバージョンを上げる必要が無い点は利点に述べた通りです。

まとめ

  • Google API Client Library は主に認証の面倒を見てくれる
  • Google API Client Library の API を呼ぶ部分の実装はかなり 薄い
  • AWS SDK のような 厚い実装が良いなら Google Cloud Client Library を待つ?

自分がAPIを提供する側だったら、この薄い実装 の方がいいなとは思いました。最近は自動生成とかできますけどね。。

Google Cloud SQL 2nd Generation Proxy接続する

8月下旬、 Google Cloud SQL 2nd Generation がGAとなりました。すでに検証はしていますが、今回は Cloud SQL Proxy を中心に書きます。

iga-ninja.hatenablog.com

Cloud SQL Proxy とは?

前回の記事でも記載しましたが、Cloud SQL への接続は全てグローバルIPアドレス となります。これに嫌悪するひとは多いと思いますが、この点はどうすることも出来ません。で、GCEのVMからアクセスする場合、は2つの方法どちらかをとることになります。

  1. 特定IPアドレスからのみアクセスを受け付ける(グローバルIPのみローカルIPは意味ない)
  2. Cloud SQL Proxy を使う

1を採用したでも、GCEの特定VMからのみだときまっていたり、スケールアウトは考える必要がなく、固定台数でのみサービスする場合は、あまり問題はありませんが、スケールアウトするならば新規VM追加の度にIPアドレスを追加していく必要があります。他の方法としては、NATやHAProxy, MaxScale などで、どこかのIPアドレスに束ねてしまう方法ですが、新たなspofを産んでしまうので、あまりよい手とは言えません。

そこで、2の Cloud SQL Proxyです ちなみに、 MySQL Proxy とは全く関係ありません

動作について

Cloud SQL Proxy のやることは、Cloud SQLへの接続(or 認証だけ?)を、一般的な IPアドレス(TCP)経由ではなく、Cloud SQL APIベースで行うものだと推測しています。
だから、IPアドレスにより許可は要らない。
では、プログラム側からどのように参照するかですが、これは UNIX Socket となります。後で手順を書きますが、
Proxy の役目は Cloud SQL への接続を API ベースから UNIX Socket に変換する と言って良いはずです。

構成

上記 Cloud SQL Proxy の役割を理解すると、自ずと構成は Cloud SQL へアクセスする全VMインスタンスで Cloud SQL Proxyを動かす となります。どのみち UNIX Socket なんで、必然的にこうなりますよね。また Docker用の Cloud SQL Proxy もあるようです。これはDockerイメージかな?多分他のコンテナは、 Proxy コンテナへリンクをはって、Proxyコンテナ経由で接続するではなかろうかと

また、各VMインスタンスには、Cloud SQL のスコープ(AWSでいうEC2ロール)を付けておくことを激しく推奨します。スコープ無しでも出来ると思いますが、面倒だと思います。

手順

簡単です。GCSからダウンロードして実行するだけです。多分 Go製かな? https://cloud.google.com/sql/docs/compute-engine-access

wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64
mv cloud_sql_proxy.linux.amd64 cloud_sql_proxy
chmod +x cloud_sql_proxy
sudo mkdir /cloudsql; sudo chmod 777 /cloudsql
sudo ./cloud_sql_proxy -dir=/cloudsql &

この実行により、 /cloudsql/ ディレクトリの下に、各Cloud SQL への接続用 Unix Socketができ、 mysqlでアクセスするなら

mysql -uroot -p -S /cloudsql/[INSTANCE_CONNECTION_NAME]

で接続できます。 [INSTANCE_CONNECTION_NAME] ってなんじゃい?知いますが、大して難しい話ではなくて、-S で指しているのは UNIX Socket の場所です。 Cloud SQL Proxyが INSTAANCE_CONNECTION_NAME(Cloud SQLのね!) で UNIX Socketを作るってだけの話です。

自動起動

ただのアプリーケションなので、自動起動設定が必要です。勝手にデーモン化はするみたいですが。
systemd が使えるなら systemdで
ないなら upstart で設定すればよろし。

HAProxyで RR負荷分散

backend の指定は UNIX Socket でも出来るらしいです。(> 1.5)
動くことは確認出来ました。レプリラグがひどいものを排除する場合は、xinetd などで httpから確認になると思います。

inokara.hateblo.jp

まとめ

Cloud SQL 2nd gen 使うなら Cloud SQL Proxy 一択

メリットが圧倒的に多いので、上記のみとなります。

jsonの戻り値とか、複雑なオブジェクトからスマートにデータを抽出[JMESPath 最強説]

「パイセン、JSONで帰ってくる複雑な構造のオブジェクトをいい感じにするやつしらないっすか?」
「ああ、複雑な構造ね。しらん、専用の関数用意するとか?」
「やっぱ、そっすよねー」
全国津々浦々でこんな会話が繰り広げられてると思います。私も2回ぐらいこのやり取りをしました。この長年の問題に終止符、JMESPath最強じゃね?

JMESPathとは

以前、一回書いてます iga-ninja.hatenablog.com

この時私は勘違いしておりました、JMESPathはJSONだけを捌くライブラリではなく、JSONから素直にコンバート出来る各言語のオブジェクト を捌くことが出来ます。pythonで言うなら list と dictで出来てるものなら JMESPathでクエリ出来ます。# setとかは知りません。

似たようなのに jq コマンドがありますが、JMESPathのほうがいいです、多分。jq 詳しく知らないので、機能・性能面では評価出来ないですが、どう考えても動作環境の広さから言えばこっちが上です。能書きは最後の方に書きます。

JMESPathでこんなことができる!

前回も書きましたが

JMESPath Tutorial — JMESPath

ここ見てもらえばだいたい分かります。しかも試せます。そして私も勘違いしてましたが、このXPathぽい表記が JSON文字列ではなく、各言語のオブジェクト(JSONに変換出来る)に対して通用します。READMEの抜粋ですがこれは、 pythonのdictに対して問い合わせしています。

>>> import jmespath
>>> path = jmespath.search('foo.bar', {'foo': {'bar': 'baz'}})
'baz'

チュートリアルは、ナナメ読みせず、きちんと読んだほうがいいです。

もっと難しい問い合わせ

もっと複雑な問い合わせについて、ちょっと解説します。
http://jmespath.org/examples.html#working-with-nested-data

ここ見てください。awsの ec2_describe_instances の戻りっぽいJSONですが

{
  "reservations": [
    {
      "instances": [
        {"type": "small",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "large",
         "state": {"name": "stopped"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }, {
      "instances": [
        {"type": "medium",
         "state": {"name": "terminated"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "xlarge",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["DB"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }
  ]
}

reservations[].instances[].[tags[?Key=='Name'].Values[] | [0], type, state.name]

[
  [
    "Web",
    "small",
    "running"
  ],
  [
    "Web",
    "large",
    "stopped"
  ],
  [
    "Web",
    "medium",
    "terminated"
  ],
  [
    "DB",
    "xlarge",
    "running"
  ]
]

が帰ってきます。最後の [tags[?Key==`Name`].Values[] | [0], type, state.name] が特にヤヴァイ。select name, type, state from .. みたいに、欲しい項目を複数抜き出してます。プロジェクションってやつです。昔、情報処理とかで勉強した 射影 ですな。 ちょっと気おつけて欲しいのは このサンプルでは ?Key=='Name' と シングルクオートでNameを囲っているが、pythonとかで使うときは

?Key==`Name`

バッククオートで区切ること

pythonから使ってみる

import jmespath

dt = {
  "reservations": [
    {
      "instances": [
        {"type": "small",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "large",
         "state": {"name": "stopped"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }, {
      "instances": [
        {"type": "medium",
         "state": {"name": "terminated"},
         "tags": [{"Key": "Name",
                   "Values": ["Web"]},
                  {"Key": "version",
                   "Values": ["1"]}]},
        {"type": "xlarge",
         "state": {"name": "running"},
         "tags": [{"Key": "Name",
                   "Values": ["DB"]},
                  {"Key": "version",
                   "Values": ["1"]}]}
      ]
    }
  ]
}


jmespath.search(
    'reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name]',
    dt
)

>>[['Web', 'small', 'running'],
 ['Web', 'large', 'stopped'],
 ['Web', 'medium', 'terminated'],
 ['DB', 'xlarge', 'running']]

pythonでのサンプルの解説

まず、

reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name]

について解説します。(後でアレンジも試します)

最初の部分

reservations[].instances[]

は特に問題無いと思います。実行したらこんな感じ

In [33]: jmespath.search('reservations[].instances[]', dt)
Out[33]:
[{'state': {'name': 'running'},
  'tags': [{'Key': 'Name', 'Values': ['Web']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'small'},
 {'state': {'name': 'stopped'},
  'tags': [{'Key': 'Name', 'Values': ['Web']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'large'},
 {'state': {'name': 'terminated'},
  'tags': [{'Key': 'Name', 'Values': ['Web']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'medium'},
 {'state': {'name': 'running'},
  'tags': [{'Key': 'Name', 'Values': ['DB']},
   {'Key': 'version', 'Values': ['1']}],
  'type': 'xlarge'}]

次の構文ですが、最初にこのクエリのやりたいことは、 instances配下にある、

  • tagsのNameタグのValue(配列なので Key=Nameのもの)
  • type
  • stateのname(dict なので、pythonで書くと state['name']の値)

だけを抜き出したいということです。チュートリアルを終わった段階で、いきなりこのクエリを書ける人はまず、いないと思います。
問題は、複数の要素を抜き出したいが、すべての要素がフラットに並んでいないという点です。フラットに並んでいるとは、わかりやすい例だとこんな感じ

{
  "people": [
    {"first": "James", "last": "d"},
    {"first": "Jacob", "last": "e"},
    {"first": "Jayden", "last": "f"},
  ]
}

こういう例だと、そもそもJMESPath要らなくね? という話ですが、 (people 以外にもう一個ゴミ要素があるとかなら意味ある) people[*].[first,last] とでもすれば複数のキーの値を抜くことは出来ます。

では、まず失敗例から。tagsの下の Key=name のうちの Values (配列) に着目し、これを狙い打ちます。

In [39]: jmespath.search('reservations[].instances[].tags[?Key==`Name`].Values[]', dt)
Out[39]: [['Web'], ['Web'], ['Web'], ['DB']]

これだと tags.Key='Name'のValuesは引けますが、 reservations[].instances[].tags まで掘り下げているので、どうやっても typeやstateは出てきません。

ちなみに.Valuesのケツの[] は flattenです。無いとこうなります。

In [40]: jmespath.search('reservations[].instances[].tags[?Key==`Name`].Values', dt)
Out[40]: [[['Web']], [['Web']], [['Web']], [['DB']]]

じゃあ、どうすればいいか。reservations[].instances[].tags まで掘り下げないで reservations[].instances[...] でマルチセレクト処理すれば良いということです! お手本の
[tags[?Key=='Name'].Values[] | [0], type, state.name] に着目、 これは

  • tags は tags[?Key==`Name`].Values[] | [0]
  • type は 素直に type
  • state は state.name

抜いています。更に解説を進めますと。
tags[?Key==`Name`].Values[] | [0] のパイプよりも前はもう説明いらないとおもいますが、なんでわざわざ | [0] としているかに注目。Valuesはlist型なので、 ['Web','01'] みたいな値が帰ってくることもありえます、その場合先頭を抜くという意味になります。(これはサンプルなので、参考程度に) あとの type, stateは解説不要ですね!

ちょっとアレンジ

In [46]: jmespath.search('reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name ]', dt)
Out[46]:
[['Web', 'small', 'running'],
 ['Web', 'large', 'stopped'],
 ['Web', 'medium', 'terminated'],
 ['DB', 'xlarge', 'running']]

これでも十分なんですが、どうせなら dict の listがかえってくるようにしたいです。どうやればよいのか、、って簡単ですよね!今までの解説を読んでたら、linstance[]の下からマルチセレクトしているだけなので、ここで{}で囲って適当にkeyをつければ終わりです。

In [48]: jmespath.search('reservations[].instances[].{_name: tags[?Key==`Name`].Values[] | [0], _type: type, _state: state.name }', dt)
Out[48]:
[{'_name': 'Web', '_state': 'running', '_type': 'small'},
 {'_name': 'Web', '_state': 'stopped', '_type': 'large'},
 {'_name': 'Web', '_state': 'terminated', '_type': 'medium'},
 {'_name': 'DB', '_state': 'running', '_type': 'xlarge'}]

このサンプルの落とし穴

instances[] からの マルチセレクトであるため、 Nameタグが存在しない場合、除外する という動きになっていません。試しにデータの一部の tag.Key Nameを NNameとかにいじってみてください。多分こうなるはずです。

In [54]: dt2
Out[54]:
{'reservations': [{'instances': [{'state': {'name': 'running'},
     'tags': [{'Key': 'Name', 'Values': ['Web']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'small'},
    {'state': {'name': 'stopped'},
     'tags': [{'Key': 'Name', 'Values': ['Web']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'large'}]},
  {'instances': [{'state': {'name': 'terminated'},
     'tags': [{'Key': 'Name', 'Values': ['Web']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'medium'},
    {'state': {'name': 'running'},
     'tags': [{'Key': 'NName', 'Values': ['DB']},
      {'Key': 'version', 'Values': ['1']}],
     'type': 'xlarge'}]}]}

In [55]: jmespath.search('reservations[].instances[].[tags[?Key==`Name`].Values[] | [0], type, state.name ]', dt2)Out[55]:
[['Web', 'small', 'running'],
 ['Web', 'large', 'stopped'],
 ['Web', 'medium', 'terminated'],
 [None, 'xlarge', 'running']]

最後がNone で抜けてきてます。これを避けるには、更にパイプを使って listの最初の要素がNoneで無いものを抽出するようにするか、これぐらいならpython側でやっちゃうかの2択かなと思います。ココらへんを選べるのがプログラム言語から使える利点ですね。

能書き

対応言語

github.com

python, ruby, php, js, go, lua LWの主要言語ほぼ全部やないか! 言語オタクもここまで来たら凄いわ! しかも jp というコマンドまであるし、goでね。去年の記事では rubyは対応してなかったけど、対応してる。

あとは c/c++, C#, java, swift とかの heavy系
Rust, Elixir とかの 変態関数型言語

ここまで来たら言語オタクから言語変態紳士へクラスチェンジ。

動作環境の優位性

AWSを使っている人なら、少なくとも pythonのJMESPathは絶対に入っているといえる。なぜなら aws-cliが依存しているから。また、Rubist曰くもAWS-SDK2が依存しているらしく、AWSに携わっている人なら、まずJMESPathは入ってる、はず。

ただ、非AWSな人にとっても、各言語でネイティブ対応できていて、バイナリ依存とかも無いので、インストール自体はさくっと簡単に出来る、はず。

なんでこんなにAWSなの?と思ったけど、作者がAWSの中の人?(オーガには入ってる)なので、AWSとは切っても切れない。けどAWS関係なしにこのライブラリはイケてる。

jqとの比較

jq のほうが歴史は古いと思いますが、JMESPathは各言語におそらく言語ネイティブ(俗にゆう pure-javaみたいな)で対応している点が優れていると思います。

あと、作者が一人であるという点も、私はプラスに考えています。なぜならこのJMESPathの仕様自体は、彼がこの世で一番知っているのだから。もちろんバグもどこかにあるでしょうが、JMESPathの仕様に対する実装レベルが(どの言語でも)一定であると想像します。

まとめ & 上達のコツ

  • AWS人ならJMESPath 最高、嫌でも入ってるし!
  • プログラマなら jq より jp のほうが明るい未来(と思う)
  • 学習は、とにかく叩いて試してみる。遅くてもチュートリアルは完全に理解するほうが、結果お得
  • 正規表現と同じ、1手ですべてを解決しようとしない。ちょっとずつ書き足す
  • パイプは素直にパイプ、わからなければパイプがなんとかしてくれる!
  • 今回触れなかったが、UDFも作れるらしい!

個人的な感想ですが、今更 jq を使う理由が見つからないです。過去の遺産(資産)とか関係なしのはなしですよ、もちろん。