Posts for: #Gitlab

結合テストのデバッグ

1時に寝て5時半に起きて7時半に起きた。なんか週の前半からバテている。

gitlab ci/cd の dind で mongodb のレプリカセット接続ができない

先日対応した mongodb のレプリカセット対応 で残った最後の課題。ローカルで実行すれば結合テストは動くが、gitlab ci/cd 環境では動作しないという問題が残っていた。gitlab-runner をローカルで実行できる ようにして、設定やパラメーターを変えたり、デバッグコードを埋め込んだり、コンテナに attach して振る舞いを確認したり、いろいろデバッグして原因はレプリカセット接続におけるホスト名の解決がコンテナ間でできていなかったことがわかった。

mongodb の結合テストは dockertest を使って実装している。これを gitlab ci/cd で動かすには dind を有効にする必要がある。dind 環境では2つのコンテナを使って結合テストが実行されるわけだが、テストが実行されるコンテナと mongodb コンテナが起動するコンテナの2つが生成される。このときにテストが実行されるコンテナから実際に mongodb が起動するコンテナのホスト名の解決と、mongodb が起動するコンテナ上での自分のホスト名の解決の2つが成立していないとレプリカセット接続ができない。要は1台のローカルホスト上で結合テストを実行するのと、2つのコンテナ上で実行されるのでは設定を変更する必要があるということに気付いた。

具体的には dockertest の次のパラメーターを、実行環境から解決するホスト名を考慮して設定すればよいと気付いた。

pool.RunWithOptions(&dockertest.RunOptions{
    ... 
    Hostname:   executor,
    Env: []string{
        ...
        fmt.Sprintf("MONGODB_ADVERTISED_HOSTNAME=%s", executor),
        ...
    }
})

たったこれだけの修正だし、現状の動作の振る舞いが分かればすぐに直せるものではあるけれど、このデバッグにはまた2-3時間を費やした。mongodb のレプリカセット接続はなかなか大変。

gitlab ci/cd のローカルデバッグ

23時頃から寝始めて3時に起きて5時半に起きて8時過ぎに起きた。久しぶりに寝坊した。

gitlab-runner のデバッグ

mongodb のレプリカセット対応して、ローカルでは結合テストが動くものの、gitlab ci/cd 環境では動かなくなった。gitlab ci/cd は GitLab Runner によって提供されている。そのデバッグのため、ローカルに gitlab-runner をインストールして調査した。

GitLab Runner のインストール ドキュメントにそれぞれの OS 向けのドキュメントがある。Debian/Ubuntu/Mint 向けのインストール手順を行う。

$ curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
$ sudo apt-get install gitlab-runner
$ gitlab-runner --version
Version:      16.6.1
Git revision: f5da3c5a
Git branch:   16-6-stable
GO version:   go1.20.10
Built:        2023-11-24T21:11:36+0000
OS/Arch:      linux/amd64

.gitlab-ci.yml があるディレクトリへ移動して、ジョブを指定して実行する。ローカルでの変更内容を検証するときはブランチにコミットしないといけない。コミットしていないと次のワーニングが表示される。

WARNING: You most probably have uncommitted changes. 
WARNING: These changes will not be tested.         

dind なジョブを実行するときは --docker-privileged で特権を付けて実行する。環境変数は --env KEY=VALUE で渡せるが、CI_JOB_TOKEN のような組み込みの環境変数は上書きできない。

$ cd path/to/repo
$ gitlab-runner exec docker --docker-privileged ${ジョブ名}

資料作りの一日

23時に寝て何度か起きて6時に起きた。疲労も溜まっているのでなるべく早く寝るように努めている。旅行中ずっと早起きしていたから早起きするのは苦にならない。

今開発の大きなふりかえりの資料作り

ふりかえりのために課題管理システムの issue 情報から統計的な数値を取得する。

gitlab の cli ツールを使って issue 情報を取得して mongodb にインポートする。

$ glab api --paginate "groups/product%2Funicorncidm/issues?milestone=2023-09-05&not[labels]=Duplicate,Invalid,Wontfix" | jq -c '.[]' > 2023-09-05-issues.json
$ mongoimport --authenticationDatabase=admin --uri "mongodb://root:secret@localhost:27017" --db gitlab --collection issues 2023-09-05-issues.json

mongodb で aggregation (sql で言うところの group by 句に相当する) するときはパイプライン処理を実装する。例えば、最初にデータをフィルターして、次にグルーピングして、最後にソートするのは次のようなパラメーターになる。

