Kubernetes Admission Webhookでリソース作成を自在にコントロールする

この記事はJX通信社 Advent Calendar 2019の1日目の記事です。

こんにちは、SREのたっち(TatchNicolas)です。

先日開催されたKubeCon 2019でもセッションで紹介されていた、Admission Webhooksについて書きます。

Admission WebhooksとはKubernetesリソースを操作(CREATE/UPDATE)する時に、作成や変更の内容をチェックしたり、書き換えたりすることができる機能です。

TL;DR

  • Admission Webhooksを使うと、あらゆるKubernetesリソースの操作をトリガーに 「チェック(Validation)」「変更(Mutation)」 を行える
    • 身近なところでは、Istioでサイドカーのauto-injectionで使われています
  • どの種類のリソースにどんな操作をするときにWebhookを呼ぶかは細かく条件指定ができる
  • 最低限自分で実装すべきは非常にシンプルなWebサーバのみで、簡単に試すことができる
  • 実運用するには冪等性、可用性、適用範囲など考えることが色々

サンプルコードは以下に置いてあります。

github.com

やってみる

f:id:TatchNicolas:20191129002303p:plain
Kubernetes Admission Webhooksの位置付け

今回は、以下の二種類のAdmission Webhooksを作ってみます。

  • 「Pod作成時にenv ラベルにdev,stg,prdのいずれかが付与されているかチェックする、なかったらリソース作成を拒否する」 = ValidatingWebhook
  • 「Pod作成時にリソースの名前にある接頭辞を付与する」 = MutatingWebhook

Kubernetes環境はAdmission Webhooksのbetaが取れている v1.16を使います。今回はMinikube v1.4.0を利用しました。*1

Webhookを書いてServiceとして動かす

では早速、Webhookを実装しましょう。要は、kube-apiserverからJSONを受け取ってJSONを返すことができれば良いので言語やフレームワークは何でも良いです。 前述の通り今回は手軽さ最優先でFlaskを使います。

最低限のValidationは非常にシンプルで、以下のようなレスポンスを返すだけです。allowedtrue であればValidationは成功し、false であれば失敗します。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "<value from request.uid>",
    "allowed": false
  }
}

実際に書いたコードはこちら。今回はstatus フィールドにメッセージを入れてみました。たとえばkubectlによるPod作成時にValidationに失敗した場合、このメッセージが返ってきます

これをDeploymentとしてデプロイし、Serviceとして叩けるようにします。*2

今回は同じクラスタ内から利用するのでClusterIPだけで十分です。 真面目にやるなら、目的の異なるWebhookは別にデプロイしたほうが良いと思いますが、今回はお試しということで一つのアプリケーションとして書いてしまいました。

Admission WebhooksはHTTPS必須なので、Podを作る前にまずcfsslを使ってオレオレ証明書を作成します。

$ cat <<EOF | cfssl genkey - | cfssljson -bare server
{
  "hosts": [
    "sample-admission-webhook.default.svc"
  ],
  "CN": "sample-admission-webhook.default.svc",
  "key": {
    "algo": "ecdsa",
    "size": 256
  }
}
EOF
2019/11/27 18:28:38 [INFO] generate received request
2019/11/27 18:28:38 [INFO] received CSR
2019/11/27 18:28:38 [INFO] generating key: ecdsa-256
2019/11/27 18:28:38 [INFO] encoded CSR

server-key.pemserver.csr が作成されます。続いて、Webhookに持たせるための証明書を作ります。

$ cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadata:
  name: sample-admission-webhook.default
spec:
  request: $(cat server.csr | base64 | tr -d '\n')
  usages:
  - digital signature
  - key encipherment
  - server auth
EOF
certificatesigningrequest.certificates.k8s.io/sample-admission-webhook.default created
$ kubectl certificate approve sample-admission-webhook.default
certificatesigningrequest.certificates.k8s.io/sample-admission-webhook.default approved

base64エンコードしてダウンロードし、そこからSecretリソースを作ります。

$ kubectl get csr sample-admission-webhook.default -o jsonpath='{.status.certificate}' | base64 --decode > server.crt
$ kubectl create secret tls --save-config sample-admission-webhook-secret --key server-key.pem --cert server.crt
secret/sample-admission-webhook-secret created

