Posts for: #Cli

ローカルに window server をインストールした

1時過ぎに寝て6時半に起きた。久しぶりによく眠れた。6時半に起きてたのに8時ぐらいまでだらだらしてた。

今日の筋トレは腹筋:20x1,腕立て:15x1,スクワット20x1をした。

windows server 2022 試用版のインストール

Windows Server 2022 の試用版が提供されていると知ったのでローカルの VirtualBox 環境にインストールしてみた。Active Directory の検証に使うのでディレクトサービスや ldaps 接続のための証明書サービスなどを設定する。メンバーがインストールして課題管理システムにメモを残してくれてあったのでそれを見ながらインストールや設定自体はすぐにできた。1つだけうまく接続できないことがあった。

ホスト os から ldapsearch で ldaps で接続しようとすると次のようなエラーになる。

$ ldapsearch -x -H "ldaps://192.168.56.101" -W -b "CN=Users,DC=myad,DC=com" -D "CN=Administrator,CN=Users,DC=myad,DC=com"
ldap_sasl_bind(SIMPLE): Can't contact LDAP server (-1)

ldap では接続できるので tls の検証のチェックに失敗していることは自明だったが、メンバーは接続できているようにみえたので私の環境の設定が誤っているのかどうかを調べていた。2時間ぐらいデバッグしたりしながら調べてもよくわからなくて、たまたまチーム勉強会があったので終わったときにメンバーに聞いてみた。すぐに完結した。ldapsearch は LDAPTLS_REQCERT という環境変数で tls のリクエストの振る舞いを制御できる。次のように明示的に指定すると接続できる。

$ LDAPTLS_REQCERT=never ldapsearch -x -H "ldaps://192.168.56.101" ...

この設定はいくつかの方法で設定ファイルに書いておくこともできる。メンバーが使っている環境には ldap.conf でこの設定を有効にしていたので ldaps 接続できていたというオチだった。私が openldap について明るくないのでこういった背景知識をもっていなくてはまっていただけだった。

Thus the following files and variables are read, in order:
    variable     $LDAPNOINIT, and if that is not set:
    system file  /etc/openldap/ldap.conf,
    user files   $HOME/ldaprc,  $HOME/.ldaprc,  ./ldaprc,
    system file  $LDAPCONF,
    user files   $HOME/$LDAPRC, $HOME/.$LDAPRC, ./$LDAPRC,
    variables    $LDAP<uppercase option name>.
Settings late in the list override earlier ones.

明石海峡大橋海上ウォーク

神戸ジャーナルの記事をみかけて、そういうイベントがあるのは知っていた。

開発合宿の翌週だし、わざわざ行くほどでもないかとスルーしていたものの、関東から知人がわざわざ歩きに来るという話しを聞いて、せっかくの機会なので私も参加することにした。土曜日の午前中に明石海峡大橋を歩いてくる。まだ申込みが始まったばかりなので希望の時間帯を選択できると思う。

vhs コマンドの使い方

23時過ぎに寝始めて何度か起きて7時半に起きた。

今日の筋トレは腹筋:10x1,腕立て:10x1,スクワット15x1をした。

sveltekit で context のデータを扱う

ui 側でページに依存しない形で設定情報などを扱いたいとする。svelte の store と context api を組み合わせて sveltekit として context を管理するサンプルコードが紹介されている。

これだけですぐ動くのだけど、このときに LayoutData はサーバー側で作るとバックエンドの仕組みを隠蔽できて嬉しい。そういったときは src/routes/+layout.server.ts にバックエンドの api 呼び出しを隠蔽することで意図した振る舞いになる。

import type { LayoutServerLoad } from "./$types";

export const load: LayoutServerLoad = async () => {
  const r = await fetch(`localhost:18080/myapi`);
  return r.json();
};

アニメ gif をスクリプトから作る

