続 カッコの付け方

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

Google Data Studio から AWS athena につなぐ

最近流行りのワンイシューってやつに見せかけてちょっと寄り道します。タイトルの通りですが、Data Studio の知名度や、GASについて色々気づいた点も触れます。実は応用の効く内容です。

f:id:iga-ninja:20190826010217p:plain

Google Data Studio ってなによ?

Googleが提供しているBIツールです。無料です。 正確には今は Google Data Portal というらしいですが、ググラビリティーが低いので、Data Studio で書きます。

メジャーなパブリッククラウドはたいていBIツールを持っていますが、無料ということも有りはっきり言ってかなりプアです。ただし、無料の割には結構やれるので、私の周りにもファンはいます。

中立的な評価とは言えないかもしれませんが、だいたい下記に書いてあることに賛同します。

https://www.holistics.io/blog/google-data-studio-pricing-and-in-depth-reviews/

BIツールを入れるとなったら、誰が編集可能で、誰が閲覧だけでとか、ユーザーレベルごとにライセンス料金が異なるとか、結構な面倒事がありますが、無料でそれこそGoogle Docsとかと同じ感覚で使えるので敷居は低いです。私はBI入門レベルなので、偉そうなことは言えませんが、これで感じをつかむのはいいかもしれません。

今回やること

  1. Data Studio のデータコネクタを自作する
  2. GASのソースをCLIで更新する
  3. Data Studio でAthenaのデータを参照する(ようは作ったコネクタを使う)

その前に種明かし
今回のネタはGitHubでたまたま見つけたものを使うだけです。他人の褌で遊ぶだけです。わかってらっしゃる人は下のリンクを辿って貰えばOKで、このエントリの内容を読むのは時間の無駄です。神に感謝しましょう!

github.com

データコネクタの自作

随分と敷居の高いことのように見えますが、結構かんたんです。 すごく丁寧に書いてくれているので、下記を読みましょう。

https://codelabs.developers.google.com/codelabs/community-connectors

GASの組み方(仕様、インターフェース)と、参照のしかたが書いてあります。

GASのソースをCLIで更新する

clasp という謹製ツールができています!やったぜ! 使い方はググってください。勘で使えるレベルだと思いますが、Web上のコードとローカルのコードが上書きされるだとかディレクトリの配置だとかは調べたほうがいいかも。ローカル上のファイル拡張子は.js で問題なし、このツールがPush時にgsに変換されます。

とりあえず手順だけ書きます。意味とかは自分で考えてください。

# とりあえず git clone とかで上記 datastudio-aws-athena-connector のコードを取ってくる
clasp login #初回ログイン
clasp create # standalone えらぶ エラーでコケたらメッセージ読む
vim .clasp.json

{"scriptId":"xxxxxxxxxxxxxx",
"rootDir": "src"}

clasp push
clasp deploy #出てくるIDぽいのメモっとく

Data Studio でAthenaのデータを参照

自作のコネクタを指すときに、clasp deploy の結果のIDを貼り付けます。 f:id:iga-ninja:20190826005906p:plain

あとはよくご存知のCredentialsとかを入れて、データ参照できれば完了 f:id:iga-ninja:20190826005929p:plain

脇道

aws.js(gs) について

今回の収穫の一番大きいのはこれです。できるのはわかっていたけど、こんなに小さくaws-api叩くのに最低限だけのセットを実装されていてこれはナイス。以前aws-sdk for javascriptを動かそうとしていたけど、XHRの実装がGASには無いので、これのラッパーかぁ、きついなと思って諦めた記憶がある。
まあ、そうだよね、これだけあれば動くよね。あとはテメエで aws api をググってなんとかしろやってことで。

clasp便利

これはいい!早いうちに決定版が出ないものかと思っていたけどようやくですな。helpとかも見てほしいけど、logs とか使えそう。
サーバーレスはデプロイ方法がまだ定まってないとずっとずっと言われているけど、少なくとも私には 大体こんな感じ で通用し、違和感なくコマンドが打てる。書いててただのポエム感もするが、世の中の大体こんな感じから大きく乖離していないと思うし、世の中の求めるものが少しずつ定まってきたと感じる。

data studio の進化

進化してると思います。出始めの印象はパブリッククラウド各社がBI出したんでとりあえずウチもってところだろぐらいでしたが、ちゃんと進化しててびっくりした。2テーブルをデータソースでくっつけたりとか昔はできなかった記憶。機能もバリバリではないものの、少しずつ追加されていて、有料版も見据えているのかもしれない。ちゃんとそだてる気、あるんだ。。と実感。

CLI向けhttpsキャプチャーを使う

全部httpsじゃないとだめな時代になりました。http通信の中身を見るだけならproxyで抜けばよかったですが、httpsの場合はそうも行きません。mitm(man-in-the-middle)をやって、httpsの暗号をproxyでほどかないといけません。
この手の情報がほしい人は限られていると思うので、細かい解説はすっ飛ばします。わかってらっしゃる方向け

f:id:iga-ninja:20190630015954p:plain

tl;dr

  • mitmproxyというcli向けproxyがある、cliなら多分これ一択
  • ca証明書指すのが面倒 がんばって調べて指す

元も子もないですが、もしあなたがhttps通信を行うプログラムの作者自体なら、log-level=debug,trace とかにしたときにヘッダを含めて全部標準出力などに垂れ流すようにしたほうが100倍楽です。そもそも世の中のhttp(s)クライアントはlog-levelを上げればverboseになるようにできてます。

対してこのmitmproxyは万能・汎用的である点は優れますが、ともかくca証明書がだるい。

mitmproxyの紹介

mitmproxy - an interactive HTTPS proxy