$ mongosh --username root --password secret
test> use gitlab
gitlab> db.issues.aggregate([
  { $match: { $or: [ { "milestone.title": "2023-09-05" },{ "milestone.title": "2023-09-19" } ] } },
  { $group: { "_id": { milestone: "$milestone.title" , author: "$author.username" }, councount: { "$sum": 1 } } },
  { $sort: { _id: 1 } }
])

これで前回の開発のときに取得した数値と今回の開発の数値を比較してみると、いろいろわかることもあって、メンバーが成長していることも伺えて、課題管理をしながら開発を進めることのメリットを実感できる開発になったのではないかと思う。gitlab のアクティビティ図 (草生え図) をみても前回の開発よりも草がたくさん生えているので課題管理に習熟している様子が伺える。これをあと2-3年ぐらいすれば、課題管理を使いこなせる一般の開発者になるのではないかと思う。課題管理 + イテレーション開発でチームビルディングしていくところの、地盤のようなものはできたようにみえる。

この草生え図を匿名化して利用許可をもらって、うちの会社でいうところの課題管理ができるようになると、メンバーの働き方はこうなるというサンプルとして紹介させてもらうつもり。また来週メンバーにもその許諾を取ろうと思う。

次開発の要件打ち合わせの資料作り

今開発を始めるときに洗い出した要件の未対応のものと、今開発でやり残した非機能要件の開発課題を資料にまとめてたたき台とした。それだけでも3-4ヶ月分の開発課題になりそうだし、アーキテクチャとして大きな意思決定も1つある。私自身、7割型は決まっているが、考え方や要件によってはもう1つの案でいくのもよいかもしれない。機能要件は実際にお客さん先へ導入するときにもいくつか出てくるだろうけれど、非機能要件の運用に影響を与える懸念のところはなるべく早く改善しておきたい。次の開発期間にそこだけ対応すれば、一定の安心をもってお客さんへ提供できるようになると思う。

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 }
]

gitlab packages api の使い方

0時に寝て何度か起きて6時半に起きた。先週より少し早く起きれるようになってきた。

gitlab ci/cd で別プロジェクト (リポジトリ) の成果物を取得する

gitlab では複数のパッケージリポジトリに対応しているが、それらに当てはまらない汎用の成果物向けに GitLab Generic Packages Repository というものがある。zip でもバイナリファイルでも何でも置くためのリポジトリと言える。但し、同じパッケージ名でバージョン管理するといった作りにはなっていなくて、同じパッケージ名でアップロードしても別のパッケージ id が割り当てられて管理される。他バージョンとの紐付け自体はできているので、おそらく歴史的経緯でそういう仕様なのだと思う。そのために、あるパッケージの最新のバージョンを取得したいときは作成日の降順でソートして最初のパッケージを取得するといったコードを書かないといけない。それは Packages API を駆使して簡単なスクリプトを書くことになる。

もう1つ分からないことにトークンの使い分けがある。なるべく ci/cd での処理は GitLab CI/CD job token を使いたいところだが、どうも Packages API の呼び出しはできなくて別途プロジェクトでアクセストークンを作成して呼び出すようにしている。これはもしかしたら別の設定で CI/CD job token でも呼び出しできるかもしれない。rest api への呼び出し権限そのものがないのかもしれない。

最終的には次のようなスクリプトで任意のプロジェクトの generic リポジトリの最新の成果物を取得できた。

rm -rf ${TARGET_DIR}
mkdir -p ${TARGET_DIR}
for project in $PROJECTS
do
  prj=$(echo "$project" | jq -Rr @uri)
  base="${CI_API_V4_URL}/projects/${prj}"
  pkg=$(curl -s -H "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" "${base}/packages?order_by=created_at&sort=desc&per_page=1" | jq '.[0]')
  pkg_id=$(echo $pkg | jq -r .id)
  pkg_name=$(echo $pkg | jq -r .name)
  pkg_version=$(echo $pkg | jq -r .version)
  file_names=$(curl -s -H "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" "${base}/packages/${pkg_id}/package_files" | jq -r '.[].file_name')
  for file_name in $file_names
  do
    dw_endpoint="${base}/packages/generic/${pkg_name}/${pkg_version}/${file_name}"
    curl -s -H "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" "${dw_endpoint}" -o "${TARGET_DIR}/${file_name}"
  done
done
find ${TARGET_DIR} -type f

集中のち寝不足

