ツナワタリマイライフ

日常ネタから技術ネタ、音楽ネタまで何でも書きます。

Roomba で掃除が完了したら IFTTT 経由で Pixe.la で草を生やす

かくかくしかじか部屋を綺麗にする意識が高まり、Roomba e5 を買いました。

で、かくかくしかじかそういう話を社内 LT でして、この時健康やお金の話も一緒にしていて、定量的に評価して目標値を決めるっていう SRE 的なアプローチを、部屋の綺麗さでやれたらいいよねって話題があって

f:id:take_she12:20210322185938p:plain:w300

まぁ結局 Clean map 機能は e5 では使えなかったのでどのみち面積はわからないんですが、床には物を置かないという前提で、スケジュール実行していれば、毎日実行はしてくれるので、その結果をとりあえず可視化してみようと思い、pixe.la を試してみました。

pixe.la

ご存知の方も多いと思います。GitHub の草を API 経由ではやしてくれるサービスです。

たくさん事例があって楽しいですね。

github.com

Roomba が掃除完了したら IFTTT 経由で草を生やす

さて本題。とても簡単かつこちらの記事で紹介されているものとほぼ同じです。

t.co

アカウント作成

README にある通りですが

curl -X POST https://pixe.la/v1/users -d '{"token":"secret", "username":"chaspy", "agreeTermsOfService":"yes", "notMinor":"yes"}'
{"message":"Success. Let's visit https://pixe.la/@chaspy , it is your profile page!","isSuccess":true}

Graph 作成

curl -X POST https://pixe.la/v1/users/chaspy/graphs -H 'X-USER-TOKEN:secret' -d '{"id":"roomba","name":"roomba","unit":"commit","type":"int","color":"shibafu"}'
{"message":"Success.","isSuccess":true}

Webhook 作成

curl -X POST https://pixe.la/v1/users/chaspy/webhooks -H 'X-USER-TOKEN:secret' -d '{"graphID":"roomba","type":"increment"}'
{"message":"Success.","isSuccess":true,"webhookHash":"1234567890123456789012345678901234567890"}

草を生やす

上で返ってきた webhookHash を使います。

curl -X POST https://pixe.la/v1/users/chaspy/webhooks/1234567890123456789012345678901234567890
{"message":"Success.","isSuccess":true}

これまでで草を生やす準備は完了。

IFTTT で iRobot 連携する

iRobot アカウントでログインします。

ifttt.com

IFTTT で掃除が完了したら Webhook を発行する

実際に作ったのはこれなんですが、

ifttt.com

If は iRobot で JobComplete Then で上記の webhook の設定をします。

f:id:take_she12:20210322191708p:plain:w300

これで、実際に roomba をリモートで動かしてみて、完了したら IFTTT が実行されました。

f:id:take_she12:20210322192128p:plain:w300

ちゃんと草も生えた!

pixe.la

今後

  • 生やした草を表示する Static site をホストする
  • Withings の体重計とも連動できそうなので、体重増減を草生やす
  • Withings の体重増減のグラフもパブリックにしたいので調べる
  • ジムに行ったら、運動したら草生やす

などなど、Pixe.la 使い倒していこうと思います!最高のサービスを a-know さんありがとうございました!

Cloud Native 読書会第5回 NGINX Ingress Controller に参加した

した。

zenn.dev

コードを読むことは重要である。これはここ数年思っていることである。そしてある程度読めるようになった。コード、読めば読めるし、書けば書けるな、というところまではなってきた。しかし、解決したい課題がない状態でコードを読むということはあまり経験がない。しかし優秀なソフトウェアエンジニアはみんなきっとそうしているのであろう。そういうものが体験できるかな、と思い参加した。あと Nginx Ingress Controller は(あんまり関わってないけど)弊社でも動いているので意味は大きい。

主催の Wantedly 南さんや田中さんとはもう何度も話している間なので、安心して参加できた。白金台のオフィスに遊びにいくみたいな気持ちで。自己紹介を軽くして、entry point からさっそく読んでいく。そしてさすが南さん、これが早い。このスピードで読んでいくのね。もちろん手元に Clone はしていたが、自分でコードジャンプや grep をしながら全体の話についていくのは無理だと早々に判断したので、手を止めて聞きに徹した。

そうすると面白くて、他人がコードを読むときの思考をトレースできて貴重な体験だった。コメントをちゃんと読んでるなあ、とか、メインの処理のあたりをつけて、それ以外は読み飛ばしているなあ、とか、ここがこうしているという仮説を立てて、その検証をするために別のコードを追っているなあ、とか。これは実はあまりできない経験なのではなかろうか。

今回参加者のほぼ全員(多分)が Nginx Ingress は使っていて、動作概要は知っていたようで、この前提がないと厳しいだろうなと感じたし、逆に前提がみんなあるからこそこのスピードで読み終えられたなと思った。エントリポイントで config を読み込んで nginx を起動している部分や、config の変更を検知して再起動をしている部分、そしてコアの部分には lua のコードが多く出てきて、nginx ingress を深く読み特には lua を読めないといけないのね、と(おそらく参加者全員が)驚いた。

みんなの感想を聞いてて思ったのは、基本挙動は知っているメンバーが揃っているとはいえ、Ingressyaml がだいたいこんなんだとして、Nginx Ingress Controller が Ingress を読んで、nginx.conf を生成して、nginx として動いているってのが基本的な動作ですよね。だいたいこういう流れになっていると(いったん)想像するとして、読んでいきましょう、でコードの該当部分をマッピングしていく、みたいにできると便利そうだと思った。どうにもテキストベースの GitHubパーマリンクと図をあらわすツールみたいなのをいい感じにあわせるのが難しいが。

あと今回は南さんの画面共有だったけど、VSCode LiveShare とかで見ていくとどんな感じかなーとも気になった。今日は〜の何行目!みたいなことを言ってたけど、そういうのも共有楽になったりするかもしれない。あとかめねこさんが言ってたブックマーク のエクステンションも使えばなおさら便利そう。一方 GitHub もコードジャンプできるし十分使えるので一長一短かも。自分は Go は Vim か Goland で読んでジャンプできるようにしています。

次回は ArgoCD。弊社でもバリバリ使っているし、使ってる人も多いので参加者も増えるんじゃないかな、と期待。次回は少し予習をしてきて、よりみんなをワクワクさせられるような場になるように貢献できたらいいなと思う。

南さん、田中さん、参加されたみなさんありがとうございました!

CircleCI の Insights API の結果を Prometheus 形式で Export する OSS 作った

作った。

github.com

シリーズものです。Prometheus Exporter としては7作目。

経緯

解決したい課題

所属している会社では monorepo を採用しており、大量の Build と Test と Deploy とその他いろいろの Job が超巨大ワークフローとして走る。

しかしまぁこれが遅い。改善を続けてきているがまだ遅い。そしてそれがどう遅い、何が遅いということが見えやすい状況とはいえない。

あと Flaky Test がぼちぼちあって、それによって脳死 Rerun が状態化している。よくない。

テストやビルドなんかは基本的にサービスを開発している Developer が詳しく、SRE が直接改善していくことはできないではないが、効果的とはいえない。

SRE としてはサービスの Observability と同じ構図で、Insights を得られる指標を提供し、その活用方法を組織にインストールして、それを Developer に活用してもらうということを CI でも取りたい。

CircleCI Insights では不足か

最初は多分うちの monorepo が裏のデータが大きすぎて GUI がうんともすんとも開かないみたいな状態で使い物にならなかったが、最近は十分活用できるレベルになっている。

API ではここで表示している値を取ってきてるだけなので、単に取り回しの問題で、Datadog に送ったほうがよりカスタマイズ性があがるという理由である。

サマリを把握する分には CircleCI Insights の GUI で十分である。

これまでやってきたこと

これまでも Insights API が出る前から Job や Job 内の Step ごとにかかった時間を Push で Datadog に送るなどしていた。

しかし活用が難しい。なぜか。それは前述したように、Sparse metrics になるから。

Push として metric を送る場合、それは Event 的なデータとなる。継続的な"状態"を示すデータではない。これは解析しづらい。データ量が不足するということと、データ量自体が得られるかどうかの頻度が metric によるからである。適当な間隔で Aggregation / Rollup したりなんたりの処理が必要で面倒である。

そういうわけで値自体はとれていたが活用には至っていなかった。

差分検知に関する問題

これは今回の件と関係するようで関係しないんだが、弊社では Build / Test の時間を短縮するために独自の差分検知の仕組みを使っている。

これは monorepo 内の各サービス(ディレクトリがトップレベルで切られている)の sha1Kubernetes の configmap に保存しており(ここに置くのが良いのかというのはある)ここと差があれば差分ありと判定されビルドやテストが走る。差分なしと判定されればビルトやテストはスキップされる。

...んだが、CircleCI の Workflow は Dynamic に構成することはできない。条件によってこのジョブを走る、走らせない、みたいなことができない。そのため、ジョブ自体は走るがすぐに halt するということをしている。

これによって何が問題かというと、前述する metric がスキップしたものとそうでないものが区別できない点にある。これは Insights の情報も同じである。

その点で GitHub Actions の Path filter は優位にあり、CircleCI も現状開発中のようだ。

どうするのか

大きく2点の対応が必要。

  1. ビルドやテストなど、傾向を追いたいものは Nightly で定期的に実行し、統計情報を安定的に取得する
  2. よりコントローラブルにするために Insights API の結果を Prometheus 形式で Export して Datadog に送る

今回作った OSS は 2 を解決するものであり、1 は別途行う必要がある。

CircleCI Insights は結構イケていて、統計情報を提供してくれる点が大きい。これまで自前でやっていた"Duration Second" という事実を Event 的に送っても活用は難しい。CircleCI Insights に統計情報の計算はお任せして、その推移を追って活用することができる。

統計情報というのは API のページを見てもらえばわかるが一定の Time Window における Job / Workflow の Success Rate や Duration Second の P95, median, min, max などのことである。

circleci.com

結果

いい感じ。

  • Workflow ごとの Success Rate

f:id:take_she12:20210217064247p:plain

  • Workflow ごとの Duration metrics, median

f:id:take_she12:20210217064253p:plain

  • Job ごとの Success Rate

f:id:take_she12:20210217064300p:plain

  • Job ごとの Duration metrics, median

f:id:take_she12:20210217064306p:plain

「やだ、、、私のテスト/ビルド、遅すぎ?」みたいなことがすぐわかって便利ですね。

まだこれらは差分検知の影響もあって確からしい値じゃないものもあるのですが、それでもそれなりに事実に近い結果は出ていそうです。

わかっていない点

CircleCI API の Rate Limit

rate exceeded のエラーをよくもらう。(後述するが API call 数が多いので)

これ結局いくつが上限なのかわかってない。

実際に動かすときは30分に1度実行するようにしていて、それだと大丈夫そう。

CircleCI Insights の計測間隔

metric を見る限り、last-7-days の time window にすると、24時間は同じ値を返し続けているように見える。その瞬間から24時間前を常に出しているわけではなく、1日に1度バッチ的にリフレッシュしているようだ。

last-90-days にした場合、GUI 上はグラフが週に一度しか変わってないように見えるので、もしかして API でも 7 days に一度しかこの値はリフレッシュされないのか?という点を気にしている。

f:id:take_she12:20210217064947p:plain

毎回瞬間ごとに計算する必要はないと思うが、せめて 24時間に一度は Time window が伸びても計算しなおしてほしい。表示が重くなるから GUI 上だけそうなっているのかどうかはわかっていない。

改善したい点

OSS のほうの話です。

Repository / Branch の持たせ方

現状はそれぞれ環境変数で持たせていて怒りのループぶんまわしを決めているので無駄がすごく多い。

例えば repo が "chaspy/hoge,chaspy/huga" で、branch が"develop,release,master" だとして、chaspy/hoge はこの3つの branch が存在するとして、chaspy/huga に release branch がなかったとしても API call してしまう。

この辺は yaml みたいな構造データで設定をいれるべきなんだろうなあと思っている。

repo: 
  - name: chaspy/hoge
    - branch
      - develop
      - master
  - name: chaspy/huga
    - branch
      - develop
      - release
      - master

Go の CircleCI SDK がない

これがあるがメンテされておらず、Ingiths API はもちろんサポートされてない。v1 まで。

github.com

v2 用のライブラリ書いてもいいかなーと思ったけどいったんパスした。このリポジトリ内でまず外部利用可能な感じにできたらしようと思う。

いまはなんかパッケの名前がそれっぽいがこれそうじゃなくて API call をラップした一連の処理群で切っている。

github.com

学んだこと

JSON-to-Go 便利

API Response 格納する構造体定義するのばりめんどいやんと思ったけどレスポンスペーストするだけでできてめっちゃ便利。

json.Unmarshal 便利

response を構造体につっこむの楽で便利。

               err = json.Unmarshal(body, &wfJobsInsight)
                if err != nil {
                    return []WorkflowJobsInsightWithRepo{}, fmt.Errorf("failed to parse response body. body %v, err %w", string(body), err)
                }

wfJobsInsight がその構造体。body は response body。

おわりに

AWS は go-sdk を使うだけって感じだったけど http response をそのまま扱ったので勉強になった。

うまく改善につなげられますように。

おまけ

いろいろ書いてデータ眺めて観察してわかった知見をまとめて話そうと思うよ。

AWS Config の Compliance の Capped Count を Prometheus 形式で Export する OSS 作った

作った。

github.com

AWS の Prometheus Exporter では4作目、Prometheus Exporter としては6作目。

blog.chaspy.me

blog.chaspy.me

blog.chaspy.me

blog.chaspy.me

blog.chaspy.me

なぜ作ったか

もともとのきっかけは AWS IAM で 2FA が有効になっていないユーザがいて、たまたま気づいたが定期的に検知、通知ができてないことから。

AWS Config と SeucrityHub は有効化していたので、mfa の rule は有効になっていた。

ちょっとまだこの違反時の Slack 通知はまだできていないんだが、ついでなのでこの数も送っちゃおう、となった。

使ってる API

describe compliance by config rule

CLI

docs.aws.amazon.com

go sdk

https://docs.aws.amazon.com/sdk-for-go/api/service/configservice/#ConfigService.DescribeComplianceByConfigRule

今回は特にシンプルで、この API 1つから帰ってくる Compliance by Config Rule を metric として送っていて、Value としては Capped Cound を出している。

https://docs.aws.amazon.com/sdk-for-go/api/service/configservice/#ComplianceByConfigRule

type ComplianceByConfigRule struct {

    // Indicates whether the AWS Config rule is compliant.
    Compliance *Compliance `type:"structure"`

    // The name of the AWS Config rule.
    ConfigRuleName *string `min:"1" type:"string"`
    // contains filtered or unexported fields
}
type Compliance struct {

    // The number of AWS resources or AWS Config rules that cause a result of NON_COMPLIANT,
    // up to a maximum number.
    ComplianceContributorCount *ComplianceContributorCount `type:"structure"`

    // Indicates whether an AWS resource or AWS Config rule is compliant.
    //
    // A resource is compliant if it complies with all of the AWS Config rules that
    // evaluate it. A resource is noncompliant if it does not comply with one or
    // more of these rules.
    //
    // A rule is compliant if all of the resources that the rule evaluates comply
    // with it. A rule is noncompliant if any of these resources do not comply.
    //
    // AWS Config returns the INSUFFICIENT_DATA value when no evaluation results
    // are available for the AWS resource or AWS Config rule.
    //
    // For the Compliance data type, AWS Config supports only COMPLIANT, NON_COMPLIANT,
    // and INSUFFICIENT_DATA values. AWS Config does not support the NOT_APPLICABLE
    // value for the Compliance data type.
    ComplianceType *string `type:"string" enum:"ComplianceType"`
    // contains filtered or unexported fields
}
type ComplianceContributorCount struct {

    // Indicates whether the maximum count is reached.
    CapExceeded *bool `type:"boolean"`

    // The number of AWS resources or AWS Config rules responsible for the current
    // compliance of the item.
    CappedCount *int64 `type:"integer"`
    // contains filtered or unexported fields
}

struct はこんな感じの階層構造になってる。

ComplianceByConfigRule
|__Compliance
   |__ComplianceContributorCount
     |__CapExceeded
     |__CappedCount
   |__ComplianceType
|__ConfigRuleName

ConfigRuleName が rule 名、ComplianceType が COMPLIANCE か NONCOMPLIANCE か。CapExceeded が、よくわかんないけどルール違反が 25 をこえたかどうか。Capped Count が違反してる数で、25以上の場合は25になる。

課題

Capped Count を Value として送っているので、そもそも ComplianceType が COMPLIANCE - ちゃんと遵守している場合、value が 0 となってしまい、どの rule が守られているのかという情報は送信されない。考えたがどうにもしょうがないような気もする。あと気にすべきは NONCOMPLIANCE なものの数なのでやむなしかなと。

COMPLIANCE の rule が全部で何個、NONCOMPLIANCE の rule が全部で何個かを出すなら別の API と別の metrics として export すれば良いと思うが、まぁいったんこれは別になくてもいいかなと思った。見たい気もするけどなくても良い。

結果

こんな感じ。

f:id:take_she12:20210213033141p:plain

AWS MFA の数だけ注目してみるとこんな感じ。7人もいるのでどげんかせんといかん。

f:id:take_she12:20210213033146p:plain

まぁルールを完璧にいきなりするのは難しいので少しずつ、できるところから。

次回

AWS 編は Cost が気になっているのだけれど、ちょっとおやすみして、CircleCI Insight に挑戦する予定。

AWS RDS の Max Connections を Prometheus 形式で Export する OSS を作って current connections / max connections 比率でアラートを出す

作った。

github.com

これは何か

RDS の設定値、max_connections は PostgreSQLMySQL ともにある。接続数がこの数値を超えるとエラーとなるため、この数値を超えないように監視できることが望ましい。

しかしこの max_connections、RDS の parameter group の value の1つであり、cloudwatch metrc に存在しない。そのため Datadog Integration でも取ることができない。

というわけでそれを取って Prometheus 形式で Export することにした。

なお、現在の接続数 database_connections は取れる。

docs.datadoghq.com

これとの比で、例えば9割になったら Alert みたいなことができるようになって便利。

また、新規サービスリリース前に Production Readiness Checklist でも DB の接続数大丈夫?みたいな項目があるが、それを減らすことができる。

結果

こんな感じ。まぁ Maxcon を可視化しても全然面白くはない。そうそう変わるもんではないしね。Datadog 上に metric として値が欲しかっただけである。

f:id:take_she12:20210209040027p:plain

アラート

ここで気づくが、aws.rds.database_connections はよしなにタグに値をつけてくれるが、この Exporter でもつけている、DB Instance Identifier と DB Instance Class の Key 名を合わせないと Alert が作れない。

雑に GitHub で調べると、付与されるタグはこのように決まっていることがわかる。

github.com

RDS auto_minor_version_upgrade、dbinstanceclass、dbclusteridentifier、dbinstanceidentifier、dbname、engine、engineversion、hostname、name、publicly_accessible、secondary_availability-zone

snake case かと思ったらそうでもなかったりしてポリシーがよくわからないが、とにかく同じ tag として扱いたければ dbinstanceclass dbclusteridentifier に変える必要があった。

タグさえ一致させればこんな風に欲しい値が手に入った。

f:id:take_she12:20210209044016p:plain

こんな感じで monitor を作る。

f:id:take_she12:20210209044531p:plain

いったん低めの8割で作った。

課題

Issue にあげているが、、、

Max Connection の値の確からし

一応 autora-postgresql の場合、デフォルトは以下のような計算式で max connections は決まる。

Aurora PostgreSQL: "LEAST({DBInstanceClassMemory/9531392},5000)"

ただ、この計算式がイマイチ実際の default と違うような予感がしているので、要検証だと思っている。

ちなみにこの default 値の場合の instance class に対する maxcon はこのように map を持っている。

github.com

   auroraPostgresMaxcon := map[string]int{
        "db.r4.large":    1600, // Memory  15.25 GB
        "db.r4.xlarge":   3200, // Memory  30.5  GB
        "db.r4.2xlarge":  5000, // Memory  61    GB
        "db.r4.4xlarge":  5000, // Memory 122    GB
        "db.r4.8xlarge":  5000, // Memory 244    GB
        "db.r4.16xlarge": 5000, // Memory 488    GB
        "db.r5.large":    1800, // Memory  16    GB
        "db.r5.xlarge":   3600, // Memory  32    GB
        "db.r5.2xlarge":  5000, // Memory  64    GB
        "db.r5.4xlarge":  5000, // Memory 128    GB
        "db.r5.8xlarge":  5000, // Memory 256    GB
        "db.r5.12xlarge": 5000, // Memory 384    GB
        "db.r5.16xlarge": 5000, // Memory 384    GB
        "db.r5.24xlarge": 5000, // Memory 768    GB
        "db.m4.large":    900,  // Memory   8    GB
        "db.m4.xlarge":   1800, // Memory  16    GB
        "db.m4.2xlarge":  3600, // Memory  32    GB
        "db.m4.4xlarge":  5000, // Memory  64    GB
        "db.m4.10xlarge": 5000, // Memory 160    GB
        "db.m4.16xlarge": 5000, // Memory 256    GB
        "db.m5.large":    900,  // Memory   8    GB
        "db.m5.xlarge":   1800, // Memory  16    GB
        "db.m5.2xlarge":  3600, // Memory  32    GB
        "db.m5.4xlarge":  5000, // Memory  64    GB
        "db.m5.8xlarge":  5000, // Memory 128    GB
        "db.m5.12xlarge": 5000, // Memory 192    GB
        "db.m5.16xlarge": 5000, // Memory 256    GB
        "db.m5.24xlarge": 5000, // Memory 384    GB
        "db.t2.micro":    125,  // Memory   1    GB
        "db.t2.small":    250,  // Memory   2    GB
        "db.t2.medium":   450,  // Memory   4    GB
        "db.t2.large":    900,  // Memory   8    GB
        "db.t2.xlarge":   1800, // Memory  16    GB
        "db.t2.2xlarge":  3600, // Memory  32    GB
        "db.t3.micro":    125,  // Memory   1    GB
        "db.t3.small":    250,  // Memory   2    GB
        "db.t3.medium":   450,  // Memory   4    GB
        "db.t3.large":    900,  // Memory   8    GB
        "db.t3.xlarge":   1800, // Memory  16    GB
        "db.t3.2xlarge":  3600, // Memory  32    GB
    }

これが多分ちょっとあやしいので確認したい。

RDS MySQL / Aurora MySQL サポート

現状していない。

やればできると思うが、こいつらの Default もまぁ結構キモい。特に Aurora MySQL。どうしてそうなったんだろう。

Aurora MySQL: "GREATEST({log(DBInstanceClassMemory/805306368)*45},{log(DBInstanceClassMemory/8187281408)*1000})"
RDS MySQL: {DBInstanceClassMemory/12582880}

正規表現マッチやってやれないことはないけど、log とかなんとか言ってるしマジ。まぁ結局 default の場合結果の値は static に持つのだけれども。

驚いたこと

max_connections は cluster parameter group と db parameter group 両方で設定できる

max connections は実は cluster parameter group と db parameter group 両方で設定できるのだが

あとに設定したほうが有効になるということ。知らんわ。

今回作った Exporter は DB Instance の Parameter Group を describe しているので、cluster parameter group にだけ設定しているパターンだと正しい値が取れないです。

Default はインスタンスクラスによって決まるが、それを静的な値で上書きできる

なんか全部 max はこれで決まるかと思ったけど、fixed value を入れることもできる。

そうしている場合はそちらが値に入ってるので、素直にそれを使うようになっている。

感想

Database Instance を for でぶんまわしたあとインスタンスごとに db parameter group を describe するので、loop の頻度によっては結構すぐに rate limit に達してしまう。DB Instance、いま動かしている環境だと 90 なんだけど、1分おきでたかだか3周とかで rate limit に達してたので、270 / 3min でアウトは厳しすぎない?あと rate limit 各サービスによって違うので結局調べても正しい値にたどり着けなかった。

コード的には parameter group value が default だと LEAST({DBInstanceClassMemory/9531392},5000) こういう式が、それ以外だと static に fixed value もとりうるというところがだいぶキモかった。計算式はわかったんだけど default は素直に計算された値を parameter group の value にいれといて、、、と思った。そうすれば今回書いたような map を持つ必要はなかった。

おわりに

これで aws.rds.database_connections の anormary のアラートで、接続数が少ない DB で false positive が起きてたのをアラート消せる。めでたしめでたし。

次回

AWS Config の結果を export するやつの予定。

docs.aws.amazon.com

なんかいろいろ考えてたけど忘れた。このへんか。

AWS RDS の Engine Version ごとの EOL 情報からアラート通知させる

これの続き。

blog.chaspy.me

解決したかった課題はこれ。

github.com

AWS RDS の Engine Version を Prometheus 形式で Export することで、今何のバージョンが何個あるかが可視化できるようになった。

あとは EOL 期限になったら、近づいたらアラート発砲できればいいんだが、どのように EOL 情報を取ってくればいいか、が課題だった。

残念ながら RDS の Recommendations は API が存在しない。し、Engine version outdated では過ぎたかどうかしかわからない。

docs.aws.amazon.com

どうする

こういう csv を持つ。

  • minimum_supported_version.csv
Engine,MinimumSupportedVersion,ValidDate
mysql,5.7.0,2021-02-05
postgres,9.6.0,2021-02-16
postgres,9.7.0,2022-01-18
aurora-postgresql,10.4,2022-01-31

逐一送られてくる AWS からのメールをみたら、この CSV を更新する。1年前から余裕をもって何度も送ってくるので、どうにかして気付けるだろう。

これ、何を意味しているかというと、例えば RDS MySQL の 5.6 系が 2021-02-05 で EOL となった。強制アップデートはまだ先だが、セキュリティパッチが当たらない日がこの日なので、この日に設定した。

aws.amazon.com

で、この日を過ぎると、5.6 が非推奨となるため、"Minimum Supported Version" はその1つ上、5.7.0 となる。

この情報をもとに、各 RDS の Engine, Engine Version を比較し、以下の4種類の string を eol_status tag につける。

  • 期限切れ: expired
  • 期限の1ヶ月以内: alert
  • 期限の1ヶ月以上3ヶ月以内: warning
  • 期限の3ヶ月以上: ok

これで、このタグを使って、expired, alert, warning の内容に応じてアラートをしかけることができる。

本当はこの csv の中身にあたる情報を API で取得したい。

学びとか感想とか

結構めんどくさかった。

Version 比較

github.com

が便利だった。

func compareEngineVersion(rdsInfo RDSInfo, minimumSupportedInfo MinimumSupportedInfo) (bool, error) {
    engineVersion, err := version.NewVersion(rdsInfo.EngineVersion)
    if err != nil {
        return false, fmt.Errorf("failed to declare engine version: %w", err)
    }

    minimumSupportedVersion, err := version.NewVersion(minimumSupportedInfo.MinimumSupportedVersion)
    if err != nil {
        return false, fmt.Errorf("failed to declare minimum supported version: %w", err)
    }

    return engineVersion.LessThan(minimumSupportedVersion), nil
}

LessThan 以外にもいろいろある。

pkg.go.dev

CSV の構造体への Parse

最初普通に1行ずつ読んであれしようと思ったけど面倒になったので少し探すと csvutil という便利そうなライブラリがあったので使った。

github.com

type MinimumSupportedInfo struct {
    Engine                  string
    MinimumSupportedVersion string
    ValidDate               string
}

func readEOLInfoCSV() ([]MinimumSupportedInfo, error) {
    var eolInfos []MinimumSupportedInfo

    csv, err := ioutil.ReadFile("minimum_supported_version.csv")
    if err != nil {
        return []MinimumSupportedInfo{}, fmt.Errorf("failed to read CSV file: %w", err)
    }

    if err := csvutil.Unmarshal(csv, &eolInfos); err != nil {
        return []MinimumSupportedInfo{}, fmt.Errorf("failed to unmarshal: %w", err)
    }

    return eolInfos, nil
}

Unmarshal で短くかけて良い。

github.com

Binary から見た読み込むファイルのパス

地味にハマった。こういう変更が必要だった。

github.com

csv, err := ioutil.ReadFile("minimum_supported_version.csv")

こういう風にファイルを指定していて、実行バイナリは /app/ 以下にあって、そこと同じ場所に csv をおいていた。

Dockerfile の最後らへん。

COPY minimum_supported_version.csv /app/minimum_supported_version.csv


ENTRYPOINT ["/app/aws-rds-engine-version-prometheus-exporter"]

この場合、ファイルパスは Dockerfile の Workfir からの相対パスになるので、no such file or directory となった。まぁそれはそうか。

/etc/ 以下に csv をおいて、読み取るようにした。

なるべく絶対パスで書く方がいいんだろうな。

結果

ちゃんととれた。

f:id:take_she12:20210207185609p:plain

空白の string が返っているのは、csv に engine 名がない場合。undefined なのは事実なのでそれでいいかと思いこうしている。

ちなみに中身は Aurora MySQL

expired なのは RDS MySQL で、近々メンテ予定しています。

これで こういう Datadog Alert が作れる。

f:id:take_she12:20210207191441p:plain

ちゃんとアラート発火した。めでたしめでたし。

f:id:take_she12:20210207223105p:plain

なんか metric value 変だな。。。

課題

Aurora MySQL のサポートがまだ。

ただ、そもそも EOL 情報がよくわからんというのと、Aurora MySQL の場合 engine_version が以下の2パターンあってキモい。

  • engine_version="5.7.12"
  • engine_version="5.7.mysql_aurora.2.07.2"

パースしたくないでござる。

ので現状サポートできてないです。

おわりに

コード書くの、まだ楽しいにはならないけど、量が質に転嫁する部分もあるだろうし、書いていればネタが浮かんでまたコードを書くというループが起きることを感じているので、今後もコード書きは継続的に続けてみる。

おしまい。

Ruby sshkey gem で OpenSSH 6.8 以降のデフォルトで生成される形式の private key の initialize に失敗する

背景

OpenSSH 6.8 以降と以前では ssh-keygen で生成される private key の形式が変わっている。

haruyama.blog.jp

  • Add FingerprintHash option to ssh(1) and sshd(8), and equivalent command-line flags to the other tools to control algorithm used for key fingerprints. The default changes from MD5 to SHA256 and format from hex to base64.

    FingerprintHash オプションを ssh(1) と sshd(8) に追加する. また, 鍵の指紋のために使われるアルゴリズムを制御する他のツールに 同様の指定をするコマンドラインフラグも追加する. デフォルトが MD5 から SHA256 に, 形式が hex から base64 になる.

問題

社内の内製ツールで、CircleCI の sshkey や環境変数をコード管理して適用するツールがある。そのツールが sshkey gem を使っているが、上記新形式の private key を読み込むときに以下のエラーで失敗する。

irb(main):004:0> f = File.read(File.expand_path("./openssh"))
=> "-----BEGIN OPENSSH PRIVATE KEY-----\nbbbbbbbbb (snip) ssssssssssecret\n-----END OPENSSH PRIVATE KEY-----\n"

irb(main):005:0> k = SSHKey.new(f, comment: "foo@bar.com")
Traceback (most recent call last):
        9: from /Users/chaspy/.rbenv/versions/2.6.5/bin/irb:23:in `<main>'
        8: from /Users/chaspy/.rbenv/versions/2.6.5/bin/irb:23:in `load'
        7: from /Users/chaspy/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        6: from (irb):5
        5: from (irb):5:in `new'
        4: from /Users/chaspy/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/sshkey-1.9.0/lib/sshkey.rb:214:in `initialize'
        3: from /Users/chaspy/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/sshkey-1.9.0/lib/sshkey.rb:218:in `rescue in initialize'
        2: from /Users/chaspy/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/sshkey-1.9.0/lib/sshkey.rb:218:in `new'
        1: from /Users/chaspy/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/sshkey-1.9.0/lib/sshkey.rb:218:in `initialize'
OpenSSL::PKey::DSAError (Neither PUB key nor PRIV key)

上記 key は以下のコマンドで生成している。

ssh-keygen -t rsa -b 4096

なお、旧形式では成功する。

$ ssh-keygen -t rsa -b 4096 -m pem
irb(main):002:0> f = File.read(File.expand_path("./pem"))
=> "-----BEGIN RSA PRIVATE KEY-----\nbbbbbbb (snip) aaaaaaa\n-----END RSA PRIVATE KEY-----\n"
irb(main):003:0> k = SSHKey.new(f, comment: "foo@bar.com")
=> #<SSHKey:0x00007fa9ef87e6a0 @passphrase=nil, @comment="foo@bar.com", @directives=[], @key_object=#<OpenSSL::PKey::RSA:0x00007fa9ef87e5b0>, @type="rsa">

バージョン

  • Ruby 2.6.5
  • sshkey 1.9.0

調査

sshkey gem

エラーが発生している箇所は以下。

github.com

  # Create a new SSHKey object
  #
  # ==== Parameters
  # * private_key - Existing RSA or DSA private key
  # * options<~Hash>
  #   * :comment<~String> - Comment to use for the public key, defaults to ""
  #   * :passphrase<~String> - If the key is encrypted, supply the passphrase
  #   * :directives<~Array> - Options prefixed to the public key
  #
  def initialize(private_key, options = {})
    @passphrase = options[:passphrase]
    @comment    = options[:comment] || ""
    self.directives = options[:directives] || []
    begin
      @key_object = OpenSSL::PKey::RSA.new(private_key, passphrase)
      @type = "rsa"
    rescue
      @key_object = OpenSSL::PKey::DSA.new(private_key, passphrase)
      @type = "dsa"
    end
  end

最初の begin block でエラーとなり、resque でキャッチして DSA のところでこけてスタックトレースに出ている箇所が表示されているのだと思う。

OpenSSL::PKey::RSA.newRuby 標準ライブラリの openssl である。

github.com

が、issue を見ても似たような現象を見つけられず、そもそも Ruby のどのバージョンが openssl のどのバージョンを使っているかわかっていない。

Ruby の Version を変えてみる

irb(main):001:0> require 'sshkey'
=> true
irb(main):002:0> f = File.read(File.expand_path("./openssh"))
=> "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAgE...
irb(main):003:0> k = SSHKey.new(f, comment: "foo@bar.com")
Traceback (most recent call last):
        9: from /Users/chaspy/.rbenv/versions/2.7.2/bin/irb:23:in `<main>'
        8: from /Users/chaspy/.rbenv/versions/2.7.2/bin/irb:23:in `load'
        7: from /Users/chaspy/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>'
        6: from (irb):3
        5: from (irb):3:in `new'
        4: from /Users/chaspy/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sshkey-1.9.0/lib/sshkey.rb:214:in `initialize'
        3: from /Users/chaspy/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sshkey-1.9.0/lib/sshkey.rb:218:in `rescue in initialize'
        2: from /Users/chaspy/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sshkey-1.9.0/lib/sshkey.rb:218:in `new'
        1: from /Users/chaspy/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sshkey-1.9.0/lib/sshkey.rb:218:in `initialize'
OpenSSL::PKey::DSAError (Neither PUB key nor PRIV key)

再現する。

  • Ruby 2.7.2 / sshkey 2.0.0
irb(main):001:0> require 'sshkey'
=> true
irb(main):002:0> f = File.read(File.expand_path("./openssh"))
=> "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAgE...
irb(main):003:0> k = SSHKey.new(f, comment: "foo@bar.com")
Traceback (most recent call last):
        9: from /Users/chaspy/.rbenv/versions/2.7.2/bin/irb:23:in `<main>'
        8: from /Users/chaspy/.rbenv/versions/2.7.2/bin/irb:23:in `load'
        7: from /Users/chaspy/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/irb-1.2.6/exe/irb:11:in `<top (required)>'
        6: from (irb):3
        5: from (irb):3:in `new'
        4: from /Users/chaspy/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sshkey-2.0.0/lib/sshkey.rb:246:in `initialize'
        3: from /Users/chaspy/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sshkey-2.0.0/lib/sshkey.rb:250:in `rescue in initialize'
        2: from /Users/chaspy/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sshkey-2.0.0/lib/sshkey.rb:250:in `new'
        1: from /Users/chaspy/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/sshkey-2.0.0/lib/sshkey.rb:250:in `initialize'
OpenSSL::PKey::DSAError (Neither PUB key nor PRIV key)

再現する。

  • Ruby 3.0.0 / sshkey 2.0.0
irb(main):001:0> require 'sshkey'
=> true
irb(main):002:0> f = File.read(File.expand_path("./openssh"))
=> "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAgE...
irb(main):003:0> k = SSHKey.new(f, comment: "foo@bar.com")
Traceback (most recent call last):
        9: from /Users/chaspy/.rbenv/versions/3.0.0/bin/irb:23:in `<main>'
        8: from /Users/chaspy/.rbenv/versions/3.0.0/bin/irb:23:in `load'
        7: from /Users/chaspy/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
        6: from (irb):3:in `<main>'
        5: from (irb):3:in `new'
        4: from /Users/chaspy/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/sshkey-2.0.0/lib/sshkey.rb:246:in `initialize'
        3: from /Users/chaspy/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/sshkey-2.0.0/lib/sshkey.rb:250:in `rescue in initialize'
        2: from /Users/chaspy/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/sshkey-2.0.0/lib/sshkey.rb:250:in `new'
        1: from /Users/chaspy/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/sshkey-2.0.0/lib/sshkey.rb:250:in `initialize'
OpenSSL::PKey::DSAError (Neither PUB key nor PRIV key)

再現する。

sshkey に類似 issue があったのでコメントしておいた。

github.com

openssl library

さて最新の Ruby でも再現するので、openssl library の問題の可能性が高い。こっちでも再現させる。

使っているのはこれである。

docs.ruby-lang.org

irb(main):001:0' require 'openssl'
=> true
irb(main):002:0> f = File.read(File.expand_path("./openssh"))
=> "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAgE...
irb(main):003:0> OpenSSL::PKey::RSA.new(f)
Traceback (most recent call last):
        6: from /Users/chaspy/.rbenv/versions/3.0.0/bin/irb:23:in `<main>'
        5: from /Users/chaspy/.rbenv/versions/3.0.0/bin/irb:23:in `load'
        4: from /Users/chaspy/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
        3: from (irb):3:in `<main>'
        2: from (irb):3:in `new'
        1: from (irb):3:in `initialize'
OpenSSL::PKey::RSAError (Neither PUB key nor PRIV key: nested asn1 error)

再現する。

さてここからは C の世界である。

トレースが出てないので、推測だが、初期化はここではないか。

github.com

raise されてるところはここではないか。

github.com

ただ、 nested asn1 error というのがどこで出ているのかわかっていない。

repository を検索すると別の部分で同じエラーが出ているようだ。

github.com

ここでお手上げ。

まとめ

  • OpenSSL::PKey::RSA.new に失敗する
    • OpenSSL::PKey::RSAError (Neither PUB key nor PRIV key: nested asn1 error)

  • Ruby Version は 3.0.0
  • OpenSSH 7.8 以降でデフォルトで生成される Key 形式は OpenSSL で読み取ることはできなさそう
    • 試したバージョン: OpenSSH_8.1p1, LibreSSL 2.7.3