最後に、このSecretを使ってDeploymentを作成し、それに紐づくServiceも作成します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-admission-webhook
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-admission-webhook
  template:
    metadata:
      labels:
        app: sample-admission-webhook
    spec:
      containers:
      - name: app
        image: tatchnicolas/sample-admission-webhook:0.10
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        volumeMounts:
        - name: tls
          mountPath: /tls
        command: ["gunicorn"]
        args: 
        - "main:app"
        - "--bind"
        - "0.0.0.0:8080"
        - "--access-logfile"
        - "-"
        - "--certfile"
        - "/tls/tls.crt"
        - "--keyfile"
        - "/tls/tls.key"
      volumes:
      - name: tls
        secret:
          secretName: sample-admission-webhook-secret
---
apiVersion: v1
kind: Service
metadata:
  name: sample-admission-webhook
spec:
  selector:
    app: sample-admission-webhook
  ports:
  - port: 443
    protocol: TCP
    targetPort: 8080

Webhookの設定を登録する

さて、それではいよいよAdmission Webhooksの設定を登録します。必要なManifestは至ってシンプルです。

caBundle フィールドにてクライアント(=kube-apiserver)がWebhookへリクエストを送る際に信用する証明書を指定することができます。

まず、ValidatingWebhookのほうから登録してみましょう。

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: "sample-validating-webhook"
webhooks:
- name: "sample-validating-webhook.hoge.fuga.local"
  failurePolicy: Fail
  rules:
  - apiGroups: [""]
    operations: ["CREATE"]
    apiVersions: ["v1"]
    resources: ["pods"]
    scope: "Namespaced"
  clientConfig:
    caBundle: <server.crtの中身をbase64エンコードして貼る>
    service:
      namespace: default
      name: sample-admission-webhook
      path: /validate
  admissionReviewVersions: ["v1", "v1beta1"]
  timeoutSeconds: 5
  sideEffects: None

続いてMutatingWebhookも同様に

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: "sample-mutating-webhook"
webhooks:
- name: "sample-mutating-webhook.hoge.fuga.local"
  failurePolicy: Fail
  rules:
  - apiGroups: [""]
    operations: ["CREATE"]
    apiVersions: ["v1"]
    resources: ["pods"]
    scope: "Namespaced"
  clientConfig:
    caBundle: <server.crtの中身をbase64エンコードして貼る>
    service:
      namespace: default
      name: sample-admission-webhook
      path: /mutate
  admissionReviewVersions: ["v1", "v1beta1"]
  timeoutSeconds: 5
  sideEffects: None

Manifestを適用したら、リソースが作成されたことを確認しましょう

$ kubectl get validatingwebhookconfigurations
NAME                        CREATED AT
sample-validating-webhook   2019-11-28T05:58:28Z
$ kubectl get mutatingwebhookconfigurations
NAME                      CREATED AT
sample-mutating-webhook   2019-11-28T05:39:28Z

動きを確認する

では、実際に動作確認してみましょう。

apiVersion: v1
kind: Pod
metadata:
  name: webhook-demo
  labels:
    env: stg
spec:
  containers:
  - name: nginx
    image: nginx

こんなManifestからPodを作ってみましょう。

$ kubectl apply -f pod.yaml
pod/dummy-prefix-webhook-demo created
$ kubectl get pod
NAME                                        READY   STATUS              RESTARTS   AGE
dummy-suffix-webhook-demo                   0/1     ContainerCreating   0          2s
sample-admission-webhook-7ddfc8c784-6pf7k   1/1     Running             0          6m6s

Pythonで書いた処理の通り、名前が書き換わっています。

Validationのほうもチェックするために、env ラベルが dev,stg,prd のいずれにも一致しないPodを作ってみましょう。

apiVersion: v1
kind: Pod
metadata:
  name: validation-demo
  labels:
    env: willberejected
spec:
  containers:
  - name: nginx
    image: nginx

設定した通りのエラーメッセージが返ってきており、Podも作られていないことが確認できます。

$ kubectl apply -f invalid_pod.yaml
Error from server: error when creating "invalid_pod.yaml": admission webhook "sample-validating-webhook.hoge.fuga.local"
denied the request: Required label env is not set or its value is invalid

