tech.guitarrapc.cóm

Technical updates

Git GUIクライアントと GitKraken と Fork

Git の GUI クライアント、いろんなツールがあってそれぞれ使いやすさがあります。

ここしばらくはForkをメインで使っていた中で、私がForkに感じた良さと苦手なことをメモしておこうと思います。

普段私は、GitKraken をメインにしていますが、サイズの大きなリポジトリでは Fork を利用しています。(ここ最近は手になじませるため、Forkをメインにしている)

私がgitで利用するのは、主にコードのコミット (Unity や C#、Go、React 他)、並びに Git LFS です。 バイナリファイルや、PSD や画像ファイルを編集してコミットする機会は少ないので無視します。

コミット頻度は高く、ブランチを切ったり、マージ操作、ファイルごとの差分確認、stageのフラット/ツリーでの確認、コンフリクト解消を重視しています。

tl;dr;

  • SourceTree 使っているなら、 Fork は上位互換。有料なのだけネック。
  • GitKraken 使っているなら、 Fork はgit操作が高速でUIもロックされないのが快適。ただ、UIのコンセプトが違うので一つ一つの操作が手間に感じる。
  • Git GUI クライアント、まだまだ全然決定版がない。

SourceTree使ってた、 あるいは10GB 超えるようなリポジトリだったら、私は Fork 使います。そんなに大きなリポジトリじゃないなら、GitKraken 使っているのが幸せです。

GitKraken が Fork の早さをもって、ファイルロックが起こらなければ最強といえるんですがそんなものはない。あるいは、Forkが GitKraken のように操作ごとの視点の動きを考慮したUIになればいいですが、そんな未来も来ないでしょう。諸行無常。

Fork とは

Fork は、無料で試用ができる有料Git GUIクライアントです。$49.99 (One-time purchase)

試用のみ無料ですが、無料版は現在存在しないと明言されています。その上の発言と一貫性取れてない気がするけど。有料の価値はあるいい製品です。

git-fork.com

OAuth 認証が必要なので、Organizationで使うには Fork をOAuth 許可する必要があります。

SourceTree と非常に似通っているので、SourceTreeを使っていたら違和感なく移行できるでしょう。 Git操作が高速なのが特徴で、例えば10GB越えのリポジトリでUnity で3DモデルやLFSを使っていても、checkout、diff、stage、commit、push、pullのいずれも重いと感じることなく操作ができます。

製品としては、SourceTree の高速版という印象でほぼずれはないと思います。

SourceTree比較のFork

SourceTree を使っていると Fork は気持ちいいぐらい使いやすいと思います。

有料なだけの価値があります。

pros:

  • どの操作でも固まらない
  • ファイルdiff 違和感はなく高速。
  • 操作UI も違和感ほぼなし
  • push/pull が爆速
  • checkout が爆速
  • tree が SourceTree より見やすい

cons:

  • 有料
  • OAuth 認証が必要

特筆

特筆することはないです。 SourceTree 使っているなら使いやすくて軽い、でも有料かー。という印象に落ち着くと思います。

GitKraken比較のFork

GitKraken で重いと感じているならFork は高速にgit操作できるのですごくいいです。 一方で、GitKraken に慣れている場合、Forkは一つ一つの操作で目線を移動する必要があり一貫した操作性がないのが使いにくいと感じます。

有料の価値があるかは、GitKrakenが重いと感じるか次第。

pros:

  • push/pull が爆速。
  • checkout が爆速。
  • リポジトリごとの操作が非同期でロックされないので快適。
  • 素直なgit。hooks など妙な挙動がない。
  • カスタムコマンドができる。(私はいらないけど)

cons:

  • diff をファイルの下に出すのはごみといわざるをえない。
  • 操作していて、視点があっちこっち見ないといけない。
  • 画面内でタブを使っているので、一覧しても見えないものがある。
  • branch作成、ammend など些細な操作が厳しい。
  • tree が厳しい、見やすくはない。
  • repo 初期化はセルフサービス。
  • repo 追加もセルフサービス。検索などない。
  • Conflict 解消がやりにくすぎる、Forkでやるの無理では...
  • SSH 鍵生成などもセルフサービス。
  • PR は Web でどうぞスタイル。
  • ライセンス管理は原始的。

特筆

視点の移動はかなりあって、私がGitKraken に慣れてるときに Fork を久々に触って使いにくく感じた原因はこれです。 コンセプトの違いはあると思いますが、油と水ぐらい違う。

またツリーに関しては、好みがありますが、結構読みやすさが違います。 個人的に Fork のツリーはよくある表示ですが、コミットメッセージに食い込んでて、普段見せたいもの(head/changeの有無/コミット一覧)と、必要に応じて見たいもの(ブランチ名、commit user、commit id、commit日付) の区別がついていないと思います。

f:id:guitarrapc_tech:20211104020627p:plain
上 Fork の ツリー、下 GitKraken の ツリー

GitKraken の評価

Forkだけ評価しても一方的なので GitKraken も書きます。

GitKraken は、個人利用は無料利用が可能な有料Git GUIクライアントです。$4.95 per user/month (paid annually)

無料版だと OSSのみ + VCS が github.com だけだったり制約があります。有料の価値はあるいい製品です。

www.gitkraken.com

OAuth 認証が必要なので、Organizationで使うには GitKraken をOAuth 許可する必要があります。

総じて良く、私は個人的には GitKraken が最も好きな GUI Git クライアントです。 リポジトリが小さい限りは cons に挙げたデメリットはほぼ発生することなく、非常に快適に使えます。 いろいろ GUI Git を使ってきていますが、最も操作しやすく、UIとツリーが洗練されていると感じます。

Fork とは全然違うので、pros/cons を挙げておきます。

pros:

  • git 操作で操作時に視点を移動させないでいい
  • ツリーが見やすい
  • diff を見ながら表示を3パターンに変更できる。(Hunk/Inline/Split)
  • Conflict 解消がしやすい、優秀。
  • Terminal 統合ができるので、CUI 派でも使いやすい。Terminal なら重くもないし。
  • LFS 操作に意識することがない (LFSが記述された .gitattributes がルート配下にある前提)
  • Profile があり、Profileごとにメアド、VCS認証など設定を持つことができます。
  • git terminal の設定や SSH鍵の生成、GitHubへの登録など一通りの操作が完結する。
  • 有料版で各種VCS に対応している。(商用するなら有料版なので自然とそうなるはず)
  • 商用で、ライセンスのシート管理が可能。Proでも個人でライセンス管理しやすい。

cons:

  • カスタムコマンドはない。
  • 商用利用には有料版が必要。
  • .git が大きなリポジトリ (10GB超えぐらい) でpush/pull が重い。(diff/stage/commit は影響なし、push/pull/checkout が影響を受けやすいです)
  • 一つの操作ごとにUIがロックされるので、連続して別のリポジトリタブに行って操作などができない。
  • git hooks に余計な処理を挟んでおり、まれにバグって詰む (どうしようもない)
  • GitKraken がgit内部のファイルをつかんでいることがあり、Windowsでファイルを消せないことがまれによくある。
  • ディレクトリをロックしていてチェックアウト失敗することがまれによくある。
  • background fetch が走っていて、無言で操作できないことがまれによくある。

特筆

Gitkraken で日常的に困る可能性があるのが、リポジトリが大きいと重いこと & 操作のロックでしょう。 また、サイレントにファイルロックしていることによるストレスがたまにあります。

一方で、使いやすさに関してはすごくいい... UI設計者すごい、尊敬します。

基本的に、GitKraken で git の一連の捜査 (clone, fork、pr) が完結することが目指されており、実際実現できています。 目指すゴールとコンセプトがはっきりしているのは良いことです。

UIの秀逸さ

GitKranen は、git 操作における視点をよく考えてUI/UX が設計されていると思っています。 画面内の要素はタブなどで表示が隠されていることがないので、操作によらず一貫した見方ができます。

また、git操作で視点はばらつくことがほぼないのが特徴です。例として、次のような画面を用意しました。

f:id:guitarrapc_tech:20211104000337p:plain
GitKrakenのUI

  • ブランチの操作をしたいときは左ペインを見て、中ペインはツリーを示す。
  • コミットのためのファイル操作をするときは右ペインを見て、中ペインに大きくdiff を示す。
  • よく使うgit操作の Pull/Push/Branch/Stash/Pop/Terminal などは上部にボタンを用意

操作を追ってみましょう。 常に視点が、 中ペイン + 左ペイン or 右ペイン + 中ペイン と動かさなくて済むようになっているのがわかります。

ブランチを切る

まずはブランチを切ります。

やりたいことに必要なのは、ツリーの状態とブランチ操作です。 そのため、視点は中ペイン (ツリー) と左ペイン (ブランチ) に限定されます。

  • ツリーの状態を見る (中ペインのツリーを見る)
  • ブランチ状況を確認する (左のブランチ一覧を見る)
  • ブランチを切る (左のブランチ一覧から追加 -> 中ペインのツリーを見る)

f:id:guitarrapc_tech:20211104000810p:plain
ファイル変更があるときの画面1

コミットをする

ローカルのファイル変更をしたのでコミットをします。

やりたいことに必要なのは、ツリーの状態とファイル一覧とファイルごとのdiff です。 そのため、視点は中ペイン (ツリー/diff) と右ペイン (ファイル一覧) に限定されます。 diff の時に、中ペインがツリーではなくdiff を大きく表示するのがよく考えられていると思います。

  • ローカル変更を確認する (右のファイル一覧、ファイルごとのdiffは中ペインのdiffを見る)
  • stage 操作をする (右のファイル状況で変更を確認)
  • コミットする (右のファイル状況で変更を確認)

f:id:guitarrapc_tech:20211104000858p:plain
ファイル変更があるときの画面 2

f:id:guitarrapc_tech:20211104000952p:plain
ファイル変更があるときの画面3

Push & PR作成

コミットしたので、pushしたり PR を作ったりします。

やりたいことに必要なのは、ツリーの状態とブランチ操作です。 そのため、視点は中ペイン (ツリー、pushボタン) と左ペイン (ブランチ一覧、PR一覧) に限定されます。

  • プッシュする (上の操作ペイン)
  • PR を作る (左のブランチ一覧を見る)

f:id:guitarrapc_tech:20211104001112p:plain
PRを作るときの画面

文章とスクショで言われてもよくわからない場合、無料版を触るといいです。

.git が大きなリポジトリ (10GB超えぐらい) でpush/pull が重い

3GB 程度なら快適ですが、10GB 程度になると push / pull が重くなります。 これは明確に CPU やネットワークにかかわらず起こるので欠点です。

fork ぐらい早くなれば完全にお勧めできるんですが、重いを感じるレベルになるリポジトリでは欠点が目立つ可能性があります。

git hooks のつらさ

めったにないのですが、この間起こって詰みました。 以前は適当に clone しなおしたら行けたんですが今回はダメっぽくて困りました。(今見たらいけてる、謎)

Fork の縦ペインで視点移動は減らせるのか

Fork を水平、縦でペイン表示変更してみて視点移動を減らせるか試してみたので、Twitter にあげたのをぺたり。 結果は、縦なら少しは良くなるけど、diff がどうしようもなく使いにくい。

LinqPad の設定

LinqPad 7 へのアップグレードが可能になっています。 今現状は LinqPad 7 ではなく 6になりますが、すでに LinqPad 6ライセンス持っている人は今なら早期アップグレードでディスカウントが大幅に利くのでお得です。具体的には、Premium 使ってても Pro の新規ライセンスより安くなるので好き。

Windows 11 でクリーンしたついでに LinqPad も入れなおしたので設定ついでにメモ。

tl;dr;

  • LinqPad で C# を書くときに便利にしている設定を公開する
  • 基本デフォルトに沿うようにしているので、最低限しか設定しない。

before & after

f:id:guitarrapc_tech:20211017072515p:plain
before

f:id:guitarrapc_tech:20211017072622p:plain
after

設定一覧

設定は基本的に、Edit > Preference から行えます。

Edit > Preference > Editor

  • Show line numbers in editor: True (行を表示します)

f:id:guitarrapc_tech:20211017070706p:plain
Edit > Preference -> Editor

Edit > Preference > Query

  • Default Query Language: C# Program
  • Enable Nullable Refrence Types in C# queries by default: True

f:id:guitarrapc_tech:20211017070636p:plain
Edit > Preference -> Query

Edit > Preference > Advanced

  • Convert tabs to spaces: True

f:id:guitarrapc_tech:20211017071654p:plain
Edit > Preference -> Advanced

LinqPad はいいぞ

会社だと Enterprise がやばいぐらいお得なのでおすすめ。C# 書くなら福利厚生といえるかもしれません、しらんけど。

Enterprise License (unlimited users, up to 10 locations) が $1390 は安すぎる。

f:id:guitarrapc_tech:20211017072204p:plain

GitHub Actions のローカル Composite Action で歯がゆいこと

GitHub Actions の Composite Action (複合ステップアクション) は便利なのですが、制約や歯がゆいことが多く悩ましいものがあります。

では何が難しいと感じているのか、その対処をどうしているのかメモしておきます。

tl;dr;

  • Composite Action は run のみ使える。uses は使えないからあきらめて。
  • Composite Action は run.if が使えないので bash if で分岐しよう。
  • Composite Action でスクリプト使うならコンテナ実行時にパス狂うから気を付けて
  • Compoiste Action の全 run ステップは Grouping log lines を使おう、絶対だ。

Composite Actions とは

GitHub Actions は、Jobで実際にやる処理一つ一つを step として記述できます。 この step で run: を使っていろいろな処理を書いたり uses: を使ってアクションを呼び出したりしていることでしょう。

さて、プロジェクトでいろいろな workflow を用意していくと、似通った run step を記述していてまとめ上げたくならないでしょうか。 TypeScriptやDockerアクションにするというわけではなく、単純に run step のYAMLを分離して呼び出すことで共通化したい。

こんな時に便利なのが Composite Action です。

docs.github.com

Composite Actions の利用例

例えば、次のように jobA, jobB, jobC それぞれで dotnet build / publish をしているときに、このdotnet 処理を別のYAMLに記述して呼び出せれば便利、みたいな感じです。(これを分離するのに価値があるかはおいておいて、まとめ上げられるというのに注目)

jobs:
  jobA:
    runs-on: ubuntu-latest
    steps:
      - run: dotnet restore
      - run: dotnet build -c Debug
      - run: dotnet publish -c Debug
      - run: nanika yaru

  jobB:
    runs-on: ubuntu-latest
    steps:
      - run: dotnet restore
      - run: dotnet build -c Debug
      - run: dotnet publish -c Debug
      - run: betsu no nanika yaru

  jobC:
    runs-on: ubuntu-latest
    steps:
      - run: dotnet restore
      - run: dotnet build -c Release
      - run: dotnet publish -c Release
      - run: tondemo naikoto yaru

Composite Actions を使うようにしてみましょう。 やることは単純です。ローカルAction として、.github/actions/dotnet_build/actions.yaml を定義して、dotnet build の記述を移します。 外から実行に値を受けるなら、inputs で指定するのは workflow_dispatch などと同じで一貫性が取れています。

name: .NET Build
description: |
  .NET Build
inputs:
  build-config:
    description: "dotnet build config. Debug|Release"
    default: "Debug"
    required: false
runs:
  using: "composite"
  steps:
      - run: dotnet restore
        shell: bash
      - run: dotnet build -c ${{ inputs.build-config }}
        shell: bash
      - run: dotnet publish -c ${{ inputs.build-config }}
        shell: bash

あとは、元の workflow で呼び出すだけです。簡単ですね。

jobs:
  jobA:
    runs-on: ubuntu-latest
    steps:
      - name: .NET Build
        uses: ./.github/actions/dotnet_build
      - run: nanika yaru

  jobB:
    runs-on: ubuntu-latest
    steps:
      - name: .NET Build
        uses: ./.github/actions/dotnet_build
      - run: betsu no nanika yaru

  jobC:
    runs-on: ubuntu-latest
    steps:
      - name: .NET Build
        uses: ./.github/actions/dotnet_build
        with:
          build-config: Release
      - run: tondemo naikoto yaru

Composite Action 利用時の注意

一見すると簡単で便利、最高って感じですが、Composite Actions は微妙に歯がゆいことがいくつかあります。 ということで、使うときはこれだけ気を付けておくといいです。(順次改善されて行ってほしい)

1. 使えるのは run: のみ (制約->改善済み)

感想: uses: 使えるようになってほしいけど無理そう 2021/8/26 に uses が使えるようになりました。 GitHub Actions: Reduce duplication with action composition | GitHub Changelog

Composite Action で使えるのは、 run: のみで uses: は使えません。 そのため、外部 Actions の呼び出しや別の composite action の呼び出しができません。

これが地味につらいところです。 たいがいは uses をいくつか使っているので、結果そのジョブを丸っと Composite Action に移して実行するというのはたいがいできません。

ほぼ毎回、runs: 部分をより分けてどれを composite action にするか検討することになるでしょう。 ただ分離したいだけなのに、というわけにはいかないのです。

2. run.if は使えない (制約)

感想: これはできるようになっていいのでは

run step は、実行するかどうかを決定する if コンディションがありますが、Composite Action の run で if: <expression> は使えません。 このため、元の run が if を使っていた場合、run: 処理の中で bash if を使って分岐することになったりします。なるほどねー。

# これはだめ
      - if: ${{ env.HOGE == 'hoge' }}
        run: do something
        shell: bash

# こうなる
      - run: |
          if [[ "${{ env.HOGE }}" == "hoge" ]]; then
            do something
          fi
        shell: bash

if 分岐を多用していると地味にめんどくさいので、ちまちま bash if にするか shell script に処理を書いてまとめたりします。

3. container で実行すると github.action_path パスが狂う (歯がゆい)

感想: 地味に罠なのでなおして~

Composite Actions の今のパスは github.action_path でとれます。このため、Composite Action で使うスクリプトは同じパスに置いておく、とかできます。

${{ github.action_path }}/prepare_env.sh

しかしコンテナで実行するときは狂うので、仕方ないので ${{ job.container.id }} でコンテナ環境か判定して、${{ github.workspace }}${{ github.action_path }} で修正してあげましょう。 これやらずパス参照で書けばいいやと思うと、actions のフォルダ名を変えるたびに毎回YAMLを修正しないと行けなくてつらいので。

やっておくのオススメです。

4. 1ステップで実行されるのでログの区切りがつかない (歯がゆい)

感想: すべての Composite Cction でやらないとつらいので大変めんどくさい

Composite Actions は、端的に言うと 呼び出し側の1 step で 呼び出した run がすべて実行されます。 つまり、1 step ログに、呼び出したすべての処理の標準出力がでるので、どの処理がどの出力か区別がつきません。

このため、Grouping log lines を使って処理ごとにログ出力をグループ化しましょう。絶対やりましょう。

::group::{title}
::endgroup::

docs.github.com

先ほどのサンプルはやってませんね、ダメな奴です。 アレに適用して次のようにすると、dotnet restore / dotnet build / dotnet publish がそれぞれグループ化されます。(こうなると、name もつけたくなるのでつけてます)

name: .NET Build
description: |
  .NET Build
inputs:
  build-config:
    description: "dotnet build config. Debug|Release"
    default: "Debug"
    required: false
runs:
  using: "composite"
  steps:
      - name: restore packages
        run: |
          ::group::Restore packages
            dotnet restore
          ::endgroup::
        shell: bash
      - name: build
        run: |
          ::group::Build
            dotnet build -c ${{ inputs.build-config }}
          ::endgroup::
        shell: bash
      - name: publish binaries
        run: |
          ::group::Publish binaries
            dotnet publish -c ${{ inputs.build-config }}
          ::endgroup::
        shell: bash

個人的には、name で自動的にグループ化してほしい気もありますが、ユーザーの好きなようにコントロールさせるために何もしていない気もします。

まとめ

Composite Actions は素朴でいいのですが実際使うときはアレってなるので、これらだけ注意すると便利です。

GitHub Actions に本当に欲しいのは、Template 機能な気もするけど ローカルアクションは便利なのでいいものです。 公開されている GitHub Actions を GHE で使うときに GitHub と Connect せずにローカルに展開することもできますし。

だいたいのことは GitHub Actions でできるようになりましたが、パイプライン的な観点がないので、今後はそっちがどうなるのか気になりますね。

Azure Bicep の設計 Resource編

前回は、Bicep の性質から、どういう基本設計でIaC を指向するか書きました。

tech.guitarrapc.com

今回は、実際に Bicep Resource を使って書くときに、どういう工夫が必要なのかメモしておきます。

tl;dr;

  • Preview リソースは ARM Template を見つけるところから気を付けよう。
  • Bicepモジュール粒度はTerraform のモジュール粒度と同じコンセプトでよく機能する。
  • param で object を使うときにはデフォルト値とaray of objectが使いにくい。
  • Role のような GUID が name のリソースでは、逆引きできるように設計が必要。

Bicep Resource

IaC で一番重要なのが、Resource Reference はどこを探せばいいのかの確認だ。

Bicep Resource は ARM Temaplte と相互に変換ができる。 ということで、Mirosoft は ARM Template の Reference に Bicep の定義も配置している。

Azure Resource Manager template reference - ARM template reference | Microsoft Docs

型定義は、次の通り。改行に意味がある構文なので、慣れてない内は、ふとした変数定義でエラーになる。

Bicep functions - objects - Azure Resource Manager | Microsoft Docs

Preview リソースと ARM Template

Previewは、Azureと付き合っていくうえでめんどくさい側面の一つだ。

Azure は Preview じゃないと使いたい機能がない、というケースが多い。(それ自体はいいが、プレビューが長いのがAzureを使っていてつらいところ) ということで Previewも扱えないか考えていこう。

Azure の ARM Template ページには「Previewを除くAzure リソース」は記載されているが、Previewリソースはここにない。 Preview リソースは、それぞれのPreview リソースの説明ページに存在する。

例えば、PostgreSQL Flexible Server は Preview なので、こっちを見ることになる。

クイック スタート:Azure DB for PostgresSQL フレキシブル サーバーを作成する - ARM テンプレート | Microsoft Docs

Previewページは手薄

Preview は、AWS だろうとどこだろうとAPIからドキュメントに至るまで何かと手薄だが、Azure も例外ではない。

このPreview のページには Database などの追加 ARM Templateの記載はもちろん、言及すらない、探す難易度が高い。そして Configuration に至っては存在しない。 幸いにして、ドキュメントになくてもVS Codeのbicep補完でリソースが出る。インテリセンスに頼ってエスパーしよう。

こういうところが Preview を使う上で本当に苦しいだろう。そしてPreview は長い、先が見えない不安が付きまとう。

細かいように思えるが、このドキュメントの一貫性の欠如はAzureを学ぶ上で、探すコストが著しく高く厳しいものがある。 Preview も同じARM template reference に置いて、定義をみるべき場所を減らせばいいと思うがしない理由もあるのだろう。

Bicep Module 粒度

BicepのModule は、Terraform 同様にある程度の粒度で組むのがよさそう。 いわゆる 1 Resource で 1 Module というのはなるべく避けるべきだろう。(拡張性が事実上ない) ただ、隠蔽するという意味では十分拡張性があってメンテコストが低いならあり。(resource を露出させたくないのもわかる)

ダメな例

Subnet が array of object を受け付けるが、1subnet 固定 + vnet が同時に作成される前提になっている。 これでは利用者は 1 vnet に n subnet はできず、かならず vnet に 1 subnet が強制されるだろう。

@description('Specifies the Azure location where the key vault should be created.')
param location string =resourceGroup().location
@description('Tag information for vnet')
param tags object = {}
@description('Virtual network name')
param virtualNetworkName string
@description('Address prefix for virtual network')
param addressPrefix string = '10.0.0.0/8'
@description('Subnet name')
param subnetName string
@description('Subnet prefix for virtual network')
param subnetPrefix string = '10.1.0.0/16'

resource vn 'Microsoft.Network/virtualNetworks@2020-06-01' = {
  name: virtualNetworkName
  location: location
  tags: tags
  properties: {
    addressSpace: {
      addressPrefixes: [
        addressPrefix
      ]
    }
    subnets: [
      {
        name: subnetName
        properties: {
          addressPrefix: subnetPrefix
          privateEndpointNetworkPolicies: 'Disabled'
        }
      }
    ]
  }
}

output id string = vn.id
output name string = vn.name
output subnetIds array = [
  {
    id: vn.properties.subnets[0].id
    name: vn.properties.subnets[0].name
  }
]

複数の AKS を構成する必要がないなら、ACR や ACR Role Assignment など、関連するリソースをまとめてしまうほうがいいだろう。

// パラメーター

// リソース
resource vn 'Microsoft.Network/virtualNetworks@2020-06-01' = {
}

resource symbolicname 'Microsoft.ContainerRegistry/registries@2020-11-01-preview' = {
}

// 他隠蔽できるリソース... 

// アウトぷっと
output id string = vn.id

Terraform などを使っている人にとっては、Terraformモジュールと同じコンセプトで分離すればいい、といえば伝わるだろうか。

Bicep Parameter

Parameterで活躍するのが型システムだ。 型が強く機能すれば、どのパラメーターに何をいれればいいのか、インテリセンスがドキュメントとして機能する。 Bicep の型定義システム自体は決して強くない。だが、VS Code のLanguage Server が強力に機能しているので、インテリセンスだけを見ると Terraform よりも書きやすい。

Data types in Bicep - Azure Resource Manager | Microsoft Docs

string, int, bool の扱いやすさ

型を指定すれば、パラメーターを渡すとき、使うときに型チェックされて入力している値の型と合致しているか見てくれる。 terraform と同程度には扱えるし、便利。

param strParam string
param enable bool

また、attirbute で @allowed などをparamの上の行の書けば入力を enum 値で制限もできて便利だ。

@allowed([
  'apple'
  'orange'
])
param fruit string

パスワードのようなセキュアな値は、@secured() を付ければSecureString として扱われて Deploy History などに乗らないのでこれも便利。

@secure()
param password string

object型の型宣言が弱い

Bicep のobject型は、型宣言時にプロパティを宣言できないため使いにくいという印象がぬぐえない。

// 宣言時にデフォルト値をもってプロパティが決まる
param foo object = {
    str_prop = ''
    num_prop = 111
    bool_prop = true
    array_prop = []
}

なぜ、型宣言時にプロパティを宣言できないのが使いにくいのだろうか。

IaC で避けられるなら避けたほうがいいのは、デフォルト値の設定だ。 デフォルト値が、オフィシャルのARM Template の bicep Resource のような本体ならいいのだが、Module として提供する場合はデフォルト値を入れた/入れてないで事故が起こりやすい。

そのため、基本的にパラメーターで与えたいものはデフォルト値なしで、型宣言だけして与えるのがよいと、私が見てきた多くの現場ではプラクティスとして得ている。

例えばterraform では、変数の型宣言は次のようにデフォルト値なしで行える。

variable "foo" {
  type = set(object({
    str_prop    = string
    num_prop = number
    list_prop   = list(string)
    set_prop   = set(string)
    map_prop = map(string)
  }))

bicep も、object型宣言 時にプロパティと型を指定できれば事故を防げてうれしいのだが、できないので諦めよう。

array 型の型宣言が弱い

同じことは array 型にも言えるが、string や int などの単一の型なら推論が効くので何も問題がない。 だが、object の array となると完全に無力だ。parameter に渡すとき、parameterを使うときの両方でインテリセンスは沈黙する。

そもそもの型宣言が array でしかないので無力としか言えない、ここからプロパティを推論できるようになるといいのだが。

param foo array = [
  {
    str_prop = ''
    num_prop = 111
    bool_prop = true
    array_prop = []
  } 
]

terraform の list(map(string)) 型のインテリセンスの利かなさと同じといえばイメージしやすいだろうか。

実行時Parameter の渡し方

bicep は、実行時に2つの方法でパラメーターを渡せる。

  1. cli 引数
  2. jsonファイル参照

cli 引数は -p key=value で指定できるので使いやすくはじめのうちはこれが多い。

az deployment group create --resource-group dev-foo -f foo.bicep -p key=value -p key2=value2 --mode Complete

ただ、実際にCIで回し始めると dev や stg など、決まった環境に決まった実行を毎度行うことが多くなる。 ということで、いちいち引数設定せず json にしておいて実行引数はいつも同じになっていくだろう。

az deployment group create --resource-group dev-foo -f foo.bicep -p @param.json --mode Complete

json parameter がちょっと使いにくいのが、bicep で指定していない parameter が json に定義されていると引数が渡せずエラーが出ることだ。 設定ファイルを共通にして、いくつかの bicep ファイルに分ける (当然bicepごとにparamはそれぞれ違う)、という使い方には向いていないのでなんとももどかしいものがある。

az deployment group create --resource-group dev-foo -f foo.bicep -p @param.json --mode Complete
# foo とbar で同じ param じゃないとパラメーター渡しでエラー
az deployment group create --resource-group dev-bar -f foo.bicep -p @param.json --mode Complete

諦めて、それぞれの bicepごとにparam を用意することになったが微妙。

existing と リソースの存在保障

existing は、いわゆる terraform の data リソースのように、既存のリソースからリソース参照を拾ってくる使い方のために用意されている。

Referencing existing resources

たとえば、次のようなstorage account リソースを拾ってくる書き方ができる。

resource stg 'Microsoft.Storage/storageAccounts@2019-06-01' existing = {
  name: 'myacc'
}

では、subnet のように、他のリソース(subnetなら vnet ) の中にあるリソースはどうやってとってくるかというと、vnet を拾ってから subnet を拾うのがいいだろう。 例えば次のようにする。

resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' existing = {
  name: 'vnet-name'
}

resource subnet 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' existing = {
  name: '${vnet.name}/my-subnet'
}