python製です。pipでも入ります。引数なしで起動したら 8080 listenして、 ~/.mitmproxy 配下にCA証明書とかを吐きます。あとは 環境変数 https_proxy と、このCA証明書を信頼してやればいいだけ。
CLIと書きましたが、mitmproxy自体はCLIですが、ローカルPCで動かして、GUIWebブラウザからプロキシ指してCA証明書をインポートすればブラウザ相手でも通信を抜けます。ブラウザ向けのやり方は公式を参照してください。
ブラウザ相手という視点なら、 macなら Charles, winなら Fiddlerとかとおんなじです。こいつらでCLIhttps通信を抜くことももちろんできます。が、Linux上でサクッとやりたいのでmitmproxyが良い。

他の競合はJMeterなどもこの機能があります、試験用のシナリオを楽に作成するときに使います。tcpdumpもサーバ側の秘密鍵が自分で手に入るなら復号化できるのでいけますが、外部サービスやらAPIの呼び出しはそんなの無理なんで、そういうケースを想定してます。

使い方は簡単です。 mitmproxy とコマンド打つだけ。あとは勘でいける。

各種ランタイムの証明書の通し方

こっからが本題です。ブラウザと違ってCLIhttps-clientは証明書の指し方がバラバラです。備忘録のように書き残し&書き足しておきます。

Java

javaはproxy自体の指し方がちょっと特殊です。
-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=8080
と、オプションを渡せるならそれで、無理なら環境変数 _JAVA_OPTIONS に足してやればOK

問題は証明書です keytool という java 用のコマンドを使って追加します。

sudo $JAVA_HOME/bin/keytool -import -trustcacerts  \
-file /path/to/mitmproxy-ca-cert.pem                 \
-alias mitmproxycert                                 \
-keystore $JAVA_HOME/jre/lib/security/cacerts

$JAVA_HOME/jre/lib/security/cacerts 以外の場所に作るなら java実行時に java vm のオプションである -D<hoge>で指定ができるはず。 $JAVA_HOME/jre/lib/security/cacerts には パスフレーズがついていて、デフォが changeitらしい。

blog.packagecloud.io

のパクリ

ruby

私の経験値が低いため、イレギュラーを知らないせいか比較的散らかってないと思う。 proxy自体は https_proxy環境変数でほとんど通るはず。 証明書は ruby -ropenssl -e "p OpenSSL::X509::DEFAULT_CERT_FILE" で調べて、そこに追記しちゃうのが多分てっとり早い。大体システムのデフォルトだと思う。

python

証明書はイレギュラーだらけでやばい、proxy自体は環境変数でまあいける。 package次第だが、たしか requests とかは独自で ca-certを持っていて、バージョン次第では証明書だけで別のpackageになっている。

dev.classmethod.jp

使うライブラリ次第で使っている証明書も異なる。頑張って調べてねとしか言えない。

nodejs

proxy自体は環境変数でOK。 証明書はある意味一番特殊で。システムのca-certは一切参照しておらず。なんとハードコードで nodeのバイナリ本体に内包されている。システムの証明書を使う場合は

--use-bundled-ca, --use-openssl-ca

が使え、今回のような専用の証明書を指すなら

NODE_EXTRA_CA_CERTS=file

https://nodejs.org/api/cli.html#cli_node_extra_ca_certs_file

という環境変数で行けるらしいです。対応バージョンは v7.3.0からとなっていますが、旧バージョンにもLTSならばもある程度バックポートされているらしい(未確認)

まとめ

このライブラリがどんな動きしてるかわからない?となったとき

  • ログレベルを上げて実行する (-v とか -l debug とか DEBUG=1 とか)
  • それでトレースできないなら、プログラム意地れるなら loggerにだすなり echoするなりする
  • それでもだめなら、無理なら サーバの秘密鍵取れるなら tcpdumpとか
  • それも無理なら mitmする

見た目はいいんですよ。機能も多機能。だけど使えるようになるまでが面倒。

EC2 DNS名前解決制限をECSでも回避する方法

非常にググりにくい事柄なんですが、なるべく浮世離れしないタイトルとして書きます。

f:id:iga-ninja:20190415012916j:plain

EC2(正確にはENI)からの名前解決、つまりリゾルバとしてのOutboundには、全て制限がかかっています。1024パケット/秒です。

VPC での DNS の使用 - Amazon Virtual Private Cloud

ECS DNS というキーワードで探すと、ほぼ間違いなくService Discoveryがうじゃうじゃ出てきますが、これはリゾルバ・キャッシュサーバの話です。 これをEC2で回避する方法はAWSが書いてくれています。

Avoid DNS Resolution Failures with EC2 Linux

似たようなことをECSで、なるべくエレガントにやります。わざわざこう書くのは、ちょっと泥臭いという意味です。

どういうとき問題になるか

そもそもこのポストを見ているひとは、今現に困っているはず。ですが意識合わせ。

  • LinuxDNSローカルキャッシュしていない
  • 1台のEC2でかなりのトラフィックをさばいている
  • 現に名前解決しましたとかエラーが出てる
  • 負荷試験とかやってて、性能が頭打ちしてる

ECS & EC2で、詰め込むコンテナの数もかなり多い場合に起こりやすい現象です。

どうすればよいか

上記のAWSのドキュメントを読めばわかりますが、ローカルキャッシュを持つようにして、とにかくEC2(ENI)からOutboud:53 を抑制するしか手はありません。
VPC内のリゾルバに対する問い合わせの回数が問題ではないのです、1歩でも53番ポートへ出ようとする通信全てが対象なので、自前でリゾルバを1台用意しても無駄です。
コンテナの場合は、コンテナ内部からの名前解決をケアすることになります。つまりはコンテナ内の /etc/resolv.conf を何らかの方法で書き換える必要があります。

  • どうやって変えるのか
  • 何(IPアドレス)に変えるのか

これが主題。

最終構成

DNSキャッシュ機能

まずは何らかの方法で、DNSキャッシュサーバをコンテナインスタンス内に立てます。他にも選択肢がありそうですが、メジャーどころで2択

  • unbound
  • dnsmasq

