Posts for: #Ci/Cd

集中のち寝不足

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

思い立ったらドキュメントを公開

23時に寝て2時頃に少し吐いて起きた。夜遅めに日本酒飲んでいい気分で寝たものの、もう夜に食べたらダメな体になりつつある。4時ぐらいまで起きててそれから寝て7時に起きた。

echo の静的ファイルの扱い

web api のドキュメントは openapi スキーマを使って生成 している。本当はこのドキュメントを gitlab pages で公開させたいのだけど、まだそのインフラ構築ができていなくて先送りになっている。いつもローカルで gitlab ci/cd がビルドしたドキュメントをみていたのだけど、ある機能開発をするときにローカルで web api ドキュメントみるの飽きたなと思って、web api サーバーに同梱してしまえと思い立った。テスト環境の web api サーバーは常に動いているのだから、そこに web api のドキュメントが同梱されていて、なんの不都合があろうか? (いや、なにもない) 。

次のドキュメントに echo で静的ファイルを扱う方法が書いてある。

ミドルウェアで実装されているようで簡単に静的ファイルを返せる。指定したパスのディレクトリ配下を扱えるのが Static で、指定したパスのファイルを扱うのが File になる。web api ドキュメントのようなものならキャッシュしてもいいなとは思ったものの、次の issue によると v4 ではミドルウェアで自前実装しないといけないらしい。v5 ではその仕組みが echo の機能として入るかもしれない。

その後、gitlab ci/cd で web api サーバーのビルド後、openapi.yml からドキュメント生成をして、任意の static ディレクトリに配置するように設定した。docker のマルチステージビルドを使うと簡単にできる。バックエンドやっていて、サーバーとインフラの両方に手を入れて機能を作っていくときの、うまくできると利便性と達成感の両方を得られるのが楽しい。web api サーバーがドキュメントを提供することは、要件に含まれるわけでも、誰かに指示されたわけでもない。私が勝手にローカルでドキュメントみるの飽きたと思って、勝手に作って、勝手に動くようにしただけ。こういう開発の遊びのゆとりや権限をチームのメンバーにも与えられるようにしていきたい。開発が楽しくて悪いことはなにもないと思うんよね。

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

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 を指定する

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

久しぶりにブログを書いた

2時過ぎに寝て7時に起きて9時まで寝てた。

もてなしだけではもう食えない

読み進めておもしろかったし学びにもなったので書評を書いた。

backlog-github-integration-action v1.0.1 リリース

backlog-github-integration-action のバグ修正 した変更を v1.0.1 としてリリースした。2週間ほど検証環境でリグレッションがないかをみていた。問題なさそうなので v1 ブランチにマージして docker イメージを push して v1.0.1 タグを付けてリリース成果物を作成した。久しぶりにやると手順を忘れていてドキュメントを書かないといけないなとか思ったりした。

意図がわかる設計とリファクタリング

1時に寝て7時に起きた。久しぶりに HELLSING をみてた。アレクサンド・アンデルセンの狂信者ノリが好き。

煩雑な保守

昨日から着手した s3 とやり取りするアプリケーションの保守をしている。一通り機能は実装できたが、このアプリケーションの保守を今後どうやっていけばいいのかが私からはみえない。要件が変わる度に継ぎ接ぎで拡張してきて、意図をもった設計があるわけではないようにみえる。このまま保守することはできるかもしれないが、このロジックの説明もテストも検証もすべてが難しい。私がみても難しいのだから、経験の浅い開発者がみるともっと難しいのではないかと思う。

これを直すにはまず単体テストを直さないといけない。単体テストの大半がモックベースなので実際の振る舞いと異なる可能性がある。とくに s3 とやり取りするところの検証ができない。testcontainers の localstack があるので単体テストはモックからこのモジュールを使うように代替できそう。まずはそこからやるべきだが、2-3日はかかると見込まれるのでチームで承認を得られるかどうか、ちょっと聞いてみてから考える。

Job Summary を使ってみた

ちょっと前に github actions のワークフローの実行画面にサマリを出力できるようになったという記事をみた。