existing の実行成功は存在保障ではない

この existing 処理の問題点は、本当にそのリソースが取れたかの確証が取れないことだ。 通常 terraform や pulumi では、data resource で対象のリソースの取得に失敗した場合エラーで中断する。 だが、bicep では中断処理が行われない。

たとえば、先ほどの vnet を name ではなく id 参照にするとどうなるだろう。

resource vnet 'Microsoft.Network/virtualNetworks@2021-02-01' existing = {
  name: 'vnet-name'
}

resource subnet 'Microsoft.Network/virtualNetworks/subnets@2021-02-01' existing = {
  name: '${vnet.id}/my-subnet' // vnet.name から vnet.id
}

結果は、subnet が取れない、だ。それにも関わらずARM Template のデプロイ時にここはパスされて、後続の処理では「取れてないsubnet」を渡そうとする。結果、デプロイ自体はは、subnetを使うリソースで作成が失敗してエラーになる。

エラーメッセージもリソースが作れなかったことを示すのみで、それが subnet が取れなかったことには連想しにくい。 本来は、原因であるsubnet の取得で失敗してエラーになってほしいのは言うまでもない。

existing は、既存のリソースをとってくるが、とってきたことを保証しない。 これはIaC としては厄介な挙動で、what-if のような 実行前の確認で検知できないことを示している。 Terraform では data source を使うことで確証を取れるのだが、Bicep では実行前に az コマンドなどで取得してパラメーターに渡すぐらいしか確証とれなさそうだ。