どちらを選ぶかは好きな方で良いのですが、dnsmasqのほうがおそらくフットプリントが小さいし、必要なことにマッチしているので良いと思います。ただし訳があってunboundにも触れます。
選んだキャッシュサーバを各EC2に一つづつ立ち上げます。お好きな方法で良いです、EC2に直接インストールするか、ECSならDAEMONモードのServiceとして、一家に一台を実現するか、ここではDAEMONで動かします。

コンテナからの参照

各EC2に立てるDNSキャッシュサーバーをプライマリ、VPCごとについてるリゾルバをセカンダリにします。
コンテナの中のresolv.conf をいじれば良いとなります。ホストのではなく。

コンテナ内のresolv.conf

ECS & EC2なら、(おそらくほとんどのケースで)ホストのresolv.confがコピーされます。
仮にホストで直接キャッシュサーバーを動かしたとして、

と書いた場合ホスト自身は名前解決問題なしですが、コンテナはから見ると127.0.0.1 はコンテナ自分自身のloを指すことになり、当然 port:53 でサービスしていないのでNGです、上記例の設定ならは、コンテナ内からの名前解決は、必ず1手目でタイムアウトしてセカンダリであるvpcのリゾルバに行くので、この設定は意味なしです。

じゃあ、ホストのローカルIPを挿せばうまく行くのかと言ったらうまく行かないです。やればわかりますが、ホストのローカルIPで問い合わせたが、レスポンスを返したIPが違うって怒られます。

なので、コンテナから見たゲートウェイ (bridgeの場合) 172.17.0.1 固定を指します。

IP固定してOKなん?

他所では場合によっては固定じゃないっていっぱい書いてありますが、ここでは大丈夫です。
説明すると長いので、断言できることだけ。少なくともECSのブリッジモードは、bridgeという名前のついたデフォルトのdockerネットワークにすべてのコンテナが入ります。よって、Gateway172.17.0.1 で確定です。Docker自体の仕様がかわったり、ECSのブリッジモードでネットワーク名をユーザーが新設できるようになるとかならない限り、ここは崩れません。
当たり前ですが、リゾルバにホスト名は書けないので、同タスク内のコンテナ同士の名前解決をLink等でできるようにしてもIPを書かねばならないので意味ないです。

ECS/EC2 での実装

キャッシュサーバ(コンテナ)の構築

サーバというものの、全EC2に立つのでそのつもりで。とりあえずecs-cliで書いてみました。 書き忘れましたが、キャッシュサーバ(コンテナ)と、メインのアプリコンテナは別タスクとなります。
docker-compose.yml

version: '3'

services:
  dnsmasq:
    image: himaoka/dnsmasq:latest
    ports:
      - 53:53/tcp
      - 53:53/udp
    cap_add:
      - NET_ADMIN

Dockerイメージの中身はGithubで見てください。注目は cap_add: NET_ADMIN です。コイツがないとコケます。でこれがあるからFargateやawsvpcモードでは使えません。更に補足としてこの cap_add はManagementConsleからは見えません(JSONベタ書きはいけるかもしれない)。ECS-CLIですら機能のすべてを網羅していませんが、タスク定義作成にブラウザポチポチはすぐに卒業したほうがいい、terraform や aws-cli などをすぐに使えるようにしてください。

このコンテナにより、コンテナインスタンスの port:53 でDNSリクエストに応答します。

ecs-params.yml

version: 1
task_definition:
  services:
    dnsmasq:
      mem_limit: 128M
      mem_reservation: 128M

多分こんなにメモリはいらない。 これで定義したタスク定義を、サービスとして設定、DAEMONで動かします。
まあ、ここでコンテナにこだわる理由はないです。よくわからんならEC2で素で動かしても全く問題なし。

アプリコンテナ

とりあえず何でもいいんですが、手っ取り早く動かして、docker exec で動いているコンテナに入り込んで確認します。なんでただ死なないコンテナであれば何でもOK

docker-compose.yml

version: '3'

services:
  nginx:
    image: nginx
    dns:
      - 172.17.0.1
      - <VPCのリゾルバのIP>

この dns はこれまた awsvpcモードでは使えません 理由はわかりません。できても良さそうなもんだけど。こっちはブラウザポチポチでも一応設定可能です。
確認方法は exec で入り込んで dig コマンドとか突っ込んで確認してください。

もう一つの方法・コンテナインスタンス側が譲歩する

基本コンテナインスタンスの resolv.confをコピーするという動きがるので、コンテナインスタンス内部側のリクエストは犠牲にして、コンテナ内部のリクエストだけをちゃんとしてやることもできます。単純にコンテナインスタンスの /etc/resolv.conf のプライマリを 172.17.0.1 にするだけです。
コンテナインスタンスからの名前解決はプライマリはすべて失敗、セカンダリとしてVPCのリゾルバを挿せば、常に1手損ですが通ることは通ります。
タスク定義を直さないでインフラの豪腕でなぎ倒すことは一応可能です。エレガント?な方法は下記です。

Amazon EC2 Static DNS Ubuntu Debian

Fargateならどうすんの?

まず、必要となるか否かですが、なんとも言えないです。Fargateということは一つのタスク全部で1024という制限になるので、上限に引っかかる可能性は減ります。
じゃあ、絶対に大丈夫か?と言われると場合によるかもしれない。1タスクに10コンテナとかあり、それらが各々がガンガン名前を引くならば絶対にOKとは言い切れない。

と私は思うので、一応試しました。ただし結論から書くと、きれいな方法は取れないです。

  • EC2という概念がなくなるので、メインアプリのコンテナと、DNSキャッシュコンテナは同タスクとする必要がある
  • DNSキャッシュサーバのListenは、メインアプリのコンテナからみても 127.0.0.1:53 となるので、nameserverは 127.0.0.1 とすればよい

対して阻害要因です

  • dnsmasqのcap_add: NET_ADMINはFargateでは使えない
  • メインアプリコンテナのDNSとして、タスク定義としてdnsを指定することはできない (awsvpcモードの制約)

Fargateの結論、一応できたけど、、

