Unyablog.

のにれんのブログ

Elasticsearch の Docker container でパスワードを指定する

環境: Docker の elasticsaerch image 8.3.2

久々に Elasticsearch を Docker で立ち上げると、初期状態でセキュリティ機能が有効になっていて、そのまま curl しても認証が通らず API が打てなかった。

$ curl -k https://localhost:9200/
{"error":{"root_cause":[{"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":["Basic realm=\"security\" charset=\"UTF-8\"","Bearer realm=\"security\"","ApiKey"]}}],"type":"security_exception","reason":"missing authentication credentials for REST request [/]","header":{"WWW-Authenticate":["Basic realm=\"security\" charset=\"UTF-8\"","Bearer realm=\"security\"","ApiKey"]}},"status":401}

環境変数で認証周りを完全に切ることもできるが、Docker image 経由で立ち上げている場合はパスワードの設定を環境変数経由で行うこともできるのでメモ。

どうするか

ELASTIC_PASSWORD で elastic user のパスワードを指定できる。これは、 Docker image の entrypoint.sh や Elasticsearch の Docker 関係のドキュメントを見ると書いてある。

github.com

www.elastic.co

実際に ELASTIC_PASSWORD を指定すると、下のように elastic user と指定したパスワードを Basic 認証で指定して curl が通ることを確認できる。

$ curl -k https://localhost:9200/ -u 'elastic:yourStrongPassword'
{
  "name" : "elasticsearch-6c77b74fd4-g9688",
  "cluster_name" : "cluster",
...

pip install --user を config でデフォルトにする

最近(2年以上前)、pip がデフォルトで --user でインストールするようになった。

github.com

ただ、この機能が有効になるにはいくつかの条件がある。

  1. The user has not explicitly specified otherwise in the command line, environment variable, or a config file.
  2. The --prefix and --target options are not in use.
  3. The global site-packages dir is not writeable, so doing a non-user install would fail.
  4. User site-packages are enabled - to avoid surprises in isolated environments.

https://github.com/pypa/pip/pull/7002#issue-491155059

ここで、Pythonbrew でインストールすると brew はユーザー権限で入るために 3 が満たせないことが多く、 brew 内のグローバルに入ってしまう状態が続いていた。

確かに権限的にはグローバルでも問題ないが、brew ディレクトリの中はクリーンであって欲しい。brew を使っていてもデフォルトで --user にしたい場合は、pip の config で設定する。

pip.pypa.io

pip の config ではコマンドのオプションのデフォルトを環境変数や config ファイル経由で設定することができる。

今回は config ファイル(~/.config/pip/pip.conf)で以下のように設定した。

[install]
user = true

こうして、環境によらず、また多少古い pip でもデフォルトで --user で入るようになった。

GKE の Preemptible VM を Spot VM にした

プライベートで GKE を Preemptible VM ベースで運用していた。だいぶ昔に Preemptible VM が Spot VM に変わるよという通知が GCP から来ていて 値段変わらないし 放置していたのだが、GKE のバージョンを上げるついでに Spot VM に変更した。

cloud.google.com

Preemptible VM は何がなんでも 24 時間以内に終了されるという AWS もびっくりの仕様だったが、 Spot VM ではそれがなくなり、 GCP のインフラ内で必要な場合にのみ終了されるようになった。

料金も変わらないのでユーザーにとってはメリットしかない変更のよう*1

GKE の Node pool を Spot VM にする

Node pool を直接 Spot VM に変更することはコンソールではできなさそうだったので、新しく Spot VM の Node pool を作って、その後 Preemptible VM の Node pool を削除した。

Spot VM の様子

5/4 に Spot VM を立ててから記事執筆時点で一週間弱立っているが、Terminate されることなく生き続けている。どれくらい生きてくれるのか、数ヶ月ぐらい様子を見て記事にできればと思う(希望)。

*1:とはいえ GKE の Node は k8s の特性上いつ壊れてもそれほど問題なく稼働し続けるので上げる理由もなく、コンソールポチポチする腰が上がらなかったのだった

TypeScript 型わくわく日記

TypeScript でオブジェクトのバリデータを作っている。それぞれの値ごとのバリデータは値の特性ごとに様々な関数がある((value: string) => Error[] のような形)。
時折 Optional な値があるものの、バリデータ自体は non-null な値のバリデーションに専念してほしくて、Optional / Required な判定は別の関数に分けたい気持ちがあった。

別の関数に分けないと、バリデータの中で一々 Optional かどうかの引数を取って判定したり、下のように各バリデータに対応して Optional な判定を加えた関数を作る必要があって面倒。

const validateOptional = (value: string | null | undefined, allowAsterisk: boolean): Error[] => {
    if (value == null) {
        return [] // ok
    }
    return validate(value, allowAsterisk)
}

これを、

  1. non-null 用のバリデーション関数を引数とし、null チェックを行う処理を差し込んだ nullable 用の新しいバリデーション関数を返す高階関数を作成する
  2. 様々なバリデーション関数の引数に対応するため、TypeScript の Variadic Tuple Types を使う

ことによってスッキリできた。

例として、non-null な値しか受け取らないバリデータに対して、値を Optional とするための高階関数を下のように作った。

const optional = <U, T extends unknown[]>(validate: (value: U, ...args: [...T]) => Error[]) => (value: (U | null | undefined), ...args: [...T]) => {
    if (value == null) {
        return []; // ok
    }
    return validate(value, ...args);
}

nullable な value を受け取って null チェックを行い、null | undefined であれば ok、non-null であれば validate 関数に再度投げている。それ自体は初めの関数と特に変わらない。

見所は Variadic Tuple Types を使っているところで、 validate 関数の1引数目以外をまとめて [...T] と受け取ることで、1つ目以外の引数(とその型情報)を維持したまま新しい関数を生成できている。
これがなかったら、バリデータ関数のオプションを全て2引数目に Object として押し込む (value: U, options: T) => Error[] のような形)ようにインターフェースを統一することになっていたと思う。