お気に入りのコマンドラインツールを淡々と紹介する をみていて vhs という cli でアニメ gif を作ってくれることを知った。試しにやってみた。ターミナルを録画するようなやり方と比べて、録画時にタイプミスしてしまうようなミスを防げる。次のようなスクリプトファイルを新規作成する。

$ vhs new bf.tape 
Output bf.gif

Require echo

Set Shell "bash"
Set FontSize 14
Set Width 800
Set Height 380

Type "genact -m bruteforce" Sleep 500ms  Enter

Sleep 10s

あとはこの設定でアニメ gif を作る。

$ vhs bf.tape 

これで 4.8 MiB なのでサイズはまぁまぁ大きい。サイズやカラーの調整をすればもう1桁は縮小できるかもしれない。

$ ls -lh img/2024/0110_bf.gif 
-rw-rw-r-- 1 4.8M  1月 10 19:48 img/2024/0110_bf.gif

gitlab issues と mongodb による分析

0時に寝て何度か起きて7時に起きた。今日も一日資料作りをしていた。

gitlab issues の解析

ふりかえりの資料を作っていて gitlab issues の解析を始めた。gitlab にも分析系機能は提供されているが、大半が有償機能で free では使えない。実質 free で役に立ちそうなレポートを私はみつけられなかった。

gitlab は glab cli というツールを提供している。試しに glab を使って issues の解析ができないかとやってみたが、グループ単位ではなくプロジェクト単位でしか操作できないようにみえた。そこで rest api を呼び出すための便利ツールとして使うことにした。要は rest api で任意のデータを取得してそれを使ってローカルで解析することにした。例えば、次のようにして特定ラベルを除外した特定グループのマイルストーンごとの issues をすべて取得できる。

$ mygrpid="xxx"
$ milestones="2022-11 2022-12 2023-01 2023-02 2023-03 2023-04"
$ for i in $milestones; do echo $i; glab api --paginate "groups/${mygrpid}/issues?milestone=${i}&not[labels]=Duplicate,Invalid,Wontfix" | jq -c '.[]' > "${i}-issues.json"; done

あとはこの json データをそのまま分析のためのデータベースに取り込む。今回は mongodb にインポートしてみた。mongodb だとスキーマを定義しなくても json データをそのままインポートできてアドホックな分析に便利そうに思えた。オブジェクトの入れ子構造をもつ json データのようなものを rdbms にインポートするのはひと工夫必要なことから json データをそのままインポートできるドキュメントデータベースの有効性を理解できた。インポートしたら MongoDB Shell を使うとてっとり早い。例えば、マイルストーンごとの issues の件数などは次のようにして集計できる。

gitlab> db.issues.aggregate([{ $group: { "_id": "$milestone.title", count: { "$sum": 1 } } }, { $sort: { _id: 1 }}])
[
  { _id: '2022-11', count: 348 },
  { _id: '2022-12', count: 346 },
  { _id: '2023-01', count: 338 },
  { _id: '2023-02', count: 357 },
  { _id: '2023-03', count: 347 },
  { _id: '2023-04', count: 336 }
]

担当者別に Enhance ラベルが付いた issues の件数を数えるときには次のようになる。sql を使えないというデメリットを json データをそのままインポートできるメリットの方が上回るときは mongodb のクエリを学ぶ機会になる。私も mongodb の aggregation の実行方法をドキュメントみながらやってた。全然わからないので慣れが必要になる。

gitlab> db.issues.aggregate([{ $group: { "_id": { assignee: "$assignee.username", enhance: {$in: ["Enhance", "$labels"]} }, count: { "$sum": 1 } } }, {$match: {"_id.enhance": true}}, { $sort: { _id: 1 }}])
[
  { _id: { assignee: 'bob', enhance: true }, count: 84 },
  { _id: { assignee: 'john', enhance: true }, count: 143 },
  { _id: { assignee: 'mary', enhance: true }, count: 53 },
  { _id: { assignee: 'parks', enhance: true }, count: 78 }
]