dnsmasqのcap_addはどうにもならないので、unboundにします。
dnsも無理なんで、Dockerの ENTRYPOINTで/env/resolv.confを無理やり書き直します。
で、一応動きました。いい方法とは言えないので、解説はしませんがこの概要だけ書いて終わります。

unboundの注意点

unboundでやるときの注意点ですが、dnsmasqとこちらは違って、ルートDNSへのヒントを持っているので、権威サーバのレコードであれば自力のみで解決できます。対してdnsmasqはキャッシュはするものの、自力で権威サーバを調べ上げることはできない。
じゃあ、なんの問題もなくね?
いやいや、 問題大有りですわ。何も設定してないと、Route53の Private Hosted Zoneのレコードが全部死にます。

先に答えを書くと、unboundで行くならば、必ず すべてのレコードをVPCのリゾルバにForwardしてください。

厄介な Private Hosted Zone

個人的には嫌いな機能です。Private
Privateは作るときに必ずアサインするVPCを指定しますが、これは VPCのリゾルバに権威サーバに飛ばさずに Private Hosted Zone に横流しするルートを作ります。もちろん特定のレコードだけですが、これがあるのですべてのレコードをVPCのリゾルバにForwardするのがインフラ屋としての正しい解です。
Privateのレコードがこれ以上増えないってわかっているならば、個別ドメインに対してのForward設定してもいいです、がECSやってるならService Discoveryの重要性もわかっているはずです。でもってService Discovery の実態は Private Hosted Zone です。特定ゾーンだけForwardしてると、Service Disvoveryをある日いきなり使い始めたら終わりです。

おまけ unboundで起動時警告が出る

cap_add: SYS_RESOURCE でいいはず、もしくはulimitをタスク定義側で指定。  

おまけ2 ubuntu の場合

あんまり調べてないですが、ubuntu ユーザーなんで。ubuntu はデフォルトで 127.0.0.53:53 でListenしています。ホストの resolv.conf も 127.0.0.53 を参照していますので、そのまんまやるとホストの resolv.conf をコピーして不味そうですが、うまく行っています(書き換わる)。理由は調べてないですが、昔はちょっと問題があったらしい。今回のテストをローカルPCでやるときにちょっと手こずった。

Healthchecksで定期実行ジョブの着火もれを検出する

日次でも月次でも、まず殆どのシステムで何かしらのバッチジョブというものを動かしていると思いますが、 そのバッチが確かに実行された ことを保証・監視することは結構難しいです。ジョブが起動しましたのタイミングでログを書き、終わってもログを書くとすれば、ジョブが成功したか失敗したかはわかりますが、そもそもジョブが着火すらしかなったはわかりません。*1
例えば、「このジョブが終わるとここにファイルが出ているはずだ!このDBのレコードが更新されているはずだ!」などと、そのジョブ固有の結果を観測して判定することはできますが、汎用的な手法ではありません。
私は詳しくはないですが、商用のジョブ管理ツールはそれぐらいは見てくれるのかもしれませんが、もっと簡単にこれを実現できます、しかも今風に。

Healthcheckesとは

f:id:iga-ninja:20190323151509p:plain

ググりにくく、ネーミングセンスとしては最悪ですが、上記に挙げたような機能を実現するSaaSはもう結構あります。が、私が 2017年頃に一通り評価したところ、healthchecks.io に勝るものはありませんでした。

healthchecks.io

SaaSとして提供していて、価格もかなり安いのですが、OSSでも公開されており、IaaSなどで動かすことも可能です。今回はOSS版を動かします。

OSS版の動かし方

Healthchecksを選んだ理由はOSS版があることと、Python製であったことも一員です。ただデプロイ方法は結構複雑で、Djangoをそこそこわかってないと難しいです。じゃあSaaSを使えばいいんですが。。
実はこれを見つけた当初、こいつをDocker化するのは手頃でそこそこ複雑で丁度いいと思い、いつかやると心に決めていましたが、2019年の今、世界のすごく優秀な人がCoolなDocker Imageを作ってくれていました。今回はここは深く触れませんが、このイメージに気づくまで自分なりに結構な時間、試行錯誤しましたがここで得たものは大きかった。

必要なもの

ローカル環境(クライアントPC)のDocker

とりあえず入れてください。docker-compose も入れておくこと。ちなみに私のようにLinuxをDesktopとして使っている人は知っていると思いますが念の為 docker-composeはdockerパッケージに同梱じゃないです。別途いれてください。

Outbound Port 25が許可されているネットワーク

Healthchecksを使うには、メールアドレスが必要です。メールでユーザー登録するというアクションが、OSS版でも必要になります。このときに、受信できるメールアドレスはないという人は居ないと思いますが、メールを送信する機能が必要となり、OP25が遮断されているAzureやGCPでは動かせません(もちろん回避方法はあります)、AWSはOP25が許可されていますが、理由は割愛しますがおすすめしません。 OP25を切っているクライアントPCはそうそう無いと思いますが、一応。まず普段お使いの自宅用PCとネット回線を使えばまず問題ないはず

gmailアカウント

インフラの説明がメインじゃないので、SMTPサーバとしては gmailを使います。G Suiteじゃなくて無料の方でOKです。わかる人はアレンジしてください。

構築手順

Gmail のアプリケーションパスワードを取得する

account.google.comへアクセスして取得します。2段階認証が設定されていないと取得できません。ここでアプリケーションパスワードを取得

f:id:iga-ninja:20190323120025p:plain

docker pull する

飛ばしてもOKですが、Dockerイメージが玉石混在なので、イケてるやつをポイントします。

https://hub.docker.com/r/linuxserver/healthchecks

github.com

DockerHubでDownload数最大のやつはあきまへん。

docker-compose.ymlを書いて動かす

version: '3'

services:
  hc:
    image: linuxserver/healthchecks
    environment:
      - PUID=1000
      - PGID=1000
      - SITE_ROOT=http://localhost:8000
      - SITE_NAME=test-hc
      - EMAIL_HOST=smtp.gmail.com
      - EMAIL_PORT=587
      - EMAIL_HOST_USER=<あなたのGmailアドレス>
      - EMAIL_HOST_PASSWORD=<上記で取得したアプリケーションパスワード>
      - EMAIL_USE_TLS=True
    ports:
      - "8000:8000"