自動でよさげなサマリを出力してくれるわけではなく、自分でサマリを作らないといけないので面倒だなと思ってそのまま放置していた。先週末に モジュール別のビルド・デプロイのワークフロー改善 を行った。ふとワークフローの実行結果をみていて、選択したモジュール名が表示されているとわかりやすくていいなと思えた。それを出力する手段としてサマリがちょうどいいやということに気付いた。inputs などで動的に変更するパラメーターをワークフローの実行画面で確認できるといちいちログ確認する手間が省けてよいという場面が他の用途でもある気がしてきた。もっと積極的にサマリを使っていこうと思えた瞬間だった。

放置していたバグを直した

1時に寝て7時に起きた。寝ていて夜中に吐き気して眠れなくて上体を起こすしかなかった。たまにある 胃食道逆流症 のひどいやつ。

参議院選挙

普段は期日前投票で済ませるのだけど、今週は他のことに注意を取られていたせいか、当日行ってきた。場所が期日前と違ったので勝手がわからなかったけど、とくに混雑もしてなかったのですぐに完了できた。タイムラインを眺めると、私のタイムライン上では投票したと発言する人が増えてきたように思う。投票率が50%程度で半分ぐらいの人が投票していない状況に懸念をもつ人たちの可視化がされている。

ふと父の選挙はどうなるんだろう?と思って検索してみた。意思表示できない状態だと選挙はできないみたい。

選挙人本人が投票所に行き自らの意思で投票することが原則であることから、意思表示が困難である場合には投票することはできません。これは投票所の係員が選挙人の投票を補助する代理投票においても同様です。したがって、家族の方が本人に代わって投票することはできません。

神戸市 FAQ > 市政情報 > 選挙

70歳以上は傷病で選挙権を行使できない人たちもいるだろうから減るのかな?と、総務省の 年代別投票率 のグラフを確認してみた。直近だと、70歳以上が61.96, 60歳代が71.43、50歳代が62.96だった。60歳代と比べて減ってはいるけど、50歳代とそう変わらないのをみると、元気な人たちに選挙へ行こうと呼びかけるのは正しい気がした。

backlog-github-integration-action のバグ修正

運用してすぐにコミットメッセージ中にシングルクォートやダブルクォートが含まれると引数を正しくパースできなくてエラーになることがわかっていた。いま運用している環境の用途だと、それほど重要ではないので後回しにしているうちに面倒になって放置していた。晩ご飯を食べてからデバッグしていたら7時間ぐらいやってた。

bash 上の文字列の扱いと action.yml の inputs の args に引数渡しするときの振る舞いの勘違いもあって、issue の見た目以上に複雑な振る舞いをしていることがわかった。github actions 上のコンテキストに依存したくなかったため、github.event.commits の json をそのまま cli パラメーターとして渡している。bash 上の json 文字列と cli パラメーターとしての扱いが煩雑になるのでこのやり方は失敗だったかもしれない。ローカルでのテストもやりにくい。私はそのことをよく理解していたはずなのに github actoins 上のアンチパターンにはまってしまった。カスタムアクションのユーザーが簡単に使えるように cli パラメーターの設定を簡単にする意図で json 渡しにしたんだけど、エスケープの振る舞いが想像以上にややこしくなって、エスケープしたいユーザーには簡単ではなくなってしまった。それでもデバッグをがんばったおかげでシングルクォートは完全に使えるように実装できた。ダブルクォートは制限付きでエラーにならなくできるが、事実上は使わないでくださいといった仕様制限にしてしまおうと思う。引用するときはダブルクォートじゃなくてシングルクォートを使ってくださいと啓蒙することに決めた。

github actions の push イベントワークフロー改善

23時に寝て6時に起きた。今週はサービスインで凸凹していて疲れた。

ストレッチ

今日の開脚幅は開始前161cmで、ストレッチ後164cmだった。ちょっと数値がよくなった。一昨日の日本酒イベントで4時間ほど立ち呑みをしていた疲労で腰に張りが少しあった。それ以外はとくに問題はなくて調子がよかった。右股関節の詰まりも2-3週間前よりもよくなっている気がする。これはトレーナーさんも注意を払って詰まりを取り除くようにストレッチのメニューを組んでくれているのでその成果が徐々に出始めている気がする。以前よりも可動領域が広がってきている気がして安心感を得た。