ローカルのゲスト os に開発環境を作る

0時に寝て7時に起きた。いろいろうまくいってない。

vagrant 再び

rpm でパッケージングされた openldap サーバーの動作検証をするために vagrant で rockylinux/8 Vagrant box の環境を構築する。 rockylinux 8 だと次のようなエラーが発生した。

VBoxManage: error: Failed to open OVF file 'path/to/.vagrant.d/boxes/rockylinux-VAGRANTSLASH-8/7.0.0/virtualbox/box.ovf' (VERR_FILE_NOT_FOUND)

既知のバグとして次の forum にワークアラウンドが書かれている。

uefi なマシンのせいなのかな?詳細を理解していないけど Vagrantfile を次のようにする。

Vagrant.configure("2") do |config|
  config.vm.box = "rockylinux/8"
  config.vm.provider "virtualbox" do |domain|
    domain.customize ["modifyvm", :id, "--firmware", "efi"]
  end
end

修正済みのイメージをダウンロードするようにメタデータを作成する。

$ vi box-metadata.json
{
  "name" : "rockylinux/8",
  "description" : "Rocky Linux 8 7.0.0 Bugfix",
  "versions" : [
    {
      "version" : "7.0.1-20221213.0",
      "providers" : [
        {
          "name" : "virtualbox",
          "url" : "http://dl.rockylinux.org/pub/rocky/8/images/x86_64/Rocky-8-Vagrant-Vbox-8.7-20221213.0.x86_64.box"
        }
      ]
    }
  ]
}

再度 vagrant の環境を作り直す。

$ vagrant box add box-metadata.json
$ rm -rf .vagrant/  # 古い設定を削除
$ vagrant up

vagrant を使うのも4年ぶりになるかな。コンテナに慣れてしまうと久しぶり感がある。使い方を忘れていて調べながらやってた。

vagrant にポートフォワーディングの設定を追加

$ vi Vagrantfile
...
  config.vm.network "forwarded_port", guest: 389, host: 1389   # ldap
  config.vm.network "forwarded_port", guest: 636, host: 1636   # ldaps
...

これでホスト os からゲスト os へ接続できる。

==> default: Forwarding ports...
    default: 389 (guest) => 1389 (host) (adapter 1)
    default: 636 (guest) => 1636 (host) (adapter 1)
    default: 22 (guest) => 2222 (host) (adapter 1)

ここでは ldap サーバーに対して Apache Directory Studio で接続できるように ssh のポートフォワーディングを設定している。

scp でファイルを転送

config を出力する。ssh の秘密鍵へのパス設定をしてくれるので scp のオプションに指定しなくて済む。

$ vagrant ssh-config > ssh.config

ポート番号も config に記載されているけれど、それは指定しないと scp できなかった。

$ scp -P 2200 -F ssh.config path/to/myfile vagrant@localhost:

vagrant ユーザーのパスワードも聞かれて vagrant を指定すればコピーできた。config を作ってもあまり便利ではなかった。

vagrant-scp というプラグインがあるのでインストールしてみる。

$ vagrant plugin install vagrant-scp

次のようにして使う。この方が簡単。

$ vagrant scp path/to/myfile ./  # 仮想マシンのホームディレクトリにコピーされる

rsync に daemon モードがあるらしい

23時に寝て2時半に起きて4時や5時に起きて7時に起きた。泊まっているホテルの低反発枕の寝心地がよい。

rsync daemon over ssh

外部向けのドキュメントを公開するための gitlab ci/cd を構築した。web サーバにドキュメントをアップロードする手段として rsync を使っている。rsync over ssh でデータを転送するときにさらに daemon モード (rsyncd) という仕組みがあって、権限や書き込み先の acl なども細かく制御できる。手順や設定は古の古臭い雰囲気はするけれど、実用的には ssh の秘密鍵を使ってちょっと高機能なアップロードを実現できる。ssh agent で鍵登録できていれば次のような cli でセキュアに rsync できる。全然知らない方法だったので学びの1つになった。