docker-compose up -d

ユーザー登録

ユーザー登録とはいうもののSaaS版とは関係ありません、OSS版で動かしているものに対して登録が必要です。 早速 http://localhost:8000/ を開きましょう そこから sign up ボタンを押して、メールアドレスを入れます。メアドは何でもOKです、composeファイルに書いたgmailのメアドでもOK。メールが届くので本文中のリンクからサクッと飛んでログイン完了。

いよいよ使ってみる

使うだけならSaaSのほうがいいかもな、とここまで書いて思いました。結構簡単になったとは思うけど、メールがやっぱ面倒だよな。。

まず、APIキーを発行します。これがないとどうにもならない。

f:id:iga-ninja:20190323124429p:plain f:id:iga-ninja:20190323124529p:plain f:id:iga-ninja:20190323124547p:plain

次にcheckを登録します。この処理はAPI Keyが必要です。 詳細はDocsのリンクから見てください。すぐわかると思います。timeout/grace については解ると思いますが、channels はアラート判定時もしくはOK判定時にどのIntegrationに送信するかのコントロールに使います。ここでは "*" を指定することにより、定義済みIntegration全部に対してアラート発砲します。

$ curl -H "X-Api-Key: KGYO7W0-7MCKh6s076X6zyjQwbUOVgew" \
 http://localhost:8000/api/v1/checks/ \
 --data '{"name": "test", "tags": "test", "timeout": 60, "grace": 60, "channels": "*"}' | jq .

{
  "name": "test",
  "ping_url": "http://localhost:8000/ping/8f894e90-cb77-45e7-971b-a8a8639edc68",
  "update_url": "http://localhost:8000/api/v1/checks/8f894e90-cb77-45e7-971b-a8a8639edc68",
  "pause_url": "http://localhost:8000/api/v1/checks/8f894e90-cb77-45e7-971b-a8a8639edc68/pause",
  "tags": "test",
  "grace": 60,
  "n_pings": 0,
  "status": "new",
  "channels": "81281b62-2619-42e2-84fc-8ae2801659c9",
  "last_ping": null,
  "next_ping": null,
  "timeout": 60
}

ブラウザでchecks一覧を確認するとこんな感じ f:id:iga-ninja:20190323124646p:plain

上のHTTPリクエストの戻りにあるping_url を叩きます。こちらは API-Keyは不要です。

curl http://localhost:8000/ping/8f894e90-cb77-45e7-971b-a8a8639edc68

そうすると下記のようにWebインターフェースに表示されます。

f:id:iga-ninja:20190323143126p:plain

このまま timeout および grace の時間が経過する2分経過まで放置しましょう。そうすると

f:id:iga-ninja:20190323143558p:plain

アラートが発砲されました!

実践でどう使うのか

順番が逆になりますが、そもそもこれをどう実践で使うかについて。 具体的にバックアップジョブを監視したいとします。仮にこれは1日1回、昼の12時に実行しているとします。このジョブの実行方法自体は変わりません、CronならCronで他のJob管理ツールならそれで良いです。変更するのは、バッチジョブ完了時に ping_url を叩くだけです、http(s) クライアントであれば何で叩いてもOK。
Healthcheckの動きに準じて説明します

  1. ジョブの定期実行頻度など仕様にあわせて check を create する APIを叩きます。timeout: 1 日 grace: 半日などで
  2. ping_url が払い出されるのでメモる
  3. バックアップジョブの処理の最後に ping_url を叩く
  4. 仮に次の日にバックアップジョブが走ったら、そのpingからまた1日延長(猶予)される
  5. その次の日にもしもバックアップジョブが失敗したら・それどころか何かが原因でcronすらコケていたら1日と半日経過した時点でアラート発砲

優れている点

なんと言ってもAPIで全部完結している点です。AutoScalingなどでインスタンスが複数立ち上がったり消えたりする、それらすべてで定期実行ジョブがあるのを見張るとなったとき、

  • 起動(Scale-Out)時に監視対象として追加する
  • 削除(Scale-In)時に監視対象から削除する

までを簡単にコントロールできる点です。その他Integration(= alart の発砲先)の多さ・グループ機能・タグ付けなど これぐらい常識だよね な機能は全部入っています。まあ、四の五の言わずに使えばわかります、それぐらいシンプルです。

感想

汎用的な手法で、この「ジョブは確かに実行された」を保証するには、必ず何らかの外部の観測者が必要となります。ジョブが走るサーバで自己完結してしまうとこの主題である「そもそも着火してないやん!」を見つけることはできない。この観測者の実装ですが、簡単そうで意外と難しい。

類似するSaaSは実は結構ありますが、私が数年前に一通り使ったところ healthchecks が最善手でした。他のものはAPIでのコントロール域が狭かったり、柔軟性が乏しかったり。

そもそも、第一感として、こんなのサーバーレスでやればいいやん! だったんですが、NG判定時に即座にアラートを飛ばすことが重要なのでDaemon化が必ずどこかに必要、よってコスト度外視しない限りサーバーレス(FaaS)での実装は理論的に無理。

あと、インフラについてですが、上で紹介したDockerイメージ(のDockerfile)はかなり良いです。勉強になるので興味がある人は見てみて。インフラ視点だと実は結構難しいことがわかり、気付きもたくさんあったので、そのうち書くかもしれない。

*1:ログの突き合わせをやるにしても、さらに定期実行ジョブを足すことになり、またそのジョブがちゃんと着火したかを誰かが見張らなければならないという無間地獄に陥ります。

最新版 Google Cloud Storage + L7LB で独自ドメインで静的サイトホスティング SSLもイケます