github actions の push イベントワークフロー改善

午後からビルド・デプロイの最適化のために github actions のワークフローを改善作業をしていた。

今週はサービスインにより、本番環境への緊急リリースを何回もやっているのを傍からみていて、ビルド・デプロイが速くなればなるほど、その回数を増やせるし、修正後の検証に時間を多く割ける。あるリポジトリが7つのモジュールをまとめてビルド・デプロイしている。これはあるモジュールの微修正の反映には向かないので改善することにした。結果として最大で50%ぐらいのビルド時間の削減、モジュールに依っては、具体的には10分かかっていたものを4分台でビルドできるようにした。

ワークフロー改善のためのデバッグしている間はビルド・デプロイが出来なくなることから開発者が使っていない時間帯が望ましい。必然的にサービス休日出勤して github actions のワークフローを改善していた。デバッグと動作の検証も兼ねて午後から半日以上やっていたので休日にやったのは正解だと言えるだろう。

push イベントの github コンテキストの github.event.commits にコミット情報が入っていて、そこにコミットのリビジョンがある。例えば、次のようなオブジェクトで id がコミットのリビジョンに相当する。

[
  {
    "author": {
      "email": "...",
      "name": "...",
      "username": "t2y"
    },
    "committer": {
      "email": "...",
      "name": "...",
      "username": "t2y"
    },
    "distinct": true,
    "id": "f8df1f77ffec9ef234e7321b2e237b663256b01c",
    "message": "コミットログのメッセージ",
    "timestamp": "2022-07-08T12:32:33+09:00",
    "tree_id": "37b066734e58779c5d2c687d40b4cc43af177cb2",
    "url": "https://github.com/OWNER/REPO/commit/f8df1f77ffec9ef234e7321b2e237b663256b01c"
  },
  ...
]

このリビジョンを使って github rest api からファイル情報を取得できる。

$ gh api -H "Accept: application/vnd.github+json" /repos/OWNER/REPO/commits/f8df1f77ffec9ef234e7321b2e237b663256b01c

いろんなデータが返ってくるけど、ここでは変更したファイルのパスを知りたい。

{
  "sha": "...",
  "node_id": "...",
  "commit": {
    ...
  },
  ...
  "files": [
    {
      "sha": "...",
      "filename": "module1/path/to/src",
      ...
    },
    {
      "sha": "...",
      "filename": "module2/path/to/src",
      ...
    }
  ]
}

この filename のトップディレクトリがモジュール名と同じなのでここだけ取り出して、管理対象のモジュールかどうかを比較する。bash でも =~ をサブ文字列のマッチングができる。

$ targets=(mymodule1 mymodule2)
$ echo " ${targets[@]} "
 mymodule1 mymodule2 
$ [[ " ${targets[@]} " =~ " mymodule1 " ]] && echo "match"
match
$ [[ " ${targets[@]} " =~ " mymodule2 " ]] && echo "match"
match

さらにマッチしたモジュールを github actions の expressions で制御しやすいように json の array に変換する。

$ jq --compact-output --null-input '$ARGS.positional' --args -- ${targets[@]}
["mymodule1","mymodule2"]

これを step の outputs として格納する。

echo "::set-output name=modules::${json_array}"

例えば、後続の job で実行条件としてモジュールの有無を調べたいときは expressions を使って次のように記述できる。if 文は ${{ ... }} のブラケットを省略できるようだけど、ここだけ省略すると返って混乱するかなと思って私は記述するようにしている。その方が統合性があってコードが読みやすいように私は考えている。

  mymodule1-job:
    if: ${{ contains(fromJSON(needs.build.outputs.mymodule_target.modules), 'mymodule1') }}
    needs:
      - build

ちなみに workflow レベルの env は job の if 文には使えない。outputs を使って動的な値を扱うようにしている。どうも workflow レベルの env はいろいろ問題があるみたいでなかなか issue がクローズされないのをみると取り扱い注意なのかもしれない。