// 例
const validate = (val: string, allowAsterisk: boolean) => {
    const errors = [];
    if (val.length >= 10) {
        errors.push(new Error("value must be shorter than 10 chars"))
    }
    if (!allowAsterisk && val.includes('*')) {
        errors.push(new Error("value must not contain character *"))
    }
    return errors
}

// validate: (val: string, allowAsterisk: boolean) => Error[]
// optional(validate): (val: string | null | undefined, allowAsterisk: boolean) => Error[]

console.log(
    optional(validate)("*", true), // => [value must not contain character *]
    optional(validate)("*", false), // => []
    optional(validate)(undefined, true) // =>[]
)

TypeScript たのし~~

AlertManager: Slack の Incoming webhook URL が Kubernetes の Secret で扱いやすくなっていた

Prometheus の AlertManager は Slack などに通知を送る重要なコンポーネントである。

当然 Slack の Webhook URL といった Credential を設定ファイルに書く必要があるのだけど、AlertManager は強い意志で環境変数や複数設定ファイルに対応していないので、Kubernetes の Secret との相性がとても悪かった。

この状況下で Secret を使うには、Secret にまるまる設定ファイルを突っ込むか、envsubst を initContainer に指定して設定ファイルを動的に生成する必要があった。

Secret は sealed-secret で暗号化した状態でレポジトリに置くようにしているので、AlertManager の設定を変えるたびに暗号化をしなければならないのは面倒すぎる。とはいえ、 envsubst をするのは configmap-reload などと相性が悪い。

AlertManager 0.22

平文でレポジトリに置くしか無いのか・・・? と思っていた矢先、去年にリリースされた AlertManager 0.22 で slack_api_url_file というオプションが生えて、ファイルから Slack Incoming webhook URL を指定できるようになっていることに気づいた。