$ rsync \
    --verbose \
    --rsh ssh \
    --stats \
    --compress-choice=zstd \
    --compress-level=10 \
    --itemize-changes \
    --recursive \
    --checksum \
    --delete \
    local/ ${USER}@${HOST}::${RSYNC_DIR}

LLMを使ってみる会

LLMを使ってみる会 に参加した。私も chatgpt に調べものやちょっとしたことを聞くようになったりしているが、他の人たちがどんな用途に使っているのかも知りたくて参加してみた。fin-py のイベントだったのでみんな金融系のドキュメントの要約に使っているのが多そうにみえた。あとは研究テーマとして gpt/llm を取り上げている人たちも何人かいた。

docker/compose のモジュールの使い方がわかってきた

0時に寝て7時に起きた。前日はバテてだらだらしていたので寝過ぎた。

案ずるよりもツールできた

先週末に docker/compose 関連のライブラリ調査 を終えて実際のツール作りをしていた。本当は日曜日に作ってしまおうと思いつつ休んでしまった。なぜか今日はメンバーが全員お休みでチームで私しか働いていなかった。年度末で有休消化しているのかな?問い合わせ対応やメンバーのサポートが不要だったので1日中、自分の開発に集中できた。そして開発に集中できた結果、一通りツールの機能の開発を終えられた。火曜日までには仕上げたいと思っていたのでぎりぎり間に合った。

最終的に testcontainers-go の compose モジュールを使うのは断念して compose cli のみ go 標準の os/exec パッケージを使ってプロセスを fork するようにした。また docker image をコンテナレジストリから取得するときに認証が必要な場合、最初の docker login すると credentials store にパスワード (またはトークン) 情報が記録される。設定情報は $HOME/.docker/config.json からも確認できる。この仕組みを使ってコンテナレジストリへのログインを自動化できる。私の環境では macbook に docker desktop をインストールしているが、普通に使っていると次のように credentials が保存されてその内容を確認できる。

$ docker-credential-desktop list | jq .
{
  "https://index.docker.io/v1/": "t2y1979",
  "https://index.docker.io/v1/access-token": "t2y1979",
  "https://index.docker.io/v1/refresh-token": "t2y1979"
}
$ echo "https://index.docker.io/v1/access-token" | docker-credential-desktop get
{"ServerURL":"https://index.docker.io/v1/access-token","Username":"t2y1979","Secret":"***"}

これと同じことを docker のライブラリで行うには次のようにする。取得したい docker image の uri を参照すればコンテナレジストリがわかる。そこからこの cli でやっているようなことを順番にやっていけばよい。これらのユーティリティは3つのリポジトリで管理されていて、この雰囲気をみただけでもこのモジュール分割が本当に適切なんやろか?とか思ったりもする。

func getRegistryAuthFromImage(
	ctx context.Context, imageURI string,
) (string, error) {
	ref, err := reference.ParseNormalizedNamed(imageURI)
	if err != nil {
		return "", fmt.Errorf("failed to parse image uri: %w", err)
	}
	repo, err := registry.ParseRepositoryInfo(ref)
	if err != nil {
		return "", fmt.Errorf("failed to parse repository: %w", err)
	}
	dcli, err := command.NewDockerCli()
	if err != nil {
		return "", fmt.Errorf("failed to create docker cli: %w", err)
	}
	auth := command.ResolveAuthConfig(ctx, dcli, repo.Index)
	encoded, err := command.EncodeAuthToBase64(auth)
	if err != nil {
		return "", fmt.Errorf("failed to encode auth: %w", err)
	}
	return encoded, nil
}

postgresql の json データ型

1時に寝て6時半に起きた。連休中に夜更ししてたから生活が乱れた。

ロガー向けのログ保存 API の開発

先週の休暇前にやっていた作業の開発に着手。一通り web api のエンドポイントの実装は終えてテストをあらかた書いたところ。いまのプロジェクトとしても、過去の私の経験としてもやったことのない新しい挑戦の1つとして postgresql の JSONデータ型 を使う。具体的には json 型と jsonb 型の2つがある。前者はテキストで保持する型で、後者は内部的にバイナリに変換されてインデックスも使える。バイナリに変換してインデックスを作る分、insert 時にテキストで保存するよりは少し余分なオーバーヘッドを要する。json のデータを参照用途で使うのか、検索するのかでこれらの型を使い分ければいいのかな。

実際の sql で json データの条件指定は次のようになる。@> というみたこともない気持ち悪い演算子を使う。

> select * from mytable where data  @> '{"x": 1, "y": 2}';

java の jdbc で扱うには PGobject という型に変換して扱う必要がある。

private PGobject convertData(String value) throws SQLException {
    var data = new PGobject();
    data.setType("jsonb");
    data.setValue(value);
    return data;
}

余談だけど、curl で json 文字列を query string としてリクエストするには url encode しないといけない。

$ curl -s --get --data-urlencode 'data={"x": 1, "y": 2}' http://localhost/path | jq .

k8s の cronjob を検証中

0時に寝て6時に起きた。寝不足を解消して体調が戻ってきた。

k8s の cronjob

バッチ処理を Kubernetes: CronJob で作る。一通り設定して minikube で検証して eks 上でも動くようになった。

apiVersion: batch/v1
kind: CronJob
metadata:
  name: my-app-hourly-job
spec:
  schedule: "5 */1 * * *"
  concurrencyPolicy: Forbid
  startingDeadlineSeconds: 600
  jobTemplate:
    spec:
      backoffLimit: 0
      template:
        metadata:
          labels:
            app: my-app-hourly
          annotations:
            dapr.io/enabled: "true"
            dapr.io/app-id: "my-app-hourly"
        spec:
          containers:
          - name: my-app-hourly-job
            image: my-app-image
            imagePullPolicy: Always
            env:
            - name: BATCH_ENV
              value: "dev"
            command:
            - "/bin/sh"
            - "/app/scripts/my-app.sh"
            - "param1"
            - "param2"
          restartPolicy: Never

command の設定がわかりにくい。さらに k8s のドキュメントのサンプル設定も誤解を招くような例になっている。どうも実行できるのは1つの cli だけで、複数コマンドを指定できるわけではない。シェルスクリプトを docker イメージに含めて、そこで任意のスクリプトを実装した方がよいだろう。

  • “/bin/sh”
  • “/app/scripts/my-app.sh”
  • “param1”
  • “param2”

この設定は次の cli として実行される。

/bin/sh /app/scripts/my-app.sh param1 param2

How to ensure kubernetes cronjob does not restart on failure によると、バッチ処理が失敗したときに再実行したくないときは次の3つの設定をする。

  • concurrencyPolicy: Forbid
  • backoffLimit: 0
  • restartPolicy: Never

restartPolicy が Never 以外だと、エラーが発生すると永遠に再実行されてしまうので障害時に2次被害を増やしてしまう懸念があったような気がする。

あと、うちの環境は dapr 経由で他の pod サービスと通信しているので dapr を有効にしないと pod 間通信ができない。dapr はデーモンでずっと起動しているからバッチ処理の終了時に daprd も shutdown してやらないといけない。Running Dapr with a Kubernetes Job にその方法が書いてある。daprd を shutdown しないと、pod のステータスが NotReady のままで Completed にならない。

まだまだよくわかってないので Jobs のドキュメントに一通り目を通そうと思っている。

maven で executable jar を作る

4時に寝て7時に起きた。

maven での executable jar の作り方

gradle では作ったことがあったけど、maven では初めてなので要領がわかっていない。

これらの記事を読むと、maven-assembly-plugin を使えばいいのかな?とまずはこのプラグインで検証を始めた。古くからあるプラグインなので実績は十分なのだけど、もうあまり保守されていないのか、他プラグインから jar のマニフェストに書き込んで git のリビジョン番号が連携できてなかったり、通常の jar の生成処理を置き換えられなかったりと、あまり使い勝手のよいものではなかった。あと log4j2 と相性が悪くて意図したように設定ファイルを読み込んで初期化ができない。

main ERROR Error processing element EcsLayout: CLASS_NOT_FOUND
main ERROR Unable to locate plugin type for EcsLayout
main ERROR Unable to locate plugin for EcsLayout
main ERROR Could not create plugin of type class org.apache.logging.log4j.core.appender.ConsoleAppender for element Console:
  java.lang.NullPointerException: Cannot invoke "org.apache.logging.log4j.core.config.plugins.util.PluginType.getElementName()"
  because "childType" is null java.lang.NullPointerException:
    Cannot invoke "org.apache.logging.log4j.core.config.plugins.util.PluginType.getElementName()" because "childType" is null

この厄介な問題をデバッグするよりも、すでにうまくいくことがわかっている spring-boot-maven-plugin を使った方が簡単そうだったのでそうすることにした。不要な spring boot 関連の jar なども executable jar や docker イメージに含まれてしまうことだけがデメリット。そこだけ目を瞑れば log4j2 の初期化エラーも起きず、正常に動作した。やっぱり最近のアプリケーションで使われているプラグインはちゃんとしてるねみたいな話しにしておく。次の設定だけでうまくいった。

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <mainClass>com.example.myapp.Main</mainClass>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
        </execution>
    </executions>
</plugin>

log4j2 の設定ファイルの動的な読み込み

0時に寝て7時に起きた。

バッチ処理モジュール

cli でバッチ処理モジュールを作った。コマンドラインの引数パーサーと yml のパーサーを使うことにした。

ロガー実装に log4j2 を使っているので設定ファイルはアプリケーションの設定ファイルと log4j2 の設定ファイルの2つになる。それぞれ環境ごとに用意してエントリーポイントから起動したタイミングで明示的に設定ファイルを読み込むようにした。

log4j2 の yml 設定ファイルを動的にどうやって設定するかはドキュメントにもとくに書いてなかった気がする。log4j2 のソースコードやテストコードを読みながら次のようにしたら反映された。

public static Config load(BatchEnvironment env) {
    var path = String.format("config-%s.yml", env.getName());
    var inputStream = ConfigUtil.class.getClassLoader().getResourceAsStream(path);
    var yaml = new Yaml(new Constructor(Config.class));
    return yaml.load(inputStream);
}

アプリケーションの設定は yml 設定に対応する Config クラスを定義しておいて次のようにして読み込む。

public static void initializeLogSettings(BatchEnvironment env) throws IOException {
    var path = String.format("log4j2-%s.yml", env.getName());
    var inputStream = ConfigUtil.class.getClassLoader().getResourceAsStream(path);
    var source = new ConfigurationSource(inputStream);
    var configuration = new YamlConfigurationFactory().getConfiguration(null, source);
    Configurator.initialize(configuration);
}

ちょっとした cli を作るときにちょっとしたライブラリがあると楽でよい。

欠損金の繰り戻し還付の申請の誤り

国税局から電話がかかってきた。初めて提出した欠損金の繰り戻し還付の申請があちこち間違ってますよと。申請書類と一緒に法人税の申告書もみてもらっていて、還付申請した金額も申告の別表1に記入する必要があって、それも一緒に修正してねという話し。法人税の修正申告と還付の訂正依頼の2つが必要とのこと。税務署の人たちは本当に丁寧で親切にあれが間違っている、これが間違っていると教えてくれる。素人が法人決算やっているので初めて行う手続きの間違いはつきものだけど、税務署の人たちが教えてくれるので本当に助かる。感謝。