1時に帰ってきてそのまま寝ないで6時10分の新幹線に乗ってから2時間半ほど寝た。これはこれで時間の使い方が有意義な気がする。午前中は翌日の定例会議の準備を着々と進めて、ci/cd 環境の改善、午後からリファクタリングなどをやっていた。15時をまわると眠くなってきて散歩したりして気分転換しつつも体調悪いなと思って17時半にお仕事を終えてホテルへ戻って2-3時間ほど寝てた。その後、晩ご飯食べるかなと出掛けたものの、あまり食欲もなくて、2時間ほど付近を散歩して運動していた。たまにはそういうのもいいか。飲食店が多い地域なので外から眺めているだけでもわりと楽しい。

ssh 経由のデプロイ

これまで ci/cd でテストして docker イメージをビルドしてコンテナレジストリに登録するところまでやっていた。実際にテスト環境にデプロイするときは、テスト環境にログインして更新用のスクリプトを私が手動実行していた。そんなに頻繁にテスト環境を更新する必要がなかったのでそれでも十分ではあるものの、ci/cd の完成形を目指すなら自動化すべきという考え方もあってデプロイの部分を作ることにした。

もっとも簡単な方法として Using SSH keys with GitLab CI/CD をみながら、ssh でテスト環境にデプロイすることにした。すでに更新用のスクリプトがあって、テスト環境にログインして実行すればできる状態なので ssh さえ使えればすぐに移行できるという話しでもある。openssh-client を使うためにベースイメージを alpine から ubuntu にしてパッケージをインストールしないといけない。実行時間がややかかるというコスト以外には気にならないかな。ssh の秘密鍵を file 種別でもつのか通常の環境変数でもつのかで扱いが異なって、それに少しはまったぐらいですぐできた。今後は docker イメージのビルド単位に自動的にデプロイされるようになる。

interface{} の型エイリアスとしての any

go のコードをリファクタリングしていて json.Marshal の引数が any となっていることに気付いた。

func Marshal(v any) ([]byte, error) {
    ...
}

go 1.18 以降で interface{} の型エイリアスとして any が定義されているらしい。任意の型を扱えるシグネチャとして、メソッドの振る舞いのみを規定する interface{} を使うというのは型システムとしては正しい。他言語でいえば object に相当するものが go はオブジェクト指向言語ではないのでそれがない。そういう間違っていないけど、わかりにくいなと思っていたものに any という名前の型エイリアスが導入されてとてもしっくりきた。プログラミングしていて、実務的にどうかというところをちゃんと改善していくところがみえるのは楽しい。

type any = interface{}

dind をやってみた

3時に寝て7時半に起きた。最後なのでワールドカップの決勝戦をみてた。接戦で試合もおもしろかったしよかったと思う。

gitlab ci/cd で docker in docker

ミドルウェアを伴う結合テストは dockertest というツールを使って docker でミドルウェアを起動して実行している。デフォルトで作成した gitlab runner で docker を使おうとすると失敗する。これは gitlab runner が ci/cd ジョブを docker で動かすため docker in docker (これを dind と呼ぶらしい) のための設定が必要になる。大雑把に言えば gitlab runner にそのための権限を設定する必要がある。gitlab の次のドキュメントに詳細が書いてある。

gitlab runner に権限を設定したら次のような job が動けば docker in docker は成功と言える。

hello-dind:
  stage: test
  image: docker:20.10.21
  variables:
    DOCKER_HOST: tcp://docker:2375
    DOCKER_TLS_CERTDIR: ""
  services:
    - docker:20.10.21-dind
  allow_failure: true
  before_script:
    - docker info
  script:
    - docker run hello-world

あとになって気付いたことだけど、dockertest の README にも Running dockertest in Gitlab CI としていくつか tips が紹介されている。dockertest で作成したリソースからホスト名とポート番号を取得するには次のようなユーティリティを使う必要がある。

func getHostPort(resource *dockertest.Resource, id string) string {
	dockerURL := os.Getenv("DOCKER_HOST")
	if dockerURL == "" {
		return resource.GetHostPort(id)
	}
	u, err := url.Parse(dockerURL)
	if err != nil {
		panic(err)
	}
	return u.Hostname() + ":" + resource.GetPort(id)
}

docker compose に期待しない

0時に寝て5時に起きて7時に起きた。起きたら冷やしたのかお腹痛かったが、まぁまぁ眠れたと思う。

テスト環境の構築