GCPGoogle Cloud Storage (GCS) & 独自ドメインで httpサーバを立てるというのを以前やりました。もはや2015年の古いエントリですが今も結構な閲覧数があるので、2019年版のリファインとして、もっといいやり方を紹介します。

iga-ninja.hatenablog.com

なお、この上記の方法は未だに通用はします。httpだけでOK, httpsは不要であれば上記の方法を今でもおすすめします。
また、本エントリーは深い解説はせず、手順だけをサラッと書きます

今回作るもの

の静的Webホスティングです。

AWSでいう CloudFront + S3 + ACM の組み合わせです。

手順

例によってフル GCP

材料

前回のエントリはGCSとCloudDNSのみでしたが、今回はL7LB + External IPアドレスが追加です。

ドメインの取得 + CloudDNSまで連携

これは以前の記事と同じです。割愛します。

Webマスターツールでドメイン所有権の証明は不要

朗報です。かなり楽になります。
正確には FQDNと同じ名前でバケットを作らないのならば 不要です。今回はGCS単体ではなくL7LB経由でアクセスさせるので、バケット名は不問になりました!

GCSでバケット作成

以前はgs と書いていましたがGCSのほうが主流のようなので、改めます。上記の通り FQDNと同じ名前のバケット名である必要はありません
好きな名前で作れればOKです

GCSへコンテンツアップロードと、ACLの調整

index.htmlをアップロードしますが、そのままでは閲覧できません。パブリック公開状態にする必要があります。以前はポチッと押すボタンがあったのですが、変わっています。

2019の手法としては、Bucket Policy で allUsers に対して Legacy Object Reader をつける方法が最善手です。本ブログでも触れています。

静的IPアドレスの取得

f:id:iga-ninja:20190216102416p:plain

f:id:iga-ninja:20190216113610p:plain

CloudDNSレコード登録

上記で取得したIPアドレスをAレコードに登録します。 f:id:iga-ninja:20190216120409p:plain

L7LBの作成

f:id:iga-ninja:20190216113615p:plain

f:id:iga-ninja:20190216113633p:plain

LBの設定はかなり複雑ですので、3パートに分かれています。各パートごとにクリックして設定していきます。 f:id:iga-ninja:20190216113641p:plain

まずはバックエンドの設定です。Backend bucketsをもちろん指定します。 f:id:iga-ninja:20190216113645p:plain

ホスト・パス ルーティングをする場合に指定しますが、今回は特に変更しません。今回はシンプルにバケットの構造をそのまま使います。 f:id:iga-ninja:20190216113648p:plain

フロントエンドとはListenerのことです。この例ではhttp 80も通します f:id:iga-ninja:20190216113651p:plain

次に本命のhttps側の設定です。証明書の設定が必要です f:id:iga-ninja:20190216115226p:plain