はまったところ

  • k8s v1.14.8で利用できるバージョン(admissionregistration.k8s.io/v1beta)では、リクエスト先 portオプションを指定するとUnknown fieldって怒られる(サンプルには書いてあるのに...)
  • Podに対してWebhookを書くと、DeploymentやReplicaset経由で作られるPodについてもValidation/Mutationの対象となってしまうので、Webhookを開発中にそのDeploymentを更新したらWebhook用のPod自体がValidationに引っかかってコケてしまった
    • あとは kube-system ネームスペースのPodに対して悪さをされると困るので、GitHubのサンプルコードでは namespaceSelector objectSelector を使って対象を絞っています

感想

オレオレ証明書の準備等の手間はかかりましたが、Webhook自体は非常にシンプルに作ることができました。Kubernetes自体にコンパイルされるAdmission controllerを作るのは非常にハードルが高いですが、こうして疎結合に機能を拡張できるのはとても魅力的ですね。

Webhook自体の可用性が低いと条件に合致するリソースの操作全体に影響してしまったり、MutatingWebhookConfigurationのほうは「変更を加える」という性質上、複数回呼ばれてしまった場合に備えて処理を冪等にする必要があったり、本番運用の際は慎重になる必要があります。

しかし、HelmやKustomize等を使うのに比べてより根っこに近いところでガバナンスを効かせることができるので、Admission Webhooksは非常に強力な機能だと思います。上手に使って、リリースや運用の負荷を下げていきましょう。

参考資料

*1:執筆時点で最新のMinikubeにはregistry-credsのaddonに不具合があり、Docker Desktopではk8sバージョンがv1.16いなかったため

*2:ServiceのほかにURLによる指定でクラスタ外のWebhookを呼ぶこともできます

potatotips #66 (iOS/Android開発Tips共有会) に登壇してきました

f:id:numatch-jx:20191120000627j:plainこんにちは、Androidエンジニアのぬまっちです。(@nuMatch)
半年ほど前からFlutterエンジニアとしてもデビューしてるので、今はAndroid/Flutter (iOS) エンジニアという感じでしょうか。

あっという間に冬が近づいてきました。 夏にはFlutter布教の為、社内でFlutterモブプログラミング回をしたのが懐かしくなります。 tech.jxpress.net

今回はyappliさんで開催されたpotatotips #66 (iOS/Android開発Tips共有会)にてLTしてきましたので、その紹介をさせて下さい。

potatotips.connpass.com

登壇前

今回、自分がLTで紹介したかったメインテーマは「怖くないよFlutter」でした。
iOS/Android開発におけるTipsがメインの勉強会なので、
「Flutterって最近良く聞くけど、まだ片方のプラットフォームしか触ったことないな」
って方を対象に、Androidエンジニア視点でFlutterに入門したらクロスプラットフォームが身近になった、という紹介をしたいと思いました。

という事でタイトルは
「Android エンジニアが Flutterに入門して驚いたこと3点」
です!

今回、久しぶりの登壇(JXエンジニアになってからは初)という事もあって準備・練習の段階から社内エンジニアに手伝って頂きました。
この場を借りて感謝!です!

f:id:numatch-jx:20191120000627j:plain

LT

実際に登壇で使った資料はこちらになります。

speakerdeck.com

怖くないよ flutter doctor

Flutter入門する方がほぼ必ず叩くだろうコマンド「$ flutter doctor」について紹介しました。

FlutterSDKのインストールが完了し、PATHが通っていれば flutter doctor コマンドを実行することが出来ます。
flutter doctor コマンドは、開発環境がFlutterで開発できるか診断してくれる便利な機能です。

で、試しに叩いてみるとかなり怒られると思います。
AndroidまたはiOSどちらかのプラットフォームのみの開発者なら、Android Studio / Xcodeが片方しか入ってない事も多いかと思いますが、両方揃えないといけません。
特にAndroidエンジニアの方はiOSの開発環境が整っていないと思います。
HomebrewやCocoaPodsも足りないと怒られますが、ちゃんと順番に何をすればいいのか指示してくれるので怖がる必要はありません。