なお、こういった subnet -> vnet という依存関係があるリソースは、id 上で {parent_id}/subnets/{subnet_name} のような resource id ルールが一般に存在するため、subnet を existing で拾う必要がない。 existing の現状の挙動では、無理して使う理由が乏しいので回避できるならするといいだろう。

Role Name の取得

Role には、Build-in Role と Custom Roleが存在する。 Azure のIAMはリソースごとに存在するので、RBAもリソースごとに他のリソースやRole と関連づけることになる。 つまり、role assignment は、リソースごとに行う。

参考: AWS の場合、IAM Role でリソースとアクションをポリシーとして集権して、IAM Role Arn をリソースに割り振る。

Role の特徴は、resourceIdの名前部分が GUID であることだ。 コマンドなら az role definition list --name 'ROLE_NAME' | jq -r .[].id のようにすることでRole名さえわかっていれば Role Idを取得できる。 だが bicepでリソースをとってくるときは、Microsoft.Authorization/roleDefinitions リソースで existing 経由で取得しようと思っても、subscriptionResourceId 関数で取得しようと思っても、GUIDがわからないと使えないことに気づくだろう。

// resourceSubscription関数で
subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ここに入れるGUIDをどう導き出すか')

// あるいは existing 使うなら
resource aksAcrPermissions 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
  name: 'ここに入れるGUIDをどう導き出すか'
}