証明書は即時発行ではありません。しばらく時間がかかります(Let's Encryptなんで) f:id:iga-ninja:20190216113705p:plain

見直してCreateしましょう f:id:iga-ninja:20190216113707p:plain

注意点

GCSに置くファイルは 公開アクセス とする必要あり

これは必須です、S3のような Origin Access Identity は存在しないからです。結構深い話なので、続きは別エントリで

証明書の問題

すぐには使えません。Let's Encryptを代行してくれるだけなので、そのつもりで。

2019年版 Google Cloud Storage (gcs) のアクセスコントロールがより簡単に

以前に書いたエントリ、結構な更新が入っているので新規エントリとしました。

iga-ninja.hatenablog.com

導入時期は不明ですが、2つ更新項目を書きます

Bucket policy として Legacy な権限を指定可能

前回のエントリで、Default ACLを使わないと危険と書きましたが、ここが解消されています。重要なポイントなのでスクリーンショットで示します。

危険なのはこちら f:id:iga-ninja:20190216150557p:plain

安全な指定方法はこちら f:id:iga-ninja:20190216150601p:plain

危険か安全かはこれで確認 f:id:iga-ninja:20190216150602p:plain

仕組みの説明

一般公開向けであっても、Legacyを使わなければディレクトリ構造が見えてしまうという弱点を以前書きましたが、Bucket全体に対してLegacyの権限も適応可能になりました。(いつから可能になったのかはわかりません)
この時点でもうObject ACL (& Default ACL) はお役御免です。お疲れ様でした。 定方法はこちら

Bucket Policy Only?

もう一つまだベータのようですが、機能が追加されていました。Bucket Policy Onlyという機能ですが、こちらはObject ACLを封じ込めてBucket Policy つまり Bucket全体でのみ権限を管理できます。AWSでもあるあるパターンですが、Bucket単位では非公開設定だけど、Object単位では公開にしていた、というパターンを抑制できます。まあ、AWSのS3にも追加された機能とほぼ同じ狙いですね。

このあたり、FTP/SFTP などのツールでサーバにアップロードしたら、パーミッションは弄るものだ!という昔からの習慣が残っている限り、絶対に発生しうるミスなのでパブリッククラウド側のシステムで封じ込めるのは致し方なしだと思います。

Bucket Policy という名前の語弊

AWS識者には大きな語弊を含みます。単刀直入に

  • アクセス元IPアドレスで絞ったりとかはできない
  • JSONで好きなルールを書いたりとかもできない

AWSでGCEぽく、Lambdaでインスタンス起動時にDNS自動登録する

GCEでインスタンスを起動するときに、先にホスト名を決める必要があります。AutoScaling等の場合は、勝手にプリフィックスがついて名前被りを避けます。これによりAWSのようなIPアドレスベースの使いにくいFQDN(Internal)ではなく、直感的な名前で名前解決ができます。
AWSでもUserData,cloud-initによる機構などで実現することは可能ですが、私の直感として「そんなことはインフラだけで完結しろよ!」と思うので、GCEぽい動きをするLambdaを約2年前に作成していたので、今更蔵出し公開します。
github.com

使いたいだけの人は使い方はすべてGitHub上に書いていますので、そちらを見てください。

f:id:iga-ninja:20181216103803j:plain

このエントリでは、今回蔵出ししたこのツールを通して serverlessフレームワークの歴史と使い方について振り返り書きます。書きたいのはこっちですが、タイトル詐欺ではないので許して。

その前にこのツールの意義

前述の通り、このツールは2年前に作成していました。現在と状況が違う点としては、Route53 auto naming (service discovery) が今は存在しますが、一応このツールとは競合していないです。

R53 auto naming との違い

結論から言うと、やっぱり ecs (eksも?) service discoveryとして使うべきものであり、名前解決が面倒だから楽したいという意図で使うべきではないと、一度使った人なら思うはず。

  • service discovery = サービス(web とか smtp とか)の生死を名前解決に反映させる
  • 本ツール = ただ、内部ホスト間の名前解決を楽したい

のように目的が根本的に違います。そのためservice discovery は見た目こそroute53でHostedZoneも見えるのですが、そのHostedZoneがAWS管轄となり、ユーザーが自力でレコードを一切いじれない となります。

例として、ただ単に名前解決したいと思い、Private Hostedzone .hoge. を作りました。
そこにはすでにいくつかFQDNを登録しています。
その .hoge. に service discovery で追加される分も混ぜることはできないです

言葉で書いてもピンとこないと思いますが、一発作ればよくわかります、ただ単に名前解決を楽にしたいだけという低い意識に対して service discovery (auto naming) を使うは完全にオーバースペックで悪手です。

UserDataなどを使う方法との違い

本ツールはEC2イベントとLambdaを使いますので、EC2インスタンス側への組み込みは一切不要です。UserData等の場合は、Powershellなのか、Shellなのか、AWSCLIなのか、Powershellのモジュールなのかいろいろ考える必要があります。もしくはGo lang win/linux 共通のバイナリを作るとか。でもそんなのめんどくさくてやってられない。

terraform/Cfnとかでインスタンスと同じタイミングにレコード追加するなどとの違い

上と一緒でめんどくさい。それよりなにより、名前解決ごときでterraformの実行反映がとろいのが嫌。IAMとRoute53のレコードは特に反映までの待ちが長いので、こんなしょうもないこと(ホスト間の名前解決)のためにいちいち待たされんのがいや。それと、AutoScaling等を導入する場合を考慮すると、結局UserDataなどを使わないといけないのでこちらのほうが守備範囲が狭い。

serverless framework(sls)

本エントリの本題に入ります。ダラダラ書きますが、基本は分かっている人向けで、入門的な話は書きません。

概要・存在意義

serverless version 1.x 系のリリースのころから私は始めました。当時の私の認識が間違っていたので、ここでも改めて書きますが

  • あくまでもLambdaが中心 (API Gatewayが中心じゃない)
  • プラグイン型にしたことに先見の明あり
  • nodejs製なので、serverless framework自体のバージョン固定必須

Lambdaを中心として、そのLambdaが必要とするIAMロールと、そのLambdaの導火線(着火剤)となるイベントを定義できます。Webに特化したものではないです、Webにも使えますが。AWSの場合はこの点が非常に重要で、特にインフラ屋が使うツールはたくさんのサービスに対してアクセスすることが多いです。これらを1元管理できるのは便利。
serverlessフレームワークは前身があったはずで、その頃はプラグイン型ではなかったはず。特にAWSのように目まぐるしき新しいサービス・サービス関連系が増えるものは明らかにこのモデルが優秀で、プラグインから本体へ取り込まれたものもいくつかあるはず。
最後に悪い点としてnodejsでできていること。ともかくアップデートのプレッシャーがきつく、nodejs本体のバージョンにも注意が必要。なので、ここはコンテナ(docker)化する。

EC2イベントから slsの本質を知る

2年前の実装をリファインするにあたり、まず最初にいじったのがここ。EC2イベントは当時はsls本体のイベントとしては存在しなかった。じゃあ、どうやって表現していたのか?slsの実装を忖度して、実行時=deploy に生成される Cfnテンプレートをイメージしながら追加リソースを書く となります。なんのとこやらわからないと思うので

# you can add CloudFormation resource templates here
resources:
 Resources:
   RegisterRule:
     Type: AWS::Events::Rule
     Properties:
       EventPattern:
         {
           "source": [
           "aws.ec2"
           ],
           "detail-type": [
             "EC2 Instance State-change Notification"
           ],
           "detail": {
              "state": [
                 "running"
              ]
           }
         }
       Name: register-rule
       Targets:
         -
           Arn:
             Fn::GetAtt:
               - "RegisterLambdaFunction"
               - "Arn"
           Id: "TargetFunc1"
   RegisterPermissionForEventsToInvokeLambda:
     Type: "AWS::Lambda::Permission"
     Properties:
       FunctionName:
         Ref: "RegisterLambdaFunction"
       Action: "lambda:InvokeFunction"
       Principal: "events.amazonaws.com"
       SourceArn:
         Fn::GetAtt:
           - "RegisterRule"
           - "Arn"
   UnRegisterRule:
     Type: AWS::Events::Rule
     Properties:
       EventPattern:
         {
           "source": [
           "aws.ec2"
           ],
           "detail-type": [
             "EC2 Instance State-change Notification"
           ],
           "detail": {
              "state": [
                 "terminated"
              ]
           }
         }
       Name: unregister-rule
       Targets:
         -
           Arn:
             Fn::GetAtt:
               - "UnregisterLambdaFunction"
               - "Arn"
           Id: "TargetFunc1"
   PermissionForEventsToInvokeLambda:
     Type: "AWS::Lambda::Permission"
     Properties:
       FunctionName:
         Ref: "UnregisterLambdaFunction"
       Action: "lambda:InvokeFunction"
       Principal: "events.amazonaws.com"
       SourceArn:
         Fn::GetAtt:
           - "UnRegisterRule"
           - "Arn"

というふうに書きました。slsはコード以外のデプロイつまりAWSリソースはすべてCfnで行われます。slsの優れているところは、このCfnに対してほぼべた書きでserverless.yml に追記できる点です。これより2年前の私はEC2イベントを実現していました。これは言葉を変えれば、今本体がサポートしていないイベントでも、Cfnがわかる人ならば自分で追加できるということになります。この点は本体slsのマージが遅いならばとっととプラグインとして公開し、みんな早くハッピーになれるので、先見の明と先に書いたのはこの点です。

解説します。

RegisterRule:
     Type: AWS::Events::Rule

はCloudWatch Events を定義しています、注目ポイントは

           Arn:
             Fn::GetAtt:
               - "UnregisterLambdaFunction"
               - "Arn"
           Id: "TargetFunc1"

で、イベント時に着火するLambdaを指定していますが、この名前は deploy 時に反映されるCfnのテンプレートやリソース名を確認して忖度し適合させています。みなさんがやる場合も生成されるCfnを読んでslsに忖度してあげましょう。

次に

   PermissionForEventsToInvokeLambda:
     Type: "AWS::Lambda::Permission"

の部分です。これはManagementConsoleでしかLambdaをいじったことがないと一生理解できないかもしれないLambda側のパーミッションです。Lambda関数の中で必要となる = APIコールするAWSリソースに対して、Lambdaが権限を持っている必要があるは直感でわかるはずですが、これは逆に S3やCloudWatch Events のようなものから対象Lambdaが呼ばれてもいいよ という許可設定です。この点は重要なので詳しく書いておきます。
例えばAWSリソース(A)から別のAWSリソース(B)へAPIコールする場合に、AにCredentialなどが持てるのであれば、A -> B の権限を、Aが持っていれば良いとなります。しかし CloudWatch EventsにはCredentialは仕込めません。私はこの手のものを俺用語で公共物と言ってます、具体的にはS3やCloudWatch Events,SQS 。対して、Credentialを仕込ませられるものは占有物と勝手にいい、代表はEC2やLambdaです。
つまり、占有物 -> 公共物ならば 占有物にCredential+権限があればOK。しかし公共物 -> 占有物には Credentialが使えない、「どうしよう、公共物ってことなので、だれでも名前さえ知ってたら呼べちゃう」を解決するのがこのLambda Permission。簡単にいうと、受ける側で送信元を限定する、A -> B の呼び出しをB側で許可するということになります。最近は公共物でもロールを当てることができたりします e.g. Cfnロール

EC2のイベントが本体に取り組まれたため、今回はこんなにスッキリしています。たったこれだけでCloudWatch Eventsの登録 & Lambdaパーミッションの設定ができます。

functions:
  register:
    handler: register.handle
    memorySize: 128
    timeout: 30
    events:
      - cloudwatchEvent:
          event:
            source:
              - "aws.ec2"
            detail-type:
              - "EC2 Instance State-change Notification"
            detail:
              state:
                - running

nodejs(sls)を固定化させる方法

最近のnpm は lockが可能なので少し立ち位置が微妙ですが yarn(hadoop関係ない方)で固定化させていました。とりあえず今回は npmでlockするので、yarnは無しにしました。 次にnodejs本体のバージョン固定と、ネイティブバイナリのビルドについての考慮です。

nodejs本体のバージョン固定

slsが入ったDockerイメージも誰かが作っているので、これに乗っかるのもありです。が、訳あってこれは使いません。

lambda向けネイティブバイナリビルド

じつは今回公開したLambdaは、ネイティブバイナリのビルドはありません。のでこの機能は必要ないのですが。ネイティブバイナリとは、まあ一般的にはC/C++ で書かれるそのOS/CPUでしか動かないコンパイル済みのバイナリを指します。これはmacでもWindowsでも引っかかる問題です。仮にローカルPCとしてmacでビルドした場合、mac用のバイナリができます。当然これをLinuxに持ち込んでも動かない。
Python使いなら requirements.txt というのを書くと思いますが、これをDocker でビルドして、バイナリをクライアントPCに持ってくるプラグインがあります serverless-python-requirements
これはrequirements.txtを扱えるだけではなく、Dockerを使ってのクロスコンパイル(というかDockerコンテナがLinuxなのでそこで普通にビルドするだけ)が可能です。
どうせやるならこの点も解消しようと思いました。

slsコマンド本体はDockerでやるのは決めていたので、そのまま何も考えないならば slsのDockerから、更にbuild用Dockerを作りそこでビルドです。ちなみにこれはDinDを使わなくとも一応実現可能ですが、macでしか使えないはずです。今回は採用しませんでしたが、一応残しておくと

  • slsコンテナに -v で docker.socket(UNIXドメインソケット)を渡す
  • slsコンテナに dockerコマンドを突っ込む
  • 上記で完璧と思ったが、slsコンテナからビルド用コンテナに行くとき、さらに -v でソース=requirements.ext がある場所をバインドしている。よって -v する mac上のパスと、slsコンテナのパスを完全に一致させる必要がある。

となります。が、この方法ははっきり言ってアホです。そもそもslsコンテナの時点でLinuxなので、だったらそこで普通にビルドしたら良いだけです。serverless-python-requirementsはDockerを使わないビルドも可能ですので、今回はこちらでやりました。方針としてはslsのblogと同じように、serverless-python-requirementsはグローバルインストールしない、dockerizePip: falseで使うことにします。こちらもpython37/36 だけですが Dockerfile & image を公開しています。 github.com

https://hub.docker.com/r/himaoka/sls-builder

まとめ・感想

2年たった今でもslsは優秀だなと改めて感じた。プラグイン型や、Cfnによるデプロイは本当に先見の明があり。

  • ド素人は本体にあるイベントで我慢して
  • ちょっと分かる人は、Cfnのリソースを直接書き足して(2年前の私)
  • もっと分かる人は、プラグインにして公開して
  • プラグインでももうこれは本体に取り込んで良いんじゃないかと思うものは、PRして

こうすることで、素人に優しい環境を提供しつつ、玄人の尊厳も忘れないというOSSの理想の形が体現できているように思う。ま、samは使ったことないんだけどね。