ツナワタリマイライフ

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

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

AWS ECR の Image Scan 結果を Prometheus 形式で Export する OSS 作った

作った。

github.com

いい加減思っているのだけどタイトルが長くてつらい。

でも CloudProvider - Service - Items to export - prometheus - exporter

だとやむなし?

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

blog.chaspy.me

blog.chaspy.me

blog.chaspy.me

これは何

AWS ECR には Image Scan 機能があり、それを "Findings" として、脆弱性や Severity Level をみることができます。

dev.classmethod.jp

クラメソさんいつもありがとう。

解決したい課題

ECR Image Scan は便利だが、まぁかなりの量が出る。

で、Severity も Critical から Information level まで様々だ。

これが Repository ✖️ Image Tag 分あるというわけで、かなりの量になる。GUI でポチポチみるより、より簡単な方法で可視化したい。

また、Severity level で Filter したり、Repository で Filter して優先度をつけたりというコントロールができるようにならないかと考えている。

f:id:take_she12:20210131184547p:plain

工夫した点

工夫というほどではない(いつもの)

IMAGE_TAG 指定を必須にした

仕様の問題だが、IMAGE_TAG 指定を必須にしました。あと現状 Image Digest での Image 指定はサポートしていません。

ユースケースを考えても特定リポジトリに対応する特定の Tag だけ見れればいいはず&Image Tag を指定しての API Call はまとめてできないので、いっそ必須にしてしまった。

あとあとで触れるがパフォーマンスの問題があるので、Image Tag で絞り込まないと結構キツい。

Pagination Support

まぁ数が多いのでちゃんと追いかけるようにした。

NextToken があれば Set、なければ終わるとすればいいだけなので楽だった。

github.com

今後改善する点

Performance

地獄の For Loop をキメてしまっているので、API Call は並行で処理できるようにしてパフォーマンスを改善する

Error Handling

この部分を switch 使えだの errors.As を使えなどと linter が言ってくるんですが

github.com

正直どうすればいいかわかってない。型アサーションは errors.As でできそうだけど、そのあと Error Name? Code? ごとに例外をキャッチしていくのをどうすればいいのかわかってない。誰かペアプロしてください。

Attributes の取得

Attributes が Key と Value をそれぞれ持った struct の List なんだけど

ecr - Amazon Web Services - Go SDK

こんな風にぐるぐるまわして switch case で取らないといけないのマジ?ってなっている。でもそんなもん?

github.com

感想

RDS のやつよりは面倒でした。あとこれもまた metric 送って眺めて味ってみないと役に立つかどうかはまだわかりません。

次回作

aws-rds-max-connections-prometheus-exporter

RDS の max connections を metric として送る。

現状、Datadog Integration ではこれらは取れていない。

いまは Connection の Anomary Detection のAlert をつけているが、False Positive に飛んできたりする。

で、max connections は parameter group の中にあってそれは API で取得できるんだが

docs.aws.amazon.com

  • MySQL: {DBInstanceClassMemory/12582880}
  • PostgreSQL: LEAST({DBInstanceClassMemory/9531392}, 5000)

マジでこういう値が入っている。

これを Parse し、Instance Class とその Memory size を取得して計算するというロジックを実装しないといけない。だるそう。

とはいえこれができると、現状の connection 数が max_con に達しそうだよ〜ってアラートが作れるので、役に立つ可能性は高いのでやる。

おしまい。

女性エンジニアのキャリアストーリーを集めた womeneng.jp 立ち上げと Netlify & hugo でハマったこと

背景

この記事を読んで

note.com

こう思い浮かんで

あおいさんとやるかーってなって

womeneng.jp 立ち上げました。

womeneng.jp

ちょっと前にこの業界の男女比であったり、男女でいうところ事実上女性がマイノリティであることであったり、そのマイノリティがゆえの不利益を何かしら被っていることであったりを考えた時期がありました。

~ Girls 系のイベントであったり、最近だと大平さん、菜穂子さん、ちょまどさんが Code Polaris を立ち上げたり、既に活躍されてる女性がこの問題の解決に向かって動いています。

chomado.com

note.com

この男女比の偏りは長期的に是正されるべきだと思います。しかしそのために、業界における圧倒的マジョリティ(情報系大学、大学院卒で、男性で、Software Engineer としてキャリアを重ねている)である自分個人に何ができるだろうとずっと考えていました。

その頃あおいさんに何かできることないですかね、って雑に相談してたことがあったりして。結局その時は何も浮かばなかったのですが、

wiroha さんの note が公開され、

ただなるべくハードルを取り除いて、自信とチャンスを与えられればもっと増えるかなぁと思ったりしています。

この考えに強く共感しました。

その頃、別の女性エンジニアの方から以下の TED の動画を教えてもらいました。

www.tedxtokyo.com

女性がエンジニアリングを職業にする割合が低いのは、その素養を養う、幼少期に与えられるおもちゃの段階で Gender 差が生まれている、それを解決するために自ら女性のためのおもちゃ会社を立ち上げた女性のエピソードでした。

wiroha さんのキャリアストーリーの共有も同様に"選択肢"を増やしてくれる取り組みだと思いました。

きっとこのムーブは続きそうだ、と思い、Static Web Site の Hosting ぐらいなら自分でもできる、と思い、あおいさんに相談して今回立ち上げに至りました。

あおいさんが寄稿してくれなかったら、やろうと言ってくれなかったら自分一人ではできなかったと思います。心から感謝します。

今後

womenengjp は GitHub の Open Repository です。Pull Request で Contribution できます。

github.com

今後は見つけ次第 Link を追加していくのをぼちぼちやろうと思っています。

また、現状は個人を特定できる形で、勇気ある方が書いてくれていますが、全員がそうではないと思っています。インタビュー形式のように、語り手の負担を下げる取り組みであったり、より匿名性が高い形でのエピソード掲載ができないか、という話をあおいさんとしています。

少しずつでいいのでキャリアストーリーの数が増え、将来、女性エンジニアになるかもしれないひとの選択肢を知る手段として役に立ち、"女性エンジニア"というくくりが必要ない世の中になればいいなと思います。

おわりに

ストーリー書いてもいいよ!とか、書いたよ!っていうひとは連絡くれたり、PR で追加いただけたり、このサイト自体を広めてくれると嬉しいです。

womeneng.jp

それでは以降は Netlify & hugo のハマりです。

ハマり

本当はもっと爆速で公開したかったのに5日間もかかってしまったのでハマったことををまとめます。

Netlify での HTTPS が有効にならない

Netlify では Netlify DNS を使っていると custom domain に対して Let's Encrypt の SSL 証明書をボタンポチるだけで準備してくれます。超便利。

が、これが24時間経っても DNS Preparation が終わらない。

サポートに問い合わせるも Community に聞いてくれと言われ、早く出したかったので Pro Plan にあげて 1on1 サポートをつけてもらった。

原因

CAA Record の設定が必要だった(?)(ただ、過去は同じドメインレジストリ、同じ Netlify の設定で、CAA Record なしでできたケースもあったので謎)

対処

以下の要領で CAA Record を Netlify DNS で作成

$ dig CAA womeneng.jp

; <<>> DiG 9.10.6 <<>> CAA womeneng.jp
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 7517
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;womeneng.jp.           IN  CAA

;; ANSWER SECTION:
womeneng.jp.        7166    IN  CAA 0 issue "letsencrypt.org"

;; Query time: 21 msec
;; SERVER: 2404:1a8:7f01:b::3#53(2404:1a8:7f01:b::3)
;; WHEN: Sun Jan 31 02:09:01 JST 2021
;; MSG SIZE  rcvd: 74

最初設定してたときはこの Issuer が間違っていた。

学んだこと

作業ログはこちら。

github.com

Preview Site で CSS が適用されない

Netlify では Pull Request を作成するごとに Preview Site が Deploy される。めちゃ便利である。が、なぜか PR 環境の場合 CSS が適用されない。

CSS を要求する url が以下のようにスラッシュが欠けていた。 js/bundle.js の前ね。

Got: Request URL: https://deploy-preview-2--naughty-mcnulty-91e54f.netlify.appjs/bundle.js Want: Request URL: https://deploy-preview-2--naughty-mcnulty-91e54f.netlify.app/js/bundle.js

Issue と PR はこれ。

github.com

github.com

.netlify.conf で build 時のコマンドを設定するわけだけど、DEPLOY_PRIME_URL がなんと preview site の場合は末尾のスラッシュを含まない。マジかよ。

docs.netlify.com

というわけで入れた。ugly な hack な気がするがしょうがない。これみんなハマらないのかな。

画像が Responsive にならない

Issue

github.com

スマホとか Window size が小さいものでみた時に画像が横幅 100% の resize されず横スクロールが必要みたいになっていてイケてない。

これはまぁもしかしたら hugo theme の upstream に contribution できることなのかもしれない。あとで Issue 起票してみよう。

css 知識 2 の知能でなんとかしました。

github.com

hugo template が custom css 使えて助かった。

2021-01-31 追記

template のほうに PR を投げて merge されました。良い話だ。

github.com