ecspresso handbookをZennで公開しました

激動の2020年もいよいよ押し詰まってきましたが、皆様いかがお過ごしでしょうか。

今年は ecspresso advent calendar 2020 というひとりアドベントカレンダーを唐突に思いついて始めてしまったのですが、なんとか完走することができました。

拙作の OSS である Amazon ECS 用のデプロイツール ecspresso の基本的な使い方から応用編、コマンドリファレンス、設計思想までを一通り書ききることができたので、せっかくなので Zenn で本としてまとめることにしました。

zenn.dev

9万字弱とそこそこのボリュームになり、書くのも大変だったしなあということで、500円の有料本としています。

内容自体はアドベントカレンダーに書いたものを再編集(章立ての変更と校正など)したものなので、特に新しい情報が入っているわけではないですが、もしよろしかったらお買い求め頂けると大変ありがたいです。売れると開発にもやる気が出ます(重要)。冬休みのお供にいかがでしょうか!

ecspresso の今後ですが、CloudFormation との連携 (OutputとExportの論理名を指定して物理名を解決できる) の実装の目処が付いたので、年明けぐらいにリリースできるといいなと思っております。

それでは、皆様良いお年を (よろしければ ecspresso handbook と一緒に) お迎えください。

Amazon Lightsail Container を Mackerel で監視する

Mackerel Advent Calendar 2020 8日目の参加記事です。

2020年11月に、Amazon Lightsail Container というサービスがリリースされました。

dev.classmethod.jp

最低月額 $7 で、コンテナを気軽に(ロードバランサー込みで!) Amazon Lightsail 上にデプロイできるというものです。普通に AWS で Fargate のコンテナとロードバランサー(ALB)を動かすと最低でも$25/月程度はかかってしまうので、これはお手軽でよさそうですよね。

ということで、この Lightsail Container を Mackerel で監視してみましょう。

Lightsail Container の実体は?

なにはともあれ、Ligtsail Container に環境変数を出力する Web アプリケーションをデプロイして素性を探ってみます。環境変数JSON で出力する HTTP サーバを起動してくれる便利な phamviet/printenv という Public Image があるので、これを適当に Lightsail Container にデプロイしてアクセスしてみると、以下のようなレスポンスが得られました。

{
  "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/v2/credentials/03aed4fb-11f7-4c84-967a-34389ff48412",
  "AWS_DEFAULT_REGION": "ap-northeast-1",
  "AWS_EXECUTION_ENV": "AWS_ECS_FARGATE",
  "AWS_REGION": "ap-northeast-1",
  "ECS_CONTAINER_METADATA_URI": "http://169.254.170.2/v3/3715f73c-a351-4df9-a923-02163d4df095",
  "ECS_CONTAINER_METADATA_URI_V4": "http://169.254.170.2/v4/3715f73c-a351-4df9-a923-02163d4df095",
  "HOME": "/root",
  "HOSTNAME": "ip-172-26-26-31.ap-northeast-1.compute.internal",
  "NODE_VERSION": "10.16.3",
  "PATH": "/usr/src/app/node_modules/.bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "PWD": "/usr/src/app",
  "SHLVL": "1",
  "YARN_VERSION": "1.17.3"
}

"AWS_EXECUTION_ENV": "AWS_ECS_FARGATE" なるほどですね。

Lightsail Container = Fargate を簡単に使えるようにいい感じにしてくれているなにか、のようです(あくまで推測ですよ?)

ということは普通に mackerel-container-agent で ECS / Fargate を監視するのと同じ方法でいけそうですよね。サイドカーを追加してみましょう。

mackerel-container-agent のサイドカーを追加する

公式のドキュメントをもとに、以下のような設定でコンテナを追加しました Amazon ECSにmackerel-container-agentをセットアップする

f:id:sfujiwara:20201205022844p:plain

  • image: mackerel/mackerel-container-agent:v0.5.0
  • environment
    • MACKEREL_APIKEY: *************
    • MACKEREL_CONTAINER_PLATFORM: ecs

デプロイしてしばらく待ってみると…

f:id:sfujiwara:20201205022918p:plain

普通に Fargate task として認識され、同様のメトリックが得られました。よかったですね。

[PR] デプロイツール sailtrim 開発中

現在、fujiwara/sailtrim という、Lightsail Container 用のデプロイツールを開発しています。

まだまだ荒削りですが、ECSのデプロイツール ecspresso や Lambda のデプロイツール lambroll と同じような使い勝手で Lightsail Container をコード管理してデプロイできるツールです。よろしくどうぞ。

ECS task definition を Jsonnet で生成する

Amazon ECS でタスクの構成定義に使う task definition をリポジトリで管理する場合、JSON 形式で取り扱うことが多いと思います。

ECS デプロイツールである ecspresso でも、task definition は環境変数等を展開できる記法があるものの、基本的には JSON ファイルとして扱っています。

これは AWS コンソールで作成した task definition を aws describe-task-definition や ecspresso init コマンドでファイルとして取り出して利用することを前提としているためで、これを YAML や別の形式で扱うことは考えていません。

ところで、実際に ECS でそれなりのサービスを運用すると、task definition JSON の管理で悩むことが増えてきました。

  • JSON は人間が編集するのに便利ではない
    • コメントが書けない、配列末尾の , の有無で余分な diff が発生、key をいちいち " " で括るのが面倒…
  • サイドカーなどがだいたい同じで、一部だけ異なる task definition を複数運用する場面がある
    • 複雑な構造の一部が異なるような場合、環境変数展開ではカバーできないので、コピペせざるを得ない
  • タスク内の各コンテナで共通で埋め込みたい要素がそこそこあるが、揃えて変更するコストが高い
    • environment 要素などが典型

例として、nginx にサイドカーとして mackerel-container-agent を追加した task definition が以下のようにあるとします。

{
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx:latest",
      "environment": [
        {
          "name": "AWS_REGION",
          "value": "ap-northeast-1"
        },
        {
          "name": "TZ",
          "value": "Asia/Tokyo"
        }
      ],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ]
    },
    {
      "name": "mackerel-container-agent",
      "image": "mackerel/mackerel-container-agent:v0.4.0",
      "environment": [
        {
          "name": "AWS_REGION",
          "value": "ap-northeast-1"
        },
        {
          "name": "TZ",
          "value": "Asia/Tokyo"
        },
        {
          "name": "MACKEREL_CONTAINER_PLATFORM",
          "value": "ecs"
        }
      ],
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "secrets": [
        {
          "name": "MACKEREL_APIKEY",
          "valueFrom": "/MACKEREL_APIKEY"
        }
      ]
    }
  ],
  "cpu": "256",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "family": "ecspresso-test",
  "memory": "512",
  "networkMode": "awsvpc",
  "requiresCompatibilities": [
    "EC2",
    "FARGATE"
  ],
  "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskRole"
}

environment には AWS_REGION と TZ が共通で設定されています (mackerel-container-agent には追加で MACKEREL_CONTAINER_PLATFORM)。

共通で設定したい環境変数に変更がある場合、nginx と mackerel-container-agent の両方のコンテナ定義を変更する必要があります。この例ではタスク内に 2コンテナしかないですが、実際に稼働するタスクでは多数のサイドカーがあったりして、人手で漏れなく変更するのは大変ダルいことになります。

Jsonnet で JSON を生成する

JSON をテンプレートから生成する処理系として、Jsonnet というものがあります。これを使って task definition JSON を生成して、辛さが軽減できないか試してみました。

jsonnet.org

qiita.com

共通要素の括りだし

まず environment 要素の共通部分を抜き出してファイルにします。

[
    {
        "name": "AWS_REGION",
        "value": "ap-northeast-1"
    },
    {
        "name": "TZ",
        "value": "Asia/Tokyo"
    }
]

このファイルを envs.libsonnet として保存して、taskdef.json をコピーして taskdef.jsonnet を以下のように用意します。もとの taskdef.json をちょっといじるだけです。

  • local envs = import 'envs.libsonnet'; を追加
  • "environment": envs, として import した値を展開
# taskdef.jsonnet
local envs = import 'envs.libsonnet'; # envs テンプレートの読み込み
{
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "nginx:latest",
      "environment": envs, # 読み込んだ envs をここに展開
      "essential": true,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ]
    },
    {
      "name": "mackerel-container-agent",
      "image": "mackerel/mackerel-container-agent:v0.4.0",
      "environment": envs + [  # envs 配列への追加
        {
          "name": "MACKEREL_CONTAINER_PLATFORM",
          "value": "ecs"
        }
      ],
      "essential": false,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecspresso-test",
          "awslogs-region": "ap-northeast-1",
        }
      },
      "secrets": [
        {
          "name": "MACKEREL_APIKEY",
          "valueFrom": "/MACKEREL_APIKEY"
        }
      ]
    }
  ],
  "cpu": "256",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "family": "ecspresso-test",
  "memory": "512",
  "networkMode": "awsvpc",
  "requiresCompatibilities": [
    "EC2",
    "FARGATE"
  ],
  "taskRoleArn": "arn:aws:iam::123456789012:role/ecsTaskRole"
}

これを jsonnet コマンドで処理すると、taskdef.json と同一のものが生成されます。

$ jsonnet taskdef.jsonnet | head
{
   "containerDefinitions": [
      {
...

$ # 差分確認
$ diff -w <(jsonnet taskdef.jsonnet | jq --sort-keys .) <(cat taskdef.json | jq --sort-keys .)

JSON より扱いやすい記法への変換

Jsonnet は JSON の superset なので JSON 記法そのままでも扱えますが、jsonnetfmt コマンドを通すと素の JSON よりも人間が扱いやすい Jsonnet の記法に変換してくれます。

$ jsonnetfmt envs.libsonnet
[
  {
    name: 'AWS_REGION',
    value: 'ap-northeast-1',
  },
  {
    name: 'TZ',
    value: 'Asia/Tokyo',
  },
]

key のクォートが不要だったり、配列やオブジェクト末尾の , が許可されたりと、すっきり書けるようになります。

コンテナ定義の共通化

タスク内に複数あるコンテナ定義をテンプレートで共通化してみます。各コンテナの共通要素を定義したファイルを用意して container.libsonnet というファイルに保存します。

# container.libsonnet
local envs = import 'envs.libsonnet';
{
  essential: true,
  environment: envs,
  logConfiguration: {
    logDriver: 'awslogs',
    options: {
      'awslogs-group': 'ecspresso-test',
      'awslogs-region': 'ap-northeast-1',
    },
  },
}

このコンテナ定義テンプレートを使って、nginx コンテナの定義を生成する jsonnet は次のようになります。 import したコンテナ定義テンプレートに、必要な要素を上書き(追加)するだけです。

# nginx.libsonnet
local container = import 'container.libsonnet';
container {
  name: 'nginx',
  image: 'nginx:latest',
  portMappings: [
    {
      containerPort: 80,
      hostPort: 80,
      protocol: 'tcp',
    },
  ],
}

同様に mackerel-container-agent のコンテナ定義も container 定義テンプレートから生成します。environment は配列要素への追加になるため、container.environment + [ ] という形で定義しています。

# mackerel.libsonnet
local container = import 'container.libsonnet';
container {
  name: 'mackerel-container-agent',
  image: 'mackerel/mackerel-container-agent:v0.4.0',
  environment: container.environment + [
    {
      name: 'MACKEREL_CONTAINER_PLATFORM',
      value: 'ecs',
    },
  ],
  secrets: [
    {
      name: 'MACKEREL_APIKEY',
      valueFrom: '/MACKEREL_APIKEY',
    },
  ],
}

この各コンテナ定義を import して、taskdef を生成する jsonnet は以下のようになりました。すっきり!

# taskdef.jsonnet
local mackerel = import 'mackerel.libsonnet';
local nginx = import 'nginx.libsonnet';
{
  containerDefinitions: [
    nginx,
    mackerel,
  ],
  cpu: '256',
  executionRoleArn: 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole',
  family: 'ecspresso-test',
  memory: '512',
  networkMode: 'awsvpc',
  requiresCompatibilities: [
    'EC2',
    'FARGATE',
  ],
  taskRoleArn: 'arn:aws:iam::123456789012:role/ecsTaskRole',
}

この taskdef.jsonnet を jsonnet コマンドでレンダリングすると、もとの taskdef.json と同一内容の JSON が出力されます。

更に似たような task definition を増やしたい場合には taskdef の定義テンプレートを用意して共通項目を括りだし、それを基にそれぞれの JSON を生成してもいいでしょう。

まとめ

取り扱いが煩雑になりがちな JSON による ECS task definition を Jsonnet というテンプレート言語で生成してみました。

Jsonnet のサイトを見るとあまりにいろいろな機能があってびっくりしますが、まずは既存 JSON の共通部分を括り出す程度のテンプレートとして最小限で使うのをお勧めします。

Jsonnet は独自関数定義なども使えるので、やり過ぎると読めなくなる可能性はありそうです。が、生成されるものは plain な JSON なので、最悪読めなくなっても生成後の JSON からリファクタリングをやり直すことは可能です。

デプロイに ecspresso を利用する場合は、デプロイ前に ecspresso diff コマンドを使用すると、現在稼働中の task との差分があるかを検出できます。Jsonnet でリファクタリングした結果意図せず壊していないかの検知に便利だと思います。

ecspresso v1 をリリースします(v0.99.x をリリースしているのでお試しください)

おかげさまで各所でご利用頂いている(模様の) Amazon ECS デプロイツール ecspresso ですが、リリースしてからもうすぐ3年になりますし、ここで一部非互換の修正を入れた v1 を出そうとしています。

非互換な点は少ないのでほとんどの方にはそのまま使って頂けると思いますが、下記を確認した上で v0.99.x を試して頂いて、もし何か問題があればフィードバックを頂けると幸いです。

(2020-10-30 追記) v1.0.0 をリリース済です。

github.com

(2020-09-25 23:00 追記) Homebrew で 0.99.x がインストールできるようになりました。 tap ecspresso@0.99 by mashiike · Pull Request #9 · kayac/homebrew-tap · GitHub

$ brew install kayac/tap/ecspresso@0.99

既に v0.x がインストールされている場合は conflict するので、一旦 unlink して link し直してください。

$ brew unlink ecspresso
$ brew link ecspresso@0.99

v0.x に戻したい場合は逆操作です。

$ brew unlink ecspresso@0.99
$ brew link ecspresso

v1 リリース後は @0.99 は削除されるので、単に brew upgrade ecspresso で v1 へ更新されるようになる予定です。 (追記ここまで)

v1 で非互換になる点

deploy --update-service (true) がデフォルトになります

従来 、service 定義ファイルを更新した場合にそれを ECS service に反映するには、ecspresso deploy --update-service とオプションを指定する必要がありました。

v1 では --update-service はデフォルトで true になります。v0.x と同様、service 定義に変更があっても deploy 時に適用したくない場合、deploy --no-update-service として明示する必要があります。

次に説明する、service 定義の desiredCount が反映されるようになる件との組み合わせで、意図しない挙動になる場合がありますのでご注意ください。

service 定義に記述された desiredCount を解釈するようになります

Does not `desiredCount` field work? · Issue #131 · kayac/ecspresso · GitHub

これまでは service 定義に desiredCount 属性が記述されていても無視されていました。v1 では service 定義に desiredCount 属性が存在して null ではない数値が記載されている場合に限り、deploy 時にその値を使用するようになります。deploy --tasks N が指定されていればそちらが優先されます。

ecspress deploy 実行時に実際に ECS task に指定される desiredCount は、以下の優先順位で決定されます。

  1. --tasks で指定された値
  2. service 定義に desiredCount で定義されている値
  3. (現在の service の desiredCount と変更なし)

これまで通りの挙動を期待する場合は、service 定義に desiredCount が指定されていないことを確認してください。

service / task 定義ファイルの path を config ファイルからの相対で解釈するようになります

feature request: resolve definition files path relatively from config.yml · Issue #140 · kayac/ecspresso · GitHub

例えば現在 config.yaml に以下のように記述されていている場合、

service_definition: ecs-service-def.json
task_definition: ecs-task-def.json

ecs-(service|task)-def.json は ecspresso 実行時のカレントディレクトリに存在している必要があります。

v1 では config ファイルからの相対で解釈するようになるため、

$ ecspresso --config path/to/config.yml

として実行した場合は ecs-(service|task)-def.json は ecspresso 実行時のカレントディレクトリではなく、config.yml と同一ディレクトリ (path/to) に存在しているものとして実行します。


以上、よろしくお願いします!

ISUCON10予選に参加して不通過でした

Webアプリケーションパフォーマンスチューニングコンテスト ISUCON http://isucon.net/、記念すべき10回大会の予選に参加して、あと3チーム100点弱の差で不通過に終わりました。悔しい!

チームメイトは会社の同僚の @acidlemon (ISUCON 3の出題、ISUCON 4, 7 のチームメイト), @mackee_w (macopy, 実はチームを組んだのは初) です。

序盤から中盤

  • ベンチを回して MySQL が重いねー(いつものことだ) と把握
  • acidlemon
    • estate の範囲検索になっているカラムを = 条件で取れるように 0〜49999 -> 0, 50000〜100000 -> 1 のようにクラスわけ(verify がたまにコケるのを解消できず取り込めず
    • interpolateParams=true (server side prepare を無効)
    • select * をやめて id だけまず取得して where id in
  • macopy
    • featureがLIKE検索で複数条件に一致をやっていたのを別テーブルに逃して縦持ち
    • index 追加
    • geo 検索でポリゴンに座標が含まれているかを MySQL ではなく Go 側で判断 (後回しにしたら試行もできず…
  • fujiwara
    • OS は Ubuntu と予告されていたので各種ツール (alp, percona-toolkit, notify_slack, netdataなど) のセットアップ用 mitamae を流して初期設定
    • MySQL を軽くチューニング
    • nginx.conf で bot に 503 を返すように正規表現
    • Regexp::Assemble でまとめてこんなのを "(I(SUCON(bot((-Mobile)?|-Image\/)|FeedSeeker(Beta)?|Coffee)|supider((-image)?\+)?)|crawler \(https:\/\/isucon\.invalid\/(support\/faq\/|help\/jp\/)|Mediapartners-ISUCON|isubot))"

今回はとにかく検索が重く (単発ではそこまで重くないけど10000〜30000行読む必要がある数十msのクエリが大量に飛ぶ)、MySQL の負荷を index では軽減しきれず、序盤〜中盤まで全くスコアを伸ばせず苦しい展開でした。初期スコアは500程度、いろいろやっても600ぐらい。

大抵の ISUCON では序盤にいくつかの定番の手を打つと上がるみたいなジャブが効く感じがあるのですが、今回は全然ダメージを与えられている気がしないという、アプリケーションとしては比較的単純な作りなのに心を折りに来る良問でしたね…

また途中、自分が用意した deploy.sh がカレントディレクトリからでないと正常に動かないという雑な作りなため、macopy がデプロイしたつもりができてない (のでスコアが何も変わらない) というので revert を連発することになり、1時間ほど消費するというトラブルもありました。

中盤から終盤

時間的にはもう終盤に近く、まったくスコアが伸びないと心が先に折れるので、力押しでスコアを出そうと MySQLレプリケーションを組んで3台構成にしました。

  • isucon1: nginx, app, mysql (writer)
  • isucon2: app, mysql (reader)
  • isucon3: app, mysql (reader)

として、isucon2, 3 にレプリケーションを設定。アプリケーションからは 127.0.0.1 に接続する reader 用の db 接続を用意し、Search 系のクエリはそちらに切り替え。レプリ遅延回避のため多少小ネタを入れます。

  • ベンチの最初に叩かれる /initialize でデータが初期化されるため、その遅延が解消するまで initialize のレスポンスを sleep(10) して粘る
  • /initialize が終わると整合性チェックが User-Agent: isucon-verify で飛んでくるので、このアクセスはすべて 1 台目で処理するように nginx を設定
    • 1台目は reader 接続も 127.0.0.1 (=writer) を向いているので確実に遅延なしで読めます

これで1000を超えるぐらいまでなんとか伸ばして一息入れました。

更に、データの更新はほぼないので MySQL のクエリキャッシュが効きそう、ということで有効化して1300ぐらい? CPU が若干使い切れてなさそうだったので db.SetMaxOpenConns(10)db.SetMaxOpenConns(30) にしたらまた少し。slowlog はもう見ないことにして無効化。

あと残り2時間程度、ここからもう MySQL そのものをどうにかするのは無理筋と判断して、acidlemon と macopy に、それぞれ chair と estate の検索をオンメモリにする実装を並行で取りかかってもらいました。

先に取りこんだ estate の検索は fail したので切り戻し、残り30分を切ったところで chair の検索オンメモリ化の投入になんとか成功して 1800 越え、再起動試験をして残り10分で 2074、競技中の最高スコアをマークしたところで打ち止めとなりました。

終戦

最終的には、一般枠予選通過まで84点差、あと3チーム上回れば…という惜しいところで敗戦しました。

アクセスログみる限り、追試でのベンチ走行では競技中の最終ベンチよりも10%ほど多くレスポンスを返せていたので、あと何度かベンチを回していたらブレで100点程度上がっていたかも知れず、通過の可能性も僅かながらあったはずなので、粘りが足りなかったですね。ISUCON 6 の時は最後の1分を切ったところでベンチを開始して、それが fail せずに通ったことで予選を通過できていたのに…

ISUCON10 オンライン予選 全てのチームのスコア(参考値) : ISUCON公式Blog

検索のオンメモリ化ももう1,2時間早く判断して取りかかっていれば、estate, chair の両方を動かすことができた可能性が高く、これは判断の遅れです。そもそも予選通過ボーダー付近は毎年混み合うので、そこで揉み合っていた時点でベンチのスコアのブレの運要素を排除しきれず、つまりは実力不足でしたね。

おわりに

ISUCONでは予選通過できたのが7まで (8は出題、9, 10 が予選落ち) ということで、2年連続で本選の地を踏めず、悔しい思いです。今年も楽しい問題でした。運営の皆様、トラブルで開始が遅れたにもかかわらず、当日中に結果の発表まで大変お疲れ様でした!

GitHub Actions の Composite Run Steps で ecspresso / lambroll をインストールする action を作った

要約

GitHub Actions に Amazon ECS デプロイツール ecspressoAWS Lambda デプロイツール lambroll を簡単にインストール action を用意しました。

steps で以下の指定をするだけです。(Linux 環境専用です)

- uses: kayac/ecspresso@v0
- uses: fujiwara/lambroll@v0

もうちょっと詳しく

これまで GitHub Actions の action は基本的に Node で書く必要があったのですが、やりたいことはバイナリをダウンロードして展開してコピーするだけなのになんで Node を書かないといけないのか……(Linux以外の環境を考えると仕方ないとはいえ)、と面倒くささが先に立って、これまで action を作らないままでした。

が、先日 Composite Run Steps が使えるようになり、これは要するに自分の steps に shell script を書くかのように定義した action.yml を置いておくだけでよいという、顧客が本当に欲しかったものだったのでした。最高。

github.blog

ソースはこれだけで、見ての通り単に curl で落として展開して install するだけ、です。

https://github.com/kayac/ecspresso/blob/master/action.yml

inputs:
  version:
    description: "A version of ecspresso"
    default: "v0.17.3"
runs:
  using: "composite"
  steps:
    - run: |
        FILENAME=ecspresso-${{ inputs.version }}-linux-amd64
        cd /tmp
        curl -sLO https://github.com/kayac/ecspresso/releases/download/${{ inputs.version }}/${FILENAME}.zip
        unzip ${FILENAME}.zip
        sudo install ${FILENAME} /usr/local/bin/ecspresso
        rm -f ${FILENAME} ${FILENAME}.zip
      shell: bash

使う側は他の action と同様、step で uses: kayac/ecspresso@v0 などとするだけです。便利。

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: kayac/ecspresso@v0
        with:
          version: v0.17.3
      - run: |
          ecspresso deploy --config config.yaml

どうぞご利用ください。

FirehoseのHTTP配信機能でMackerelにメトリックを投稿する

先日、Amazon Kinesis Data Firehose が任意の HTTP エンドポイントに対しての配信機能をサポートしました。

aws.amazon.com

従来の S3 / Redshift / Elasticsearch Service などのマネージドなリソースに対してデータを配信する機能に加えて、自分で作った HTTP(s) のエンドポイントに対しても Firehose からデータを投げ込んでくれる機能です。

Amazon Kinesis Data Firehose now supports data delivery to Datadog

既に Datadog や New Relic へもマネージドでデータを配信できていて、それらに送りたいデータはとりあえず Firehose に流しておけば、実際の送信やバッファリングやリトライは Firehose が面倒を見てくれるという嬉しい状態ですね。

で、ここで思ったのは「Mackerelにも対応したい」

…ということで作ってみました。

github.com

Firehose に Mackerel のサービスメトリックを流して、この HTTP endpoint を設定すると Mackerel API に送信してくれるものです。

fujiwara/ridge を使うことで、ひとつのバイナリで単体での Go の http server としても動きますし、Lambda (API Gateway / ALB) としても動くようになっています。

動かしてみる

普通にビルドしてどこかのサーバーで動かしてもよいのですが、Firehose の配信先は https が必須で多少面倒かもしれません。ここではとりあえず Amazon API Gateway で動かすことにします。Go が必要です。

$ GOARCH=amd64 GOOS=linux make
$ ROLE_ARN=arn:aws:iam::123456789012:role/lambda make deploy

Linux (Lambda) 用のバイナリをビルドして、適当な IAM Role (権限は AWSLambdaBasicExecution のみで十分です) を指定して fujiwara/lambroll でデプロイできる Makefile がサンプルとして入っています。function の定義は以下で、Role 以外に必要な設定は特にありません。

{
  "Description": "A PoC of Firehose HTTP endpoint for Mackerel",
  "FunctionName": "firehose-http-endpoint-for-mackerel",
  "Handler": "firehose-http-endpoint-for-mackerel",
  "MemorySize": 128,
  "Role": "{{ must_env `ROLE_ARN` }}",
  "Runtime": "go1.x",
  "Timeout": 30,
  "TracingConfig": {
    "Mode": "PassThrough"
  }
}

デプロイした Lambda を使う API Gateway の HTTP Integration を設定します。Route は $default をこの関数に向けるだけです。

f:id:sfujiwara:20200803104848p:plain
API Gateway HTTP integration

Firehose の Destination 設定は以下の通りにしてください。

  • HTTP endpoint URL: 作成した API Gateway の URL。path は /service (https://xxxx/service
  • Content encoding: Disabled
  • Access key: Mackerel の API Key
  • Parameters:
    • service: メトリックを投稿する Mackerel のサービス名
  • Buffer interval: 何秒でも動作はしますがラグが少ない方がよいので 60

Firehose に投稿したいサービスメトリックを送ります。

$ aws firehose put-record --delivery-stream-name mackerel-endpoint \
    --record "Data=Base64にしたサービスメトリックの値"

値の形式は JSON {"name":"metric.name","time":1596382129,"value":27759} とテキスト形式(mackrel plugin が出力するタブ区切りのもの) metric.name 27759 1596382129 に対応しています。

あとは Firehose が60秒ごとにまとめて API Gateway を叩いて来るタイミングで、Mackerel API へ送信されるはずです。

うれしいこと

直接 Mackerel API を叩いたり mkr コマンドなどで送信するのと比べて、Firehose を介することで嬉しいことはなにかというと…

  • Mackerel のメンテナンス時、送信失敗時のリトライ、バッファリングを Firehose に任せられる
    • 最大 7200 秒までリトライ可能
    • 失敗したデータは S3 にバックアップも可能
  • Mackerel API key を送信元に配る必要がない
    • Firehose 側に設定されていれば OK
    • 送信元には Firehose に対してのアクセス権が与えられていればよい
  • mkr / mackerel-agent などのツールがなくても aws-cliSDK のみで送信できる

ぐらいでしょうか。mackerel-agent を EC2 で動かしていれば再送やバッファリングは自動で行われますが、直接 API を叩いて値を送信する場合は失敗時のケアを自分でやらないとメトリックが欠落してしまいます。特に Lambda などから送信する場合は状態を維持するのが面倒なので、欠落の心配が減るのがいいですね。

ご要望

Firehose http endpoint を使って Mackerel へメトリックを送るコードを書いてみましたが、これはあくまで PoC ということで、Mackerel の公式で http endpoint がサポートされてほしいなと思います。そうすると Firehose の設定で公式 endpoint に向けるだけで済みますし、API を直接アクセスする以外のメトリック送信手段として AWS のユーザーには嬉しいんじゃないでしょうか。ご検討いただければ幸いです。