言われた通りにコマンドを叩いていくとFlutter開発の為の環境が作られるはずです。
自分はAndroid StudioでiOSのシミュレータが立ち上がるのを体験した時、軽く感動を覚えました。

入れやすいよ Plugin

自分が声を大にして言いたい、

「FlutterにおけるPluginの導入にしやすさ」

について語らせて頂きました!
何故ここまでピックアップするかというと、理由が大きく2つあります。

  1. 検索のしやすさ

使いたいPluginを探したい時は、まずpub.dev/flutterで検索しましょう。( Flutter packages

LTではAndroidにおける値の永続化の入門であるSharedPreferenceを例にとって紹介しました。

(紹介したPluginはこちらになります↓)

shared_preferences | Flutter Package

検索窓でSharedPreferenceと入力すれば、現在導入可能なSharedPreference用Pluginの一覧が出てくると思います。
似たような名前のPluginが沢山あると思いますが、選ぶポイントとしては

  • 「flutter.dev」の公式であるかどうか
  • 「performance score」が100、または100または100に近いかどうか

が大事かと。

  1. クロスプラットフォームのしやすさ

導入したいPluginが決まったら「pubspec.yaml」に追加記述しましょう。
記述したらflutter pub getコマンドを叩く事をお忘れなく。

基本的にはこれだけです。

PluginによりますがSharedPreference用Pluginではプラットフォーム固有の設定は不要で、導入の準備が完了になります。
AndroidのManifestファイルへの記述や、iOSのPodfileへの書き込みいらずになる事が多いのは大変ありがたいです。
「pubspec.yaml」で両プラットフォーム対応が完結するのはFlutterの大きな強みになると思います。

Flutter と Firebaseの相性の良さ

クロスプラットフォーム入門としてFlutterとFirebaseは抜群の相性を誇ります。
Flutter公式で専用のPluginを用意されていますし、
(他にも各FrebasePackage用のPluginが用意されています。)

ドキュメントは日本語が用意されています。
Flutter アプリに Firebase を追加する  |  Firebase

今回のLTでは時間が足りずに詳細は語れませんでしたが、

  • Firebase Authentication
  • Firebase Cloud Messaging

については後日、JX通信社のアドベントカレンダーにて紹介したいなと思っています!

qiita.com

@nuMatchの担当する回をお待ち頂ければ幸いです!

登壇してみて

振り返ってみると、今回のLTでFlutterのTipsを紹介していたのが自分だけだったのが印象的でした。 また、Twitterで参加者の方の感想で
「聞きやすかった」、「Flutterはまだ入門したことないので、定期的に紹介されるのは嬉しい」
との声を見かけたので、準備した甲斐があったなと嬉しく思いました。

今回はFlutterにおけるクロスプラットフォーム入門についての登壇でしたが、次は実際にプロトアプリ開発で出会った事例を元にした内容でLT出来ればと思います!

開発合宿@箱根湯本

f:id:jxpress:20191008062416j:plain

はじめに

こんにちは!CTOの柳です。今回のブログでは、9月某日に行われたJX通信社恒例の開発合宿について紹介したいと思います。

これまで、JX通信社では年1回のペースで開発合宿を実施してきました。

tech.jxpress.net

www.wantedly.com

合宿のテーマ

毎年、弊社の開発合宿では、さまざまなテーマに取り組んできました。社内環境・ツールの整備、CI/CDの導入、去年はビジネスサイドのメンバーを巻き込んだデータ分析講習会や普段できない業務改善について取り組みました。

今年はCTOである私が幹事を引き受けたので、職権乱用メンバーを説得し、より尖ったテーマに取り組みました。ずばり「rebuild」です!

組織とプロダクトが大きくなっていく中、普段の業務では慣れた技術スタックばかり使うようになってしまいます。世の中に面白い技術が続々と登場しても、中々触れる機会がありません。

そこで、合宿では「新しい技術スタックで、既存プロダクト1つを0から作り直すこと」(= rebuild)にチャレンジしました。

合宿までの準備

実りのある合宿にするために、準備は怠りません。

合宿参加者を開発領域(フロントエンド、バックエンド、アプリ、etc..)ごとにチームを分け、それぞれの領域の技術選定や設計をしてもらいました。

また、定期的に、合宿参加者が全員集まり、

  • 目指すべきMVP(Minimum Viable Product)はどういったものか

  • 合宿中に取り組むPBI(Product Backlog Item)は何か

について話し、意識のすり合わせを行いました。

箱根温泉「ホテルおかだ」

今年は箱根湯本にある「ホテルおかだ」さんにお邪魔しました。宿を決めるにあたり、株式会社Loco Partnersさんと株式会社FiNCさんのブログを参考にしました!この場を借りて御礼申し上げます。

relux 開発合宿 in 箱根湯本 - wootan's diary

FiNC 開発合宿 in 箱根! | FiNC Developers Blog

合宿の様子

f:id:jxpress:20191008062706j:plain

ロマンスカーに乗っていざ出発。

f:id:jxpress:20191008063003j:plain f:id:jxpress:20191008063015j:plain f:id:jxpress:20191008063027j:plain

宿到着。

f:id:jxpress:20191008063158j:plain

到着後、インフラチームから、デプロイ方法について説明を受けました。

弊社では、コード管理にGitLab、デプロイにGitLab CIを、普段は使っていますが、今回はGitHub Teamと、発表されたばかりのGitHub Actionsを使って、CI/CDのパイプラインを組んでみました。

f:id:jxpress:20191008145143j:plain

開発中。

f:id:jxpress:20191008063721j:plain

開発中。

f:id:jxpress:20191008145154j:plain

開発中。

f:id:jxpress:20191008063744j:plain

開発中。

f:id:jxpress:20191008063800j:plain

開発中。

f:id:jxpress:20191008145131j:plain

開発中。

f:id:jxpress:20191008063908j:plain

夕食・朝食は共にバイキング。種類豊富でした。

f:id:jxpress:20191008064133j:plain

ご飯から戻ってまた開発。深夜遅くまで続きました。

f:id:jxpress:20191008064148j:plain

開発中。

f:id:jxpress:20191008064212j:plain

そろそろ限界。

f:id:jxpress:20191008064313j:plain

合宿最終日は会議室を借りて、成果発表会を開きました。

f:id:jxpress:20191008145121j:plain

合宿の成果

最終日ギリギリまで粘ってなんとか、バックエンドからWebフロント・アプリまで疎通でき、3泊4日の日程内にプロダクトのrebuildができました!

WebフロントエンドはTypeScriptを、アプリはFlutterを使い、BFFとしてGraphQLのAPIを作りました。 バックエンドは、普段使ってるAWSではなくGCPを採用し、GCPの各種サービス(GAE、Cloud Functions、Cloud Pub/Sub)やKubernetes / Knativeにチャレンジしました。

まとめ

今回の合宿では、新しい技術にガッツリ取り組めました。また、3泊4日という期限内に、全員で1つのプロダクトの0→1開発に取り組めたことは、チームとして得難い経験になったと思います。

最後に、我々を快く受け入れてくれた「ホテルおかだ」さんに感謝!

www.hotel-okada.co.jp

JX通信社は「PyConJP 2019」にゴールドスポンサーとして協賛しています

VPoE の小笠原です。今年の PyConJP 2019 、JX通信社はゴールドスポンサーとして協賛しています! f:id:yamitzky:20190916100113j:plain

9/16(月)-9/17(火)の2日間、スポンサーブースに出展しています。ブースでは、Python で作った AI ピンポンゲーム大会を開催しています。

参加方法は、GitHub からフォークしてゲーム AI を実装するだけです! 高得点者には素敵な景品もありますので、ぜひ遊びに来てください! f:id:yamitzky:20190916101006j:plain

github.com

キャンペーン参加方法

PyConJP 2019 JX通信社ブースにてどなたでもご参加いただけます.
参加者は右上のボタンからこのレポジトリを Fork してください.
Fork したレポジトリに下記の手順で Pong チームを実装してください.
実装が終わったら弊社ブースにお越しください.
GitHub のアカウント名でエントリーできます.
プログラムでなく人力での参戦も可能です。ゲームの腕に自身がある方はぜひ参戦ください.
キャンペーンの終了は PyConJP のスポンサーブース終了時間に準じます.

こちらのピンポンゲームは社内勉強会で作ったものです。エンジニアブログもあわせてよろしくお願いいたします!

tech.jxpress.net

CloudWatch Eventsを使ってECSタスクを監視するツールをSAMで作る

こんにちは、SREエンジニアのたっち(@TatchNicolas)です。これまではPythonによるサーバサイド開発を担当していましたが、SREエンジニアとしてプロダクトを横断して安定性・パフォーマンス改善に取り組む担当になりました。

ヘルスチェックしにくいバッチ系のECSタスク

JX通信社では、ワークロードのほとんどをAWS LambdaまたはECSの上で動かしています。 Webサービス自体やAPIなどはALBやECSサービスのヘルスチェックを使って異常を検知したり、StatusCakeのようなサービスを使って外形監視をすることができます。

しかし、エンドポイントを持たないバッチ処理のようなタスクは、タスク自体が起動に失敗したり、途中で失敗した場合に上記の方法では検知することができません。

そこで、AWSの強力な機能の一つであるCloudWatch Eventsを使って、うまく動作しなかったECSタスクがあった場合にSlackへ通知できる仕組みをつくってみました。

CloudWatch Eventsとは

CloudWatch Eventsを使うと、イベントパターンを定義することでAWSリソースの変更をリアルタイムに取得してAWSのアクションをトリガーすることができます。 AWSリソースの変更だけでなく、スケジュールを組んで処理の自動化をすることも出来ます。 定期的に行いたいちょっとした処理をLambdaで書いてCloudWatch Eventsのcronで呼ぶ、みたいなことは実践していらっしゃる方も多いかと思います。

今回はイベントパターンを使ってECSタスクの状態変化を検知し、LambdaでSlackにアラートを投げてみたいと思います。

どうせやるならSAMで

監視のツール自体が秘伝のタレになってしまったり、依存しているAWSリソースがわからなくなってしまっては健全ではありません。

Lambdaの管理ツールはApex(記事執筆時点でメンテナンスがすでに終了)、Serverless Frameworkなど色々ありますが、今回はCloudWatch Eventsのイベントパターンもまとめて定義してしまいたいので、CloudFormationと親和性の高いSAMを採用します。

構成とコード

簡単な構成図とコードは以下のとおりです。

f:id:TatchNicolas:20190815142005p:plain
構成図

app/main.py

import json
import os

import requests

SLACK_WEBHOOK = os.environ['SLACK_WEBHOOK']
AWS_REGION = os.environ['AWS_REGION']


def lambda_handler(event, context):

    if event.get('source') != 'aws.ecs':
        print('Invalid event')
        print(event)
        return

    detail = event['detail']
    header = 'ECSタスクが異常終了または起動に失敗しました\n'

    cluster_name = detail['clusterArn'].split('/')[1]
    task_id = detail['taskArn'].split('/')[1]
    task_def = detail['taskDefinitionArn'].split('/')[1]

    url = f'https://{AWS_REGION}.console.aws.amazon.com/ecs/home?region={AWS_REGION}#/clusters/'\
        f'{cluster_name}/tasks/{task_id}/details'

    text = '\n'.join([
        header,
        f'クラスタ: *{cluster_name}*',
        f'タスク定義: *{task_def}*',
        url,
        '```',
        json.dumps(detail, indent=2),
        '```',
    ])
    attachments = {
        'text': text,
        'color': 'danger'
    }

    payload = {
        'attachments': [attachments]
    }
    requests.post(SLACK_WEBHOOK, json=payload)

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam_python37

  Sample SAM Template for Python 3.7

Globals:
  Function:
    Timeout: 3

Resources:
  ECSMonitor:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: app/
      Handler: main.lambda_handler
      Runtime: python3.7
      Environment:
        Variables:
          SLACK_WEBHOOK: https://hooks.slack.com/services/xxxxxxxx/xxxxxxxx/xxxxxxxxxxxxxxxxxxxxxxx
      Events:
        CommandNotFound:
          Type: CloudWatchEvent
          Properties:
            Pattern:
              !Sub |
              {
                "detail-type": [
                  "ECS Task State Change"
                ],
                "source": [
                  "aws.ecs"
                ],
                "detail": {
                  "clusterArn": [
                    "arn:aws:ecs:ap-northeast-1:123456789012:cluster/cluster-to-monitor-a",
                    "arn:aws:ecs:ap-northeast-1:123456789012:cluster/cluster-to-monitor-b"
                  ],
                  "containers": {
                    "exitCode": [
                      1,
                      127
                    ]
                  }
                }
              }
        TaskFailedToStart:
          Type: CloudWatchEvent
          Properties:
            Pattern:
              !Sub |
                {
                  "source": [
                    "aws.ecs"
                  ],
                  "detail-type": [
                    "ECS Task State Change"
                  ],
                  "detail": {
                    "clusterArn": [
                        "arn:aws:ecs:ap-northeast-1:123456789012:cluster/cluster-to-monitor-a",
                        "arn:aws:ecs:ap-northeast-1:123456789012:cluster/cluster-to-monitor-b"
                    ],
                    "stopCode": [
                      "TaskFailedToStart"
                    ]
                  }
                }

template.yaml のほうも、リージョン名やAWSアカウントの部分も疑似パラメータにできそうですが 面倒くさかった わかりやすさを優先してハードコードしちゃってます。

実装していてハマったのは、CloudFormationでイベントパターンを定義しているところです。

最近はCloudFormationをJSONで書くケースは稀かもしれませんが、イベントパターンはYAMLで書いた場合に意図せぬ挙動をすることがあります。

YAMLではクオートをつけない文字列は数値として扱われるのが基本ですが、コンテナの終了コードをイベントパターンに定義する場合、デプロイした際に数値型が文字列型に変換されてしまいます。

つまり、以下のようなイベントパターン定義がCloudFormation中にあっても、

# (略)
EventPattern:
  source:
  - aws.ecs
  detail-type:
  - ECS Task State Change
  detail:
    clusterArn:
    - arn:aws:ecs:ap-northeast-1:123456789012:cluster/samplestack
    containers:
      exitCode:
      # クォートで囲っていないので数値として扱われることを期待
      - 1
      - 127
# (略)

このようなJSONとして、exitCode の値が数値型でなく文字列型としてCloudWatch Eventsに設定されてしまいます。

{
  "detail-type": [
    "ECS Task State Change"
  ],
  "source": [
    "aws.ecs"
  ],
  "detail": {
    "clusterArn": [
      "arn:aws:ecs:ap-northeast-1:123456789012:cluster/samplestack"
    ],
    "containers": {
      "exitCode": [
        "1",
        "127"
      ]
    }
  }
}

すると、CloudWatchが発行するイベントJSONではexitCodeは数値型で渡されるので、イベントパターン定義と比較されるときに 127"127"イコールにはならず、イベント発火の条件にマッチしない結果になってしまいます。 しかし、テンプレート自体をJSONで書くのもツラいので、前述のサンプルの通りイベントパターンの部分のみをJSONで書くようにして回避できました。

また、記事執筆時点でワイルドカードや否定の条件には対応していないようでした。 そのため、「終了コードが0以外」のような条件を指定するのはやや難しいようです。

上記の例ではCloudWatch側でコンテナの終了コードを条件として指定し、Lambdaの発火をコントロールしました。 コストを受け入れられるのであればイベントパターンの条件をクラスタARNのみにして、Lambda側でイベントで送られてくるJSONの内容を元にSlack通知するかどうかを判断させるパターンでも良いかもしれません。

まとめ

今回はすでに利用中のタスク定義に手を加えずにタスクの成功/失敗を取得するような簡単な監視の仕組みを作ってみました。

次回は、サイドカーコンテナを使って(=タスク定義に手を加えて)より詳細な監視ができる仕組みを作ってみようと思います。

参考資料

  • Amazon CloudWatch Events イベントパターン
  • AWS CLI test-event-pattern
    • イベントのJSONとイベントパターンのJSONを入力として、マッチするかどうかをチェックできます。
    • 実施にイベントを起こしてAWS上で検証すると時間もお金もかかってしまうので、取りたいイベントのJSONを保存しておいて手元で検証するのが良いでしょう。