GitLab CI/CD にだいぶ慣れてきてジョブを追加したり改善したりしながらようやくアプリケーションの docker image もコンテナレジストリに push されるようになった。それを pull してきて、テスト環境を docker compose で構築する。Use Compose in production とドキュメントでは威勢がよいが、これが全然イケてない。複数の compose.yml で項目によっては変更したいところを置き換えるといった振る舞いになっていない。例えば、ポート番号などを dev と prod で置き換えたいといった運用の要件を考える。

  • dev.yml
services:
  myapp:
    ports:
      - 18080:8080
  • prod.yml
services:
  myapp:
    ports:
      - 8080:8080

これを次のように指定すると、

$ docker compose -f dev.yml -f prod.yml up -d

実際のサービスは次のように振る舞う。全然あかん。

services:
  myapp:
    ports:
      - 18080:8080
      - 8080:8080

他にもそれぞれの yml ファイルで読み込む environment file のマージなどもよくわからない振る舞いをしていて複数の compose.yml で制御するのは断念した。dry の原則に反して設定は重複するけど、それぞれの環境を個別に compose.yml として管理した方が保守コストは小さくなると私は判断した。複数の compose.yml の使い分けのデバッグを1-2日やった後に諦めてテスト環境の構築は完了した。

年金事務所の住所変更手続き

先週 法務局で法人登記の変更申請 をしていて、そのときに問題がなければ今日から登記事項証明書を取得できると案内をもらっていた。決定書が漏れていて再提出というトラブルはあったものの、最小限の損失で留めたせいか、問題なく登記事項証明書を発行できた。住所の変更だけわかればよいので履歴事項証明書ではなく現在事項証明書を発行してみた。この書類もおもしろくて1つ前の住所といまの住所の2つを確認できる。法務局へ行った帰りに年金事務所へ立ち寄って社会保険の住所変更の手続きを行った。次の3つの書類をもって窓口へ。

  • 登記事項証明書: 番地まで記載されている
  • オフィスの一時使用契約書: ビル名はあるがこのビル名は来月に改名
  • ビル名変更の証明書類: ビル名の変更のみが記載されている

この3つの書類で完全に指定された住所 (Fully Qualified Address: 造語) を丁寧に説明したところ担当者に納得してもらえて事なきを得た。

openapi-ext-tools をまた使う日がきた

0時に寝て4時に起きて7時に起きた。わりとよく眠れた。

ストレッチ

トレーナーさんと月曜日の日本対クロアチア戦の感想を話したりしていた。今日の開脚幅は開始前153cmで、ストレッチ後156cmだった。先週は疲弊と疲労で散々な数値になっていたものが復調してきつつある。今週も毎日8-22時はオフィスで缶詰め状態だった。たくさん座っている (同じ体勢でいる) 時間が増えると筋肉にはよくない。まだまだ右腰と右太もも周りの張りは強く復調にはもう少し時間がかかるようにみえる。一方で忙しさのピークを越したと思うので今週以降は少しペースダウンしながら体作りをしていく。いまお手伝いしている開発は12月にすべての集中力を費やしてもよいと考えている。残りは期間はメンバーに委譲するような体制になるとベストかもしれない。そのための体力づくりは重要。

openapi-ext-tools 再び

github pages ならぬ gitlab pages がある。ふと web api のドキュメントを作るために openapi のスキーマを定義したら gitlab の ci/cd と連携できていいんじゃないかと思い付いた。スキーマがあればフロントエンドのクライアント生成や e2e テストコードの自動生成などに使えるかもしれないし。過去に作った openapi-ext-tools を oss にしておいたからいまも使える。oss 万歳。先のことはわからない。redoc を使ってちゃっちゃと実装した。

pages:
  only:
    changes:
      - schema/*
  stage: deploy
  image: alpine:latest
  before_script:
    - apk --no-cache add python3 nodejs npm
    - python --version
    - python -m ensurepip
    - pip3 --version
    - node --version
    - npm --version
    - npm install --global redoc-cli
    - redoc-cli --version
    - pip3 install openapi-ext-tools
    - pip3 freeze openapi-ext-tools | grep openapi
  script:
    - openapi-spec-cli --spec-path schema/openapi.yml
    - |+
      redoc-cli bundle bundled_openapi.yaml \
        --output index.html \
        --options.expandResponses=all \
        --options.requiredPropsFirst=true \
        --options.jsonSampleExpandLevel=10 \
        --options.hideLoading=true \
        --options.pathInMiddlePanel=true      
    - mkdir -p public
    - mv index.html public/
  artifacts:
    paths:
      - public

久しぶりに触ったら openapi-ext-tools が依存ライブラリの変更で動かなくなっていたので直した。

gitlab の ci/cd 入門

0時に寝て3時に起きてそのまま眠れずにいたら6時になって7時過ぎから準備してオフィス行ってお仕事を始めた。

gitlab の ci/cd の調査

初めて GitLab CI/CD を触っている。まだ触り始めたばかりだが、感覚的には github actions 相当の機能はあるようにみえる。ソースコードリポジトリやパッケージリポジトリ/コンテナレジストリと ci/cd がセットになっているととても便利だ。これはすごいことだと最近思うようになってきた。もはやソースコードリポジトリのみのホスティングビジネスは成り立たない。なぜなら github や gitlab のような ci/cd が当たり前になってしまうと、その機能がない場合、デメリットを上回るメリットがないとそんなソースコードリポジトリを選択しない。

docker image をビルドして push する仕組みは既にメンバーが作ってくれていたのでその後始末の処理を作った。Container Registry API を使うと、不要な docker image を削除できる。

削除向けに便利な api 設計になっている。こういう細かい配慮は嬉しい。keep_n で最低限残すイメージ数を指定して older_than で過去何日より古いイメージを削除対象とするといったよくある運用の設定ができる。

curl -s -H "PRIVATE-TOKEN: $PROJECT_ACCESS_TOKEN" -X DELETE "${endpoint}" \
  --data "name_regex_delete=.*" \
  --data "keep_n=${KEEP_N}" \
  --data "older_than=${OLDER_THAN}"

あとは認証のトークンを指定する方法として私が調べた限りだと2通りある。

  • (ci_job_token_scope の feature flag を有効にして) $CI_JOB_TOKEN を使う
    • こっちの方が一時トークンなのでよりセキュアなはず
    • この場合はヘッダーに JOB-TOKEN を指定する
  • プロジェクトレベルのアクセストークン を発行して ci/cd の variables に登録する
    • トークンが漏洩したときにプロジェクトレベルで被害が発生する
    • この場合はヘッダーに PRIVATE-TOKEN を指定する

使うトークンによってヘッダーが変わるというのがちょっと変な認証の設計にもみえるけど、まぁそれぐらいしか気にはならない。

gitlab を使う開発のお仕事

0時に寝て何度か起きて、なぜか寝坊して9時に起きた。おそらく休日以外で9時まで寝ていたことはこの日記を書き始めて初めてだと思う。

次のお仕事の意識合わせ

来月から新しい取引先のお仕事を手伝う。先方と業務内容の確認のための打ち合わせを行った。先方はパッケージベンダーになる。私の大先輩にあたる方々が働いている会社だし、当社が目指すパッケージベンダーとしてのお手本のような会社でもある。大いに学ぶところがある業務になるだろうと想定している。私はここ数年 web 業界で働いてきたせいか、やり取りをしていても勝手の違いを少し感じる。それは web 業界が緩過ぎるからだと思う。気を付けないと先方からみて失礼になってしまうかもしれない。契約や初期のオンボーディングも兼ねて10月31日から1週間ほど東京出張する予定になる。

またリポジトリに gitlab を使っている。課題管理や ci/cd も基本的には gitlab で行うことになる。私のもっている github のドメイン知識はまったく活かせないが、gitlab の機能を学ぶよい機会でもある。gitlab と聞くと私は一番先に Remote Manifesto を思い浮かべる。

  1. Hiring and working from all over the world (instead of from a central location).
  2. Flexible working hours (over set working hours).
  3. Writing down and recording knowledge (over verbal explanations).
  4. Written processes (over on-the-job training).
  5. Public sharing of information (over need-to-know access).
  6. Opening up documents for editing by anyone (over top-down control of documents).
  7. Asynchronous communication (over synchronous communication).
  8. The results of work (over the hours put in).
  9. Formal communication channels (over informal communication channels).

https://about.gitlab.com/company/culture/all-remote/guide/

私の考える課題管理とも相性がよく、時間よりも成果、書くことや非同期コミュニケーションの重要性、組織の透明性を簡潔に表現したマニフェストとなっている。うちの会社はまだ社員採用できる状況ではないが、社員を採用する時期になったらこういったマニフェスト的なものを自社でも作りたいと思う。そのときの最も参考になるものだと思う。