Role がGUIDであるため名前から推測できない。 ということで、Built-In Role、Custom Role それぞれで既存Roleを参照するときに工夫が必要となる。

Built-in Role

Azure が提供している組み込みRole は、全アカウントで Role Name となる GUID が固定である。

一覧: Azure built-in roles - Azure RBAC | Microsoft Docs

固定値なので何も考えずに GUID を必要に応じて渡すか、Role Name から GUID を返すだけのModuleを用意すればいいだろう。 現実的に考えると、bicepのモジュールは関数的に使うには無駄にしんどいので、GUID をそのまま渡すのがいいだろう。(terraform や Pulumiを考えると、こういうAzureで決定しているものの取得はbicep が組み込み関数で用意するべきだと思う)

例えば、AKS Clusterから ACR のイメージを取得する Role Assignment を与えるRole Assignmentを行うことを考えてみよう。 ACR からの Pull権限は、Build-in Role AcrPull で提供されており、GUID は 7f951dda-4ed3-4680-a7ca-43fe172d538d とわかっているので次のように書くことになるだろう。

resource aks 'Microsoft.ContainerService/managedClusters@2021-03-01' = {
  // プロパティ
}
resource acr 'Microsoft.ContainerRegistry/registries@2020-11-01-preview' = {
  // プロパティ
}
resource aksAcrPermissions 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(aks.name)
  scope: acr
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
    principalId: aks.identity.principalId
  }
}