最終的なモジュールを判別するための step は次のようなものになった。

  build:
    outputs:
      mymodule_target: ${{ steps.mymodule-target.outputs.modules }}
    steps:
    - name: コミットログからビルド対象のモジュールを設定
      id: mymodule-target
      run: |
        declare -A modules
        target_modules=${{ env.TARGET_MODULES }}
        revisions=$(jq --raw-output '.[].id' <<< '${{ env.COMMITS }}')
        for revision in ${revisions}
        do
          names=$(gh api -H "${{ env.ACCEPT_HEADER }}" ${{ env.COMMITS_PATH }}/${revision} | jq --raw-output '.files[].filename' | cut -d"/" -f1 | sort -u)
          for name in $names
          do
            if [[ " ${target_modules[@]} " =~ " ${name} " ]]; then
              modules[${name}]=true
            fi
          done
        done
        targets=$(jq --compact-output --null-input '$ARGS.positional' --args -- "${!modules[@]}")
        echo "::set-output name=modules::${targets}"        
      env:
        TARGET_MODULES: |
          (
            'mymodule1'
            'mymodule2'
          )          
        ACCEPT_HEADER: "Accept: application/vnd.github+json"
        COMMITS_PATH: /repos/${{ github.repository }}/commits
        COMMITS: ${{ toJSON(github.event.commits) }}

処理も理屈も簡単なんだけど、実際の運用コードはもう少しだけ複雑なものの、デバッグは github actions を実行しないといけない。ちょっとした typo のために実は2時間ほどはまっていたのは内緒。シェルの配列や連想配列を使うと、記号が多くてわかりにくい。配列の閉じブラケットを忘れていたがために EOF のエラーが発生していた。閉じブラケットのミスだけにエラーが発生しているところと実際のコードがズレていて、それに気付くのに少しずつコードを足したり消したりしてデバッグするみたいな原始的なやり方で些細な typo を気付くのに時間がかかった。

xxx.sh: line 47: unexpected EOF while looking for matching `"'

個人開発楽しい

0時に寝て7時に起きた。朝から雨降りだったのでだらだらしながら家を出たけど、オフィスに着いたのは9時頃だったと思う。午前中にコードを書いてテストして、それからお昼ご飯食べて、家に戻って、ちょっとゆっくりしてからオフィスに戻ろうと思ってたら3時間ほど寝てた。

backlog のコミット連携の反応

たまたまツィートしていたらこみやさんが関心をもってくれた。fix, close などでチケットのステータスを変えたいという要望をもらったので作ることにした。

3時間ほどとわりとすぐに実装できた。いまのプロジェクトでは使わない機能なので当初は乗り気ではなかったけど、やっぱり使いたいという人がいると開発のモチベーションになる。

機能拡張して、テストしたり、ドキュメント書いたりしてた。github discussions も積極的に使ってみようと思っていてちょっとした faq も書いてみた。

1-2週間ほど試験運用して問題なさそうだったら v1 のタグをつけて marketplace などに公開してもよいかもしれない。ブログにも書かないとな。まだまだタスクは残っている。

カスタム action の開発再開

22時に寝て0時に起きて3時に起きて5時半に起きた。

ワーケーションのリトライ

オミクロン株の流行で延期していた開発合宿を行う。レンタカーと きのいえ の予約を6月3-5日で確定させた。3回目のワクチンを接種したばかりだし、世の中の雰囲気も with コロナへの取り組みになってきている気がする。行き先は同じなので前回作った旅のしおりをコピーしていくつか修正しながら再計画していく。Go To トラベル 再開が6月ではないかという噂もある。

backlog-github-integration-action の機能拡張

先日作った backlog-github-integration-action に push という新たなサブコマンドを追加した。コミットをリポジトリに push したときのイベントをフックしてカスタム action を実行する。インプットが github から取得できるデータになるため、GitHub Events 単位にサブコマンドを作ればトリガーと扱えるインプットデータが一致してわかりやすい機能分割になるんじゃないかと思えた。ひとまずそれでやってみる。今日のところはローカルで動かしてコミット連携ができることを確認して、いくつかテストを書いていた。また明日、結合レベルのテストをやってみる。