日々のあれこれφ(..)

もっぱら壁打ち

Google Cloud APIsの認証の種類と使い分けについて

以前まではAWS環境で開発を行うことが多かったのですが、最近ではGCP環境も結構触っています。

Google Cloud APIを使ったプロジェクトの開発を行う際、私はいつもサービスアカウントキー を作成してそれを読み込ませる形でローカル環境を構築していたのですが、別の人はOAuth 2.0クライアント認証を用いるやり方で開発をしていたと聞きました。

このようにGoogle Cloud APIを使う際には認証のやり方が複数あるのですが、どれをどういう思想で使い分けすればいいのか、はたまた開発者自由でいいのか、認証の設計思想に理解が浅い状態だったので腰を据えてドキュメントを読んだ時のメモです。

Authentication overview  |  Google Cloud

今回の話のスコープ

Google Cloud APIの話になります。Google Maps Platform APIのようなGCP外のサービスは含みません。

認証を知るにあたって出てくる概念

プリンシパル

プリンシパルをうまく和訳して説明するのが難しいのですが、APIを使う主語にあたるものをさしているという認識です。プリンシパルはもっぱら人間か人間以外のもの(サーバーやbotなど)によるアクセスかで種類が分かれます。

  • User accounts
    • 想定: 人間によるアクセス
    • 管理: Googleアカウントを使って行う
  • Service accounts
    • 想定: 人間以外からのアクセス(システムなど)
    • 管理: IAM

アプリケーション

プリンシパルが「"誰が"リクエストをするか」にあたる部分とするなら、アプリケーションは「"どこで"リクエストをするか」にあたる部分を示しています。具体的にはGCEやGAE、GKEやGCP外(誰かのローカル環境)などの実行環境を示すものと思われます。

3種類の認証情報の種類

これらの概念を踏まえた上で、認証の方法としては3種類用意されています。

APIキー

これは一番分かり易いもので、プリンシパルは識別せず、アプリケーションのみを識別するものです。

例えば、そのAPIキーを持っていたら、誰でもGCSの特定のオブジェクトにアクセスできるというようなことを実現できます。

OAuth 2.0クライアント認証

認証時にGoogleアカウントが必要になる、プリンシパルがUser accounts(人間によるアクセス)を想定した認証になっているようです。

サービスアカウントキー

認証時にサービスアカウントと紐づけられたサービスアカウント キーが必要になる、プリンシパルがService accounts(人間以外からのアクセス)を想定した認証になっているようです。認証情報に個人のGoogleアカウントを必要としないので、本番環境のシステムやCICDのような共有しているものに対して使えます。

サービスアカウント には厳密には二種類存在します。

  • デフォルトサービスアカウント
    • GCPのコンピュート系のリソース(GCEやGAE等)にデフォルトで付与されているもので、GCP環境内からAPIを実行するときはサービスアカウント の付与が不要になります。これはGoogle Cloud Client Libraries(Google Cloud APIを呼び出すためのクライアントライブラリ。各言語のものが一通り用意されている)は、サービスアカウントの認証情報を自動的に検索して使用するようになっているためです
  • サービスアカウント
    • デフォルトでないものは全部こちらです。IAMからユーザーが必要に応じて作成します。サービスアカウント キーとは、ここで作ったサービスアカウントで作成または登録をしたキーを、Google Cloud Client Librariesライブラリに読み込ませることで認証を行います

使い分けの戦略について

アプリケーションに必要な実行権限と実行場所を基に何を使うか決めるのが良さそうとのことで、一例が以下のようになります。

  • APIキー
    • 公開データへの匿名アクセス
  • OAuth 2.0クライアント認証
    • エンドユーザーに代わって個人データにアクセスする
  • サービスアカウントキー
    • GoogleCloud環境外のサービスアカウントに代わって個人データにアクセスする
  • デフォルトサービスアカウント(認証情報を渡さなくてもいい)
    • GoogleCloud環境内のサービスアカウントに代わって個人データにアクセスする

おわりに

APIキーはさておき、OAuth 2.0クライアント認証とサービスアカウントキー、開発時はどちらを使うのが好ましいのかわからなかったのですが、ドキュメントを見たところどちらでも良さそうでした。

強いて挙げるとしたら、サービスアカウント にはリソースに対して実行できるアクションを絞ることができるので、権限の検証をしたいときはサービスアカウント キーを設定し、それ以外は開発者個人のGoogleアカウントを使ってOAuth 2.0クライアント認証を行うのがよいのでしょうか。

ただOAuth2.0クライアント認証を使う場合、アクションに制限をつけるためにOAuth scopesの定義が求められたりして、ソースの書き方が変わってくるケースもあるかと思うので本番とできるだけ差異を小さくするのであればサービスアカウント キーを使うようにした方がよいのかなと個人的には思っていたりもします。

開発を行っていくにあたってこういった重要な周辺リソースの思想はちゃんと知っておきたいものです。

CTF入門した

2週間くらい前からCTFを始めました。

CTFとは

Catcher The Flagの略で、相手の陣地にある旗を先取するゲームです。サバゲやeスポーツなどでも出てくる用語ですが私が始めたのはセキュリティ競技CTFです。

セキュリティ競技CTFでは、情報セキュリティの知識を駆使してお題を解いて解答(フラグ)を入手していきます。クイズ形式以外にもチームを組んで相手チームのサーバに侵入して旗を取るなど色々形式はあります。

興味を持ったはいいが周りでやっている人もいなかったので取っ掛かりが掴めなかった私ですが、きなこもちさんのブログが参考になりました!

CTF初心者が考えるCTF入門 - きなこもち。

入門としてやったこと

CpawCTF

  • 大学のプログラミングサークル運営
  • 問題数全23問ほど

少しのググり力で解けるLv.1の問題から、若干ひねってきたLv.2、自力で解けずに諦めて解説を探しにいくLv.3、といった感じでした。まさに入門者にはちょうどいい難易度で普通に楽しめました。

私が全問題解き終わった数日後くらいに証明書が切れてアクセスできない状態が続くようになったのですが、復活するといいな。

picoCTF

中高生向けといいつつ、難しい問題は普通に難しいです。 問題数が多いので今回はジャンルごとに解いていくことにし、今の時点で私はWebアプリケーションを題材にしたWeb Exploitionというジャンルの問題を全13問一通り解き終わったところです。ソース見ればパッと答えが分かるような簡単過ぎる問題から、ちょっとしたバイナリの知識が求められるやや難しいものまであり、とても充実していました。

入門〜初級レベルの問題で使えるテクニック

問題を解く中で知った小技を共有します。問題のネタバレはここでは含まないようにします。

Chrome Developper Tool

「右クリック > 拡張」 で開く、webサービスの開発には欠かせないデバッグツールです。

Cookieを編集する

やり方を調べると拡張機能を使ったものが多く紹介されていましたが、拡張機能を入れなくてもdevtoolでできました。

(写真は問題関係ないページを開いた時のものです)

f:id:reiichii:20201031230238p:plain

Networkの通信を確認する

この画面を開いた状態でページを開くと、他のサイトとどのような通信を行なっているのかが記録されるページです。ページ遷移の最中に実は一瞬だけ別のサイトにリダイレクトしていたり、リクエストヘッダーやボディの中を確認したりなど、何かと使いました。

f:id:reiichii:20201101114432p:plain

debugger

Web系の問題にはJavaScriptを用いた問題も多く出されます。debuggerで変数の中身を確認したり、コードをいじったりなどして調査するのに欠かせません。

f:id:reiichii:20201101115008p:plain

tshark

ネットワーク系の問題ではパケットキャプチャのファイルを渡されて、そこからフラグを探しに行くことが多いです。 tsharkかもしくはwebアプリ版のwiresharkを使ってパケットキャプチャを読めるようにしていきます。

$ tshark -r test.pcap
    1   0.000000 192.168.1.114 → 192.168.1.1  DNS 77 Standard query 0x504d A q28.ctf.cpaw.site
    2   0.001660  192.168.1.1 → 192.168.1.114 DNS 126 Standard query response 0x504d A q28.ctf.cpaw.site CNAME host1.ctf.cpaw.site A 157.7.52.186
    3   2.380278 192.168.1.114 → 157.7.52.186 TCP 78 52852 → 21 [SYN] Seq=0 Win=65535 Len=0 MSS=1460 WS=8 TSval=246798505 TSecr=0 SACK_PERM=1
    4   2.404144 157.7.52.186 → 192.168.1.114 TCP 74 21 → 52852 [SYN, ACK] Seq=0 Ack=1 Win=14480 Len=0 MSS=1460 SACK_PERM=1 TSval=444760454 TSecr=246798505 WS=128
    5   2.404244 192.168.1.114 → 157.7.52.186 TCP 66 52852 → 21 [ACK] Seq=1 Ack=1 Win=131768 Len=0 TSval=246798529 TSecr=444760454

また対応するプロトコルでダウンロードしたファイルの復元も行えます。

問題の中ではパケットキャプチャからhtml,css,jsのファイルを復元してフラグを探すものなどもありました。

$ tshark -r hoge.pcap --export-objects http,/path/to/cpaw

fileコマンド

Binary Exploitation(バイナリ解析)系の問題ではよく分からないファイルを渡されるところから始まりますが、だいたい最初はfileコマンドで実行環境を確認するところから始めることが多かったです。

$ file hoge
hoge: tcpdump capture file (little-endian) - version 0.0, capture length 1869357413)

この場合hogeというファイルがtcpdump capture fileであることが分かります。

stringsコマンド

Binary Exploitation(バイナリ解析)入門その2。バイナリファイルの読める部分を抽出してくれるコマンドです。そんなコマンドあるのかと初めて知って驚きました。このコマンドを実行してうっすら答えが見えてくるような問題もあったのですが、本当に入門レベルの問題だけなんだろうなという気がしますね。

$ strings hoge
a``d
a``Pyr`5
~un6+
JGBFz
rd=h5
00pV
>7 n

一番有名なsqlインジェクション

' OR 1=1 --

例えばログイン名とパスワードを入力するフォームがあるとします。

サーバサイドで入力値を適切にエスケープ処理がされていない以下のようなコードがあった場合、裏側で意図しないsqlが発行されてしまいます。

サーバサイドのコード: SELECT * FROM user WHERE name='%s' AND password = '%s'

[本来のユースケース] ユーザー名: piyoko パスワード: hghghghghghg と入力した場合

SELECT * FROM user WHERE name='piyoko' AND password = 'hghghghghghg'

ユーザー名とパスワードで検索をかけ、該当するデータが存在した場合入力情報が適切とみなされてログインができます。

[悪用された場合] ユーザー名: ' OR 1=1 -- パスワード: (てきとうな文字列)

  • ' : 最初の閉じ括弧でname = '' を終わらせる
  • OR 1=1 : 任意のsqlを入れられる。この場合はpasswordの代わりにWHERE条件が必ずマッチするように1=1が入れられている
  • -- : これ以降のsqlをコメントとして無効にする。この例の場合 password一致が無効になる

SELECT * FROM user WHERE name='' OR 1=1 -- AND password =

と言った形でログイン情報を知らない人にもログインが行われてしまいます。

上記のような考え方を応用した問題がwebジャンルでは出題されます。

gdb

書いておいて何ですが、Reversing(リバースエンジニアリング)系の問題に関しては、知識が無さすぎて入門に立てていないです。 コマンドを実行しながら解説を辿っても、中々解説者と同じ思考回路を辿れません...。(そもそも逆アセンブリという言葉をはじめて知った。)

おわりに

ひとまずpicoCTFの他のジャンルに手を広げつつ、来年にはビギナーズコンテストなどにもチャレンジできたらいいなと思っています。

CloudWatch Logsの調査にuternを使ってみた

アプリケーションのエラー調査のため、CloudWatchログの中身を漁ることを行なっていました。

調査をする際にuternというGo製のツールを使っていたので、所感をまとめます。

  • ロググループとログストリームを取得するときはaws cliを使う
aws logs describe-log-groups --query "logGroups[].[logGroupName]" --output text
aws logs describe-log-streams --log-group-name /ecs/users-stg --query "logStreams[].[logStreamName]" --output text/ecs/users-stg
  • 対象のログストリームをフィルタリングして対象のログを取得する
utern --no-log-group --no-log-stream --max-length 100 --filter "error" -n ecs/datadog -s 1h /ecs/hogehoge-stg

基本的に作者のQiitaとGitHubのREADMEが充実していたので、使い方に特に困ることはありませんでした。

qiita.com

github.com

そんなに詳しく調べたわけではないのですが、個人的にありがたかったのはログがたくさん出てしまうと探しにくいので、 --max-length である程度文字数を絞って選択範囲の一覧を俯瞰し、そこからログストリームを指定したりさらにフィルターをかけたりするような使い方をしていました。

自動でtail状態になるので、そこはオプションで選択できるようになるといいなと思いました。

1日で作られたそうですごい。使い易くて良いツールでした。

tarコマンドの挙動の確認

2014-04-10/不特定多数のzipファイルがある構成

# アーカイブ前
~/test1106$ ls -lah 2014-04-10 | head -n 1
total 532K
# アーカイブ
~/test1106$ tar cvf 2014-04-10.tar 2014-04-10
~/test1106$ ls -lah
total 568K
# 展開
~/test1106/2014-04-10_tar$ ls -lah 2014-04-10/ | head -n 3
total 532K

30Kほど増えた。

#1 含む
~/test1106$ zip -r 2014-04-10.zip 2014-04-10
#2 含まない
~/test1106/2014-04-10$ zip -r 2014-04-10_2.zip ./

#1の解凍
~/test1106$ unzip 2014-04-10.zip -d ../2014-04-10_1
~/test1106/2014-04-10_1$ tree -L 2 | head -n 5
.
└── 2014-04-10
    ├── 0021_0_log.gz
    ├── 0042_0_log.gz
    ├── 0051_0_log.gz
#2の解凍
~/test1106/2014-04-10$ unzip 2014-04-10_2.zip -d ../2014-04-10_2
~/test1106/2014-04-10_2$ tree -L 2 | head -n 3
.
├── 0021_0_log.gz
├── 0042_0_log.gz

最初親ディレクトリを含んでしまっていることを分かっておらず、codebuild等で実行するymlファイルのzip化を親ディレクトリ含んで行ってしまっていて、実行ファイルがありませんとエラーが出て結構な時間悩んだことがあった(今思えばむしろなんで気づかなかったと言いたいところだけれど)

ちなみに容量に関しては、親ディレクトリを含む方が2Kほど増えた

  • (余談)lsとduについて ファイルの容量を調べたい時、lsとduコマンドどちらを使えばいいかで迷った。 二つのコマンドを実行してフォルダやファイルのデータ容量を見ると、表示される数字に大きく差分が生じた。
$ ls -lah
-rw-r--r--@  1 user  staff   3.0K 11  4 22:53 test.txt

$ du -h ./test.txt
4.0K   ./test.txt

lsは「ファイルそのものの容量」

一方duは「ファイルが使用するディスク量」

を出力する。

どちらのコマンドもよく使っていたけれど、この辺りちゃんと区別できていなかったから目から鱗だった。

今回の場合はファイルそのものの容量を調査したかったから ls でよかった。

SRE Lounge #10 行ってきました

はじめに

sre-lounge.connpass.com

今日仕事終わりに行ってきました。

今回で3回目の参加になるのですが、一番楽しみにしているTechイベントです。

資料は一旦、現時点で上がっているものだけ貼ります。

タイムテーブル

  • SREcon19 Asia/Pacific Recap #1
    メルカリ @dtan4さん
  • SREcon19 Asia/Pacific Recap #2
    Quipper @chaspyさん
  • Make It Visible 〜株式会社ビズリーチ HRMOS SREチームのObservability戦略〜
    BizReach @saitotakさん
  • ヤフー様スポンサートーク SRE部の取り組み
  • スペシャル企画】モニタリングパネルディスカッション
    ヤフー @ykawamot さん、メルカリ @spesnovaさん、ソラコム 五十嵐さん、サイバーエージェント 袴田類さん

最初の二つの発表は6月にシンガポールで行われていたSRECon19に参加されたお二方のレポ&LTの再演&面白かった発表の紹介でした。意外なことに銀行系など固めな企業もこの領域に手を伸ばしていて、SRECon19に登壇されていたとか。

▼SREcon19 Asia/Pacific Recap #1 @dtan4さん発表資料

SREcon19 Asia/Pacific Recap - Speaker Deck

Our Practices of Delegating Ownership in Microservices World - Speaker Deck

@dtan4さんのLT再演は個人的に刺さる内容でこのタイミングで聞けてよかったなと。社内に数多くあるMSのTerraformコードを一つのリポジトリで一括管理されている話なんかも、Terraformのディレクトリ構成やmodule化って他社はどうしているのだろうと気になっていたので参考になりました。(Terraformは自由度高いからこういう部分が悩みどころで、せめて公式でBestPracticeみたいなのを一つ用意していて欲しい、、それらしきもの見かけたけどどうして廃止されてしまったのか)。メルカリさんは基本的には開発が運用も行う方針になっているらしく、SREチームが監視や運用部分の共通基盤を提供しているとのことでした。その共通基盤を作る取り組みの一環としてサービスごとにレベルを設定して、そのレベルごとにReadness Checklist(AutoScalingするか、キャパシティが設定されているかなど)を提供することで運用ルールを厳格化している話も興味深かったです(正直見たいと思ったのは私だけでないはず)。

▼SREcon19 Asia/Pacific Recap #2 @chaspyさん発表資料

SRECon19 AsiaPacific Recap - Speaker Deck

@chaspyさんの発表は現地の写真も多かったです。600人でこじんまりしていたと最初の発表で@dtan4さんがおっしゃっていたけど、全然こじんまりしているように見えなかった...。Linkdin社とInstagrem社の発表内容が印象に残ったと紹介してくれました。特にInstagram社さんの話は大陸をまたいでデータセンターを構築した時の話で正直スケールが大きくて付いていけなかったです。

ビズリーチの@saitotakさんの発表は、ビズリーチ社におけるSREの取り組みについて、とりわけミッションの可視化、日々の業務の可視化計測が中心の話でした。 優先順位(やらなければやばい順)に並び替えても全部優先度《高》になってしまったタスクについて、メンバーが納得のいく優先順位をつけるために「可用性+トイル+セキュリティ指標 / 概算ストーリーポイント」で計算してissue scoreを付けた話など、普通だとこの辺まで考えるだろうなのさらに一歩先まで深く考えられていることに感心しました。可視化の話は前の発表でも出てきており、改めて大事さを思い知らされました。(弊社の場合だと可視化まではできていてもカテゴリー分けや集計みたいなところが疎かになりがちだったので)。jiraとかあると集計が楽でいいですよね。

ヤフーSRE部さんの取り組みの話、ヤフーさんの会社自体は規模が大きくてエンジニアも数多くいてどちらかといえば結構特殊だと思っていたのですが、ツールを広めるためのお話はどんなチームの規模でも共通して活かせそうな体験談になっていて聞けて良かったです。機会があれば参考にしようとメモに残しました。

パネルディスカッションは今回初の取り組みでした。司会を主催の@katsuhisa__ が行い、ヤフー@ykawamot さん、メルカリ@spesnovaさん、ソラコム五十嵐さん、サイバーエージェント袴田類さんがテーマに対して自社のお話をしていく形式で、今回は監視ツールの選定やモニタリングやアラート、オンコールがテーマでした。パネルディスカッションのいいなと思ったところが、発表ではまとめようとしたらこぼれ落ちるような些細だけど他社にとっては意外と貴重だったりする情報が聞けるところでした。個人的に印象に残っているところが、CAさんはstg環境にも監視システムを導入しているという話で、最初聞いたときは検証だったらテスト用のチャンネルやDashboardなどに向ければいいじゃないかと思っていたのですが、意図としてはfalseアラートをできる限り無くして監視担当者の余計なストレスを減らすことを気をつけているそうでした。狼少年問題はよくある話でそれに対してパネルディスカッションでも色々な意見がありましたが、CAさんのこの姿勢は見習いたいと思うものでした。またアラートの飛ばし方に関しては多くの会社が主にwarningとcriticalの2種類あって、warningはslackへ通知、criticalは合わせてPargerDutyで電話もという組み合わせが多かったです。(弊社も最近PargerDutyが導入されてそんな感じになっている)

個人的な話

人見知りなので普段は終わったらさっさと帰ってしまう派だったのですが、今日は聞いてみたい話があったのと他にも色々思うところがあって懇親会に残ることにしました。何名かのエンジニアとお話しさせてもらいました。近い分野をやっている人が同僚以外だとどうしても少なくなってしまうので新鮮な気分になりましたし、自分でも知らず知らずの内に凝り固まっているところがあったなってことに気づいたので、やっぱり色んな人と話すことは大切だなと痛感しました。(いい刺激に近い方の痛感です)

またちょっと気になったのが、自分がSREの一環としてやっていると思っていたことが、他の人だと認識の違いを感じた事がありました。組織や規模によって仕組化の部分に集中できるところもあれば、1つのサービスの業務を行いながら仕組化に昇華させなければいけなかったりだとかその辺りは組織によってまちまちなのかもしれません。

SRE Loungeは現職に就いてから同僚と来ています。数ある技術の仕事の中でも、近しい分野に興味がある歳の近い勉強会に行く知り合いがあまりいなかったので、セッション聞いて普段の業務や仕組や組織について思っていることをああだこうだと気軽に言い合える良い機会にもなっています。仕事場でも普通に話はするけれど、仕事場じゃ話せないような不安やつらみ(?)みたいなのもついでに聞いてもらえて同僚には感謝しかないです。今日も同僚との会話の中で初心を思い出したりなんかしました。

おわりに

勉強会レポートなので情報量というはテンション重視で書き上げました。(途中からは深夜テンション。一応朝に推敲してます)

元々SRE的なことがやりたいと思いつつも、日々の業務に忙殺されてやりたいことに技術力が追いつかなくて歯がゆい思いをしたりして心折れそうになることも多々あります。SRE Loungeで色んな会社の発表を聞くと、狭くなっていた視野が広がって技術以外にも得られるものも多くて本当に良い刺激になっています。 いつももらってばかりで申し訳ないので、そのうち何か発表できるようになりたいです。(凄腕のエンジニアばかりで萎縮しがち)

弊社も開発チームが増えてきたので監視運用などの運用基盤の厳格化が必要になってきましたし、元々あるTerraformをCIでもっと良い感じに実行できるようにしたいですし、インフラ環境のコスト削減や自動化など、やりたいことがいっぱいです...月曜日からも仕事頑張ります。

ECSで動かしているバッチのSlackへのアラート設定

はじめに

少し前に業務でやったことの忘備録です。

アジェンダ

  • 設計思想
  • 全体の流れ
  • ECS
  • lambda
  • Datadog経由でSlackに通知
  • その他考えていたこと
  • おわりに

設計思想

とあるECSクラスタで動いているバッチが5つあるのですが、コケた時にすぐに気づける仕組みがありませんでした。 各バッチそれぞれがログは吐いているものの内容は様々なので(DBへのupdateをかけているものもあれば、毎日1回社内向けにメールを飛ばすものなど)、最低限『コケたらすぐに気づける。ログを見に行けるようにする』という要件を満たせるようにアラートの設定を行いました。

本当はアラートと一緒にログを貼り付けたり、ここを確認してこういった手順を取ってくださいみたいなplaybookを貼り付けられたら親切だったのですが、5つもあって重要度もそれほど高くないので、そこまで個別のバッチに時間をかけるのはやめました。

またDatadogを咬ませているのは、社内のルール(というかMSの監視のベストプラクティス)でサービスに関わるアラートは一箇所でまとめて管理するという方針になっているからです。そうすることでプロダクトを構成しているあらゆるサービスの監視を行いやすくします。アラートの通知先を変える必要が出た時なんかも、Datadog上からSlackのチャンネルを変更するといったことが行えるので、Lambdaのコードをいじる必要が無くなります。 Datadogを使っていなければLambdaからそのままSlackにアラートを飛ばしていました。

全体の流れ

(1)ECS → (2)CloudWatch Events → (3)Lambda → (4)Datadog → (5)Slack

(1) ECSがタスクの起動や終了を行うとイベントストリームを吐き出します。イベントストリームで吐き出された情報には、定義や終了コード、クラスタなど実行されたタスクに関する情報が含まれます。

(2) CloudWatch Eventsの設定で、特定のクラスタのイベントストリームをトリガーに、Lambda関数を実行するルールを作成します。

(3) Lambda側ではDatadogにとばすメトリクスを作成します。 飛ばすメトリクスの内容は以下のようにしました。

・バッチが正常に実行されたら0

・バッチが正常に実行されなければ1 (イベントストリームに含まれるexitCodeが0以外の数字であること、またはexitCodeが存在しない。)

exitCodeはコンテナ終了ステータスです。exitCodeが存在しない時というのは、例えば存在しないimageが指定されていてpullに失敗した時などそもそもタスクのstartに失敗した時になります。

(4) Lambdaから送ったメトリクスに対して、Datadogでモニター作成とアラートの設定を行います。 失敗した時のメトリクスが検知された時アラートを飛ばすようにここから設定を行います。 これであらかじめ設定してあるSlackのチャンネルに条件に従ってアラートを飛ばすようにできます。

ECS

イベントストリームの中身は以下のようになっています。 これはAWSブログから持ってきたもので、Lambdaのtestイベントの設定で使っています。

{
  "version": "0",
  "id": "8f07966c-b005-4a0f-9ee9-63d2c41448b3",
  "detail-type": "ECS Task State Change",
  "source": "aws.ecs",
  "account": "244698725403",
  "time": "2016-10-17T20:29:14Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328"
  ],
  "detail": {
    "clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/eventStreamTestCluster",
    "containerInstanceArn": "arn:aws:ecs:us-east-1:123456789012:container-instance/f813de39-e42c-4a27-be3c-f32ebb79a5dd",
    "containers": [
      {
        "containerArn": "arn:aws:ecs:us-east-1:123456789012:container/4b5f2b75-7d74-4625-8dc8-f14230a6ae7e",
        "exitCode": 1,
        "lastStatus": "STOPPED",
        "name": "web",
        "networkBindings": [
          {
            "bindIP": "0.0.0.0",
            "containerPort": 80,
            "hostPort": 80,
            "protocol": "tcp"
          }
        ],
        "taskArn": "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328"
      }
    ],
    "createdAt": "2016-10-17T20:28:53.671Z",
    "desiredStatus": "STOPPED",
    "lastStatus": "STOPPED",
    "overrides": {
      "containerOverrides": [
        {
          "name": "web"
        }
      ]
    },
    "startedAt": "2016-10-17T20:29:14.179Z",
    "stoppedAt": "2016-10-17T20:29:14.332Z",
    "stoppedReason": "Essential container in task exited",
    "updatedAt": "2016-10-17T20:29:14.332Z",
    "taskArn": "arn:aws:ecs:us-east-1:123456789012:task/cdf83842-a918-482b-908b-857e667ce328",
    "taskDefinitionArn": "arn:aws:ecs:us-east-1:123456789012:task-definition/wpunconfiguredfail:1",
    "version": 3
  }
}

参考 aws.amazon.com

CloudWatch Events

ルール作成時、イベントソースの設定部分で以下のようなイベントパターンを指定しています。

{
  "detail": {
    "clusterArn": [
      "arn:aws:ecs:ap-northeast-1:[ACCOUNT ID]:cluster/CLUSTER_NAME"
    ],
    "lastStatus": [
      "STOPPED"
    ]
  },
  "detail-type": [
    "ECS Task State Change"
  ],
  "source": [
    "aws.ecs"
  ]
}

ここでの懸念事項が一つありました。

最終的には正常なメトリクスもDatadog側に飛ばすようにしていますが、当初は失敗した時のみアラートを飛ばすことが目的だったので異常な時だけメトリクスを実行できるようにすればいいのではないかと考えていました。Lambdaも実行するたびにお金がかかってしまうので。

ただ上記にも書いた通り、この時点で異常終了か否かを検知することはできなそうでした。exitCodeが存在しないケースもあるためイベントパターンで定義ができなかったです。 また正常に実行されているかどうかもモニターから確認できた方が良さそうだと要件が変わったこともあり、クラスタが実行される度にLambdaを起動させることにしました。 そもそも4/5がdailyバッチなので料金が心配になる程そもそも実行しませんでした。

参考

docs.aws.amazon.com

Lambda

Pythonで書いています。

import urllib.request
from datadog_lambda.wrapper import datadog_lambda_wrapper
from datadog_lambda.metric import lambda_metric

@datadog_lambda_wrapper
def lambda_handler(event, context):
    # success=0, error=1
    batch_exec_status = 0

    if "exitCode" not in event["detail"]["containers"][0]:
        batch_exec_status = 1
    elif event["detail"]["containers"][0]["exitCode"] != 0:
        batch_exec_status = 1

    batch_name = event["detail"]["taskDefinitionArn"].split('/')[1].split(':')[0]
    result_status = lambda_metric("stg_hoge_batch.batch_exec_status", batch_exec_status, tags=['hogehoge', 'category:batch:'+batch_name])

    return batch_name+':exitCode '+str(batch_exec_status)

Lambdaのソースコードに関してはちょうどMRのコメントで修正箇所が大量に出され、それを今直している最中なのですが、処理内容的にはこんな感じです。

LambdaのLayer機能があるのですが、Datadogが公式でDatadog Layerを用意してくれていて、それを使うとメトリクスの送信(lambda_metric())が簡単に行えるので超便利です。いい時代。

exitCode以外でやっていることは、実行されたバッチの名前を取得してメトリクスのtagsに付けています。これは通知の時に表示させるのに使うためです。 バッチの名前はタスク定義のARNから抜き出しています。

参考

docs.datadoghq.com

Datadog

以下のようにメトリクスとアラートの設定をします。

↓ 3 Set alert conditionでメトリクスが1(>0)だった時にアラートが飛ぶように設定しています。

f:id:reiichii:20190630130527p:plain

f:id:reiichii:20190630130230p:plain

2 Define the metricで「Multi Alert」の設定を行なっています。これを行うと個別のアラートを発生させることができます。今回のケースだと category:batch:[[バッチ名]] のようになっているので、アラートのタイトルにバッチ名が表示されるようになります。

f:id:reiichii:20190630131932p:plain

没にした案

CloudWatch Logsのログストリームからメトリクスを作成してアラートを飛ばす方法

当初はCloudWatch Logsで各バッチが吐き出すログから、「Error」文字などでフィルターをかけてカスタムメトリクスを作成し、それを検知したらLambdaからアラートを飛ばす、というようなことを考えていました。ただこれだとバッチが5種類あってエラーログが様々なので個別に設定しなければならないことが多くなってしまうこと、そもそもタスクが起動しなかったらエラーが検知されないからアラート設定として不完全であること、といったような理由から没にしました。

本当はログのパターンが統一されているべきなんでしょうけど。

ともあれECSのタスク実行の確認をするだけなら、Logを見るよりイベントストリームの情報を確認するやり方の方が良かったなという確信はあります。

AWS SNSを挟むか

アラートの飛ばし方を調べる中で、CloudWatch EventsやAlermからSNSに送って、そこからLambdaに送るような設定も見ました。通知先が複数あったり、中身によってフィルタリングした上で通知先を決めるみたいなことをするのであればLambdaで実装するよりSNSを利用した方が良さそうでしたが、今回は通知先は一つしかないのとメトリクスの加工をする必要があったのでSNSは不要と判断しました。

最初はSNSでexitCodeが0か0以外かでSlackに通知を流すことをやろうとしていたのですが、結局それはできなかったな。。

ECSバッチを実行する方のCloduWatch Eventsのアラート

上記のアラート設定は、ECSタスクスケジューリングの失敗はカバーしていません。そもそもバッチが起動しなかったらイベントストリームは発行されません。なのでバッチを実行するCloduWatch Eventsがコケた時もアラートを出す設定を別で用意する必要があります。

ただ今回の私のケースだとDatadogの方でCloudWatch Eventsの失敗が検知されたらアラートが飛ぶように一括で設定してあるので、ルールを作った時点でそちらに含まれる形になります。Datadog良き。

おわりに

ECSのイベントストリームを知らなかったから、exitCodeでアラートを出すやり方を見つけるまでに苦労しました。

今回の設定を通して(実は苦手意識を持っていた) Datadogとも少し仲良くなれた気がします。もっと手足のように使いこなせるようになりたい。

yieldとは

はじめに

先日webスクレイピングを実装していた時に初めてyieldを使う機会がありました。

パッとググると概要は出てくるのですが使用例などがなかなかピンと来なかったので、これを機会にきちんと理解しておこうと思い立った次第です。

yieldを知るまでの道のり

今回調べていて、やっぱり説明するのも理解するのも一筋縄では行かないなという印象でした。

なので今回のブログは以下の流れでお送りします。

yieldの動作

ざっくりと説明すると、 returnが値を返してfunctionを終了させるものに対して、 yieldはreturnと同じように値を返した後、そのyieldの続きから処理を続行させることができます。

内部的な動作

↑のような説明は動作の部分を説明しただけなので、"yieldとは"といった目的の部分を説明するのには当てはまりません。(returnの処理を続行させないverという目的で作られたわけではないということ)

yieldはジェネレーターを作っています。 ジェネレータとはリストや辞書などイテレーターの一種です。他の一般的なイテレーターとの違いは「定義された時点で要素数に限りがあるか」だと言えます。

イテレーターは hoge = [1,2,3] のように中身が決まっているもので、このhogeをまとめてfor文に引き渡して回して一つ一つの要素に処理をするのに対して、ジェネレータは定義した時点では要素の中身は決めることができなく、for文で要素に対して処理を行う段階で初めて要素が確定される形です。「遅延評価」などというみたいです。

「定義段階で要素が確定しないリストってなんだよ」と思う人もいるかもしれません。 その一例がスクレイピングしたい対象のwebページだったりします。「そのページのリンクを3階層までクローリングする」といった処理を実行させる時、実行させるまで中身の数は確定しませんよね。

使用例(webスクレイピング実装したときの話)

以下のコードは私がwebスクレイピング入門した時に書いたもので、好きなアーティストのブログをクローリングして、写真があればあるだけローカルPCにDLするものになっています。

class MimorinSpider(scrapy.Spider):
    ''' 略 '''

    def parse(self, response):
        # そのページの画像を取得
        yield self.parse_items(response)

        # 総ページ数を取得
        ''' 略 '''
        # 総ページ分回してurlを生成
        if self.current_page <= int(total_page):
            ''' 略 '''
            next_url = 'https://lineblog.me/mimori_suzuko/?p=' + str(self.current_page)
            # 次のページへのリクエストを実行する
            yield scrapy.Request(next_url, callback=self.parse)


    def parse_items(self, response):
        item = MimorinItem()
        item['image_urls'] = []

        for image in response.css('img.pict::attr(src)').extract():
            item['image_urls'].append(image)

        return item

プログラム的に重要なポイントは二つです。

  • 画像取得: scrapyが実行されたら、そのページのブログの画像を取得して、ImagePipelineに渡す(ImagePipelineの中で渡した画像をローカルに保存したりといった、取得した画像に対する処理を行ってくれるため。超便利だった)
    • → ImagePipelineにitemを渡すために def parse() 内で return item をする必要がある
  • ページ遷移: そのページのスクレイピングが終わったら次のページに進む。進める限り処理を続けるようにする。
    • → 次のページにクローリングを進めるためには、次のページのurlをリクエストするメソッドを入れて def parse() を実行させないといけない。return item で処理を終わらせられない

という要件があります。

ここではyieldは二ヶ所で使われています。

  • yield self.parse_items(response)
    • 一ヶ所はページの画像を取得するメソッドを呼び出すところです。ここでyieldを指定して値を返す処理を入れないと、itemがPipelineに渡されず、画像のローカル保存が行われません。
  • yield scrapy.Request(next_url, callback=self.parse)
    • もう一ヶ所は次のページへのリクエストを実行するscrapy.Request()を実行する部分です。yieldを使うことでスクレイピングを実行する時に呼び出されるdef parseは終わらずに、その中で次のページに対するdef parseが実行され続けています。

以下は完全に余談で、ちなみに私の詰まっていたところ

  • 🤔「(yield self.parse_items(response)に対して)どうしてここにyieldを定義しないといけなかったんだっけ」
    • → yieldがジェネレーターであるというところに意識が行き過ぎてそもそも値をreturnするものであることを忘れていた
  • 🤔「(yield scrapy.Request(next_url, callback=self.parse)に対して、最初yieldを書かないでいて、なんで次のページのスクレイピングが取得されないのかがわからなかった)」
    • → yieldは実行された時点で処理を中断し再開させるもの。yieldを書かなかったコードはscrapy.Request()を実行して処理を正常終了。当然scrapy.Request()で呼ばれた子parse()も親メソッドの終了に合わせて終了する😇

自分の場合は、この部分の意味を理解したことでyieldの使い方や役割についてイメージが湧くようになりました。

その他のユースケースと参考資料

参考にした書籍や記事では以下のようなユースケースで説明されていることが多かったです。

  • メモリの負担を和らげるために使う。一度に大きなリストを返すとメモリを多く消費してしまうため負担がかかる。yieldで小分けに実装することで負担を抑える。
    • → ジェネレーターを生成し、for文でそれを実行させて要素に対して実行させるといった使い方がやっぱり多いらしい。

webスクレイピングのコードを書いている時、当初自分は「ページは限られているのだから一度urlを全ページ分スクレイピングし切った後、リストに入れて全部まとめてImagePipelineに渡せばいい」と考えていたのですが、これはアンチパターンのど真ん中だったわけです...

参考資料:

yieldのまとめ

  • functionの中の処理を中断し、値を返して、また続きから再実行させる ≠ return
  • 素数が未確定のジェネレーターを定義し、都度処理を実行させる

おわりに

本や記事を読んでいても、webスクレイピングをしなければyieldについて今以上にピンときていなかったかもしれません。

またyield fromの書き方についてや、more_itertoolsのようなモジュールについては今回は手を伸ばしきれませんでした。