Custom Role

Custom Role を定義した場合、その Role が同じModuleやリソースから参照できるならそれを使えばいい。 そうでなく、先ほどの Build-in Role のように既存の取得をしたい場合、Role作成時 の name 時点で工夫するしかない。

RoleDefinitions の name は、GUID だ。このGUID に bicep の Guid関数を利用し、引数に roleName を指定すればいい。 こうすれば、参照する側は roleName がわかっていれば、Guid関数で逆引きができる。

コードで見てみよう。 ロールを作成するときに工夫するのがすべてだ。

var role_name = 'my_awesome_role'
resource hoge 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' = {
  name: guid(role_name) // ここで role_name を知っていればguid が算出できるようにする。
  properties: {
    roleName: role_name
    // ほかのプロパティ
  }
}

あとは、resource が直接参照できなくても、次の方法で導き出すことができる。

// subscriptionResourceId 関数で取得
subscriptionResourceId('Microsoft.Authorization/roleDefinitions', guid('my_awesome_role'))

// existing で取得
resource aksAcrPermissions 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
  name: guid('my_awesome_role')
}

来てほしい機能

いくつか書いていてつらいのでサポートが欲しい機能。

Azure Bicep の設計

Azure の構成を IaC したいとなると、おおむね選択肢は次の3つになりそうです。

  • ARM Template
  • Terraform
  • Pulumi

Terraform と Pulumi はおおむね同じ性質ですが、Pulumi は Azure に対しては他のクラウドよりも優先的に機能が入るのでちょっと面白いです。 とはいえ、世の中的には Azure の構成は ARM Template が一番合ってるといわれるとかいわれないとか。

ARM Template は人間が使うフォーマットじゃないので一ミリも興味がでなかったのですが、Azure Bicep が DSL として出てきたこともあり、ここしばらくはAzure で Bicep を使って構成してみたので設計メモを書いておきます。

tl;dr;

  • Bicep を用いて、コードとリソースの一致を保証することは ARM Template の現在の性質ではできない。Microsoft は Deployment Stacks と呼ばれる仕組みで改善を検討している。
  • Bicep は、Complete modeを使うとコードとリソースが一致することがある程度保障できる。IaCとして使うなら Complete 一択。(それでもずれる)
  • Completeでは、最後に適用したBiepデプロイでリソースが決定されるので、Bicep スコープは狭く維持する。このため、ResourceGroup を target scopeにするのがいいだろう。Bicepを分ける = ResourceGroupをライフサイクルで分けることになる。

Bicep と ARM Template

bicep は、ARM Template の DSLでARM Templateに変換される。あくまでDSLなので、デプロイを含めた仕様、制約はARM Template に準じる

ARM Template の特徴は次の通り

Stateless

ARM Template は、State を持たず Template = リソース定義のJSON とリソースを一致させる動作を目指している。(Terraform/Pulumi/CloudFormationはState を持つ)

Bicep は、コードとリソースの一致保障が仕組みとして存在していない。この対策として、bicep及び ARM Template ライフサイクル全体への改善として Deployment Stacks が検討されている。

Any plans for destroy functionality? · Discussion #1680 · Azure/bicep

デプロイのスコープ

デプロイは、target scope で設定できる。ResourceGroup / Subscription / Tenant と広くなっていく。

デプロイモードによっては、target scope で 1deployしか適用できないケースがあるので、スコープの決定は設計に大きく影響を与える。

基本的に、Bicep を分ける = ResourceGroup を分ける = ライフサイクルごとの管理、になるだろう。

デプロイモード

ARM Template Deployにはモードが2つある。

  • Increment (デフォルト)
  • Complete

Incrementは増分デプロイで古いリソースはテンプレートからは消えてもリソースは残る。

Completeはテンプレートにないリソースを消すが、消されるリソースと消えないリソースがあり、その動作はリソース次第。

Complete mode deletion - Azure Resource Manager

デプロイによる一致保障

ARM Template のデプロイの仕組みから、Bicep コードと テンプレートは一致と冪等性が保障されているが、コードとリソースの一致と冪等性は保障されていない。

あくまでも「適用されているテンプレート × デプロイモード」によって、リソースの状態が決定する。

デプロイ方法と適用

それぞれのscope に対して複数のdeploy を適用できる。

ただし、どの deploy が適用されるかは、モードによって変わる。

  • Incrementを使うと、増分なのでそれぞれの増分が適用される。(複数のDeployが当たる)
  • Complete を使うと、最後に適用したdeploy でリソースが構成される。ほかのdeployで適用されたリソースは最後のdeployのテンプレート次第で消える。(複数は当たらない)

Complete を使うとコードとリソースの一致は、デプロイ時は保障できる。だが、Completeによる削除処理はリソースによって「消えない」ため、削除操作によってコードとリソースは乖離していく可能性がある。たとえば、StorageAccountは削除されるが、Blob は消えない、など。

Increment は、削除を絶対にしないので、コードとリソースはどんどんずれていく。IaC で目指すコードとリソースの一致とは、そもそも目指すところが違うのでIaCをするのであれば Increment は使う理由がない。

IaC としての Bicep

IaC は、コードでリソースの状態を明確に示すことが重要になる。コードでリソースの状態を明確に示すというのは、今のリソースがコード通りであるといえる。

これは「コード以外のリソースは認めている」が、一方で「コードで管理されていたリソースが外れるときにそのリソースを削除する」というのも期待している。

つまり、コードとリソースを一致させるには、コードとリソースの同期をどうとるのか、同期が取れないリソースをどう扱うかが重要になる。

削除

Azure では リソース名 = リソースの一意性を意味する。

Bicep で定義したリソース名 (resource id) が変更されたときに、リソースはどのようになるのかかが IaC としてのBicep を決定する。

デプロイモードで記載した通り、次の挙動になる。

  • Increment = 増分デプロイなのでリソースの名前が変わったことを検知せず、ただ 「Template に存在しないリソースが増えたと解釈」される。結果、古いリソースは放置、新しいリソースが構成される。もしテンプレートが既存リソースに対する定義だった場合、リソースの設定をTemplateで上書き構成する。
  • Complete = Template とリソースの一致なので、Template 通りにリソースを構成する。具体的には、テンプレートにないリソースは、仕様で削除対象になってれば削除、削除対象じゃなければ放置。もしテンプレートが既存リソースに対する定義だった場合、リソースの設定をTemplateで上書き構成する。

Increment では削除が起こらない。

Complete では、Template と一致するように 削除が起こるが、削除されないリソースが多く、削除されなかったら自分で削除が必要になる。(コードとリソースの一致を目指していないのでそうなる)

Destroy

削除の挙動から予想できる通り、Destroy は存在しない。

Bicep の定義をまとめて吹き飛ばしたいときは、リソースが空な Bicep を Complete モードでデプロイする。

すると、Complete で削除する仕様のリソースは消える。消えない仕様のリソースは残る。

Bicep や ARM Template は IaC なのか

コードとリソースの一致を目指すIaC というよりは、Template と リソースの一致を目指しそのTemplateを生成する TaC というのが適切だろう。

Bicep と State

コードとリソースが一致できないのは、 Microsoft も把握している通り、Stateless であるためのライフサイクルマネージメントの困難さに起因している。

このため、Deployments Stack がくれば Bicep はおそらく コードとリソースの一致ができるようになるようにライフサイクルが改良されるだろう。

同時に、Stateを持たないことで、Template通りに構成を実行する Control Plane が Managed な Azure Resource Management であったが、State をどこに置くかというのを考えることになるだろう。

Bicep で IaC っぽく使う

Bicep というより ARM Template の現状の挙動は、いわゆる IaC に期待する挙動とは乖離しており、コードとリソースの一致も保証できないという意味では難しい。

  • Subscription: --mode がないのでComplete の指定はできない。安全優先って感じ。
  • ResourceGroup: 基本的にComplete で利用し、消えないリソースは都度消す (これが怖い) ようにすれば、おおむね IaC っぽくは利用できるだろう。

まとめ

Terraform x Azure は、書きにくいけどまともなIaC 体験は得られます。 Bicep は ARM Template が Stateless という性質を指向しているがゆえに、コードとリソースの一致が約束されないのが IaC 体験としては悪いものがあります。 というか、リソースが残ってしまって、それを消すときに事故臭しかしない。

Bicep自体というより、ARM Template の性質の問題なので、Deployment Stacks によって Stack のコンセプトが来たら体験が変わるのを期待したいところです。