[ENHANCEMENT] Add support to set the Slack URL from a file. #2534

https://github.com/prometheus/alertmanager/releases/tag/v0.22.0

最高のアップデートじゃん!ということで、Secret に Slack の Webhook URL だけを置くような構成にすることができた。これで柔軟に Config を変更することができる。

Prometheus の Helm Chart だとこんな感じ。

alertmanager:
  extraSecretMounts:
    - name: alertmanager-secrets
      mountPath: /alertmanager-secrets
      secretName: prometheus-alertmanager-secrets
      readOnly: true
alertmanagerFiles:
  alertmanager.yml:
    global:
      slack_api_url_file: '/alertmanager-secrets/slack-api-url'
    receivers:
    - ...

Git でブランチがリモートリポジトリの HEAD から派生しているかどうかを確認する

Git でトピックブランチを切るときに、元ブランチで git pull し忘れて古いコミットから生やしてしまうときがある。

最新の変更を認識できなかったり、後々コンフリクトが起きたりと不便なことがままあるので、 Commit 前に HEAD がリモートリポジトリの HEAD から派生しているかを確認するスクリプトを書いてみた。

Git には Commit 前に pre-commit hook を実行することができる。そこで non-zero code で exit すれば Commit 処理が中止されるので、これで防ぐようにする。

コード

これを .git/hooks/pre-commit 内に置く。

git fetch origin
if ! git merge-base --is-ancestor origin/HEAD HEAD; then
  echo "ERROR: HEAD is not based on upstream HEAD"
  exit 1
fi

解説

まず git fetch origin でリモートリポジトリを fetch する。

その後、「ブランチが origin の HEAD から派生しているかどうかを確認する」ために git merge-base を使う。今回は HEAD の祖先に origin/HEAD がいるかどうかを確認しており、祖先にいなければ派生元のコミットが古いということで exit 1 し、祖先にいれば Commit を続行するようにしている。

git merge-base --is-ancestor が便利、というお話でした。

Python の subprocess.run は KeyboardInterrupt で強制的に kill される

Python から Terraform をインタラクティブな(stdin をそのまま渡す)形で呼ぶために、subprocess.run を使って subprocess で走らせようとしている。

ここで、 Ctrl-C (KeyboardInterrupt) のハンドリングがネックになる。 Terraform は Ctrl-C を打つと Graceful な終了を試みることがあるので、Ctrl-C が打たれてもしばらく subprocess を終了しないようにしたい。

try で囲んで except KeyboardInterrupt すれば実現できそうだが、残念ながら subprocess.runsubprocess.call では実現できず、すぐに kill されてしまう。

Python 3.9 で確認。

subprocess.run 中に Ctrl-C を押すとどうなるのか

subprocess.run 中に Ctrl-C を押すと、まず subprocess.run 中で使われている Popen.communicate でハンドリングされる。

ここでは、0.25秒(固定値)*1待った後に KeyboardInterrupt を re-raise している。

github.com

その後、subprocess.run でハンドリングされるが、そこで有無を言わさず process.kill() が呼ばれて kill されてしまう。

github.com

  • Popen が re-raise するまでの待ち時間を伸ばせないこと
  • re-raise した場合 subprocess.runprocess.kill() を呼んでしまうこと

から、 subprocess.run 中に Ctrl-C で subprocess が終了するのを防ぐ手段はない。

どうするか

subprocess.run の便利な機能を使えないのが残念だが、Popen を直接使うようにした。

with subprocess.Popen(args) as p:
    try:
        return_code = p.wait()
    except KeyboardInterrupt:
        # Ctrl-C が打たれたら、プロセスが終わるのを待ってから raise する
        # 補足: 2回目の p.wait() は try-except で囲んでいないので、もう一度 Ctrl-C が打たれた場合は特にハンドリングされず raise される
        return_code = p.wait()
        raise

*1:初回以降は wait しない。詳細: https://github.com/python/cpython/pull/5026