tech.guitarrapc.cóm

Technical updates

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 のコンセプトが来たら体験が変わるのを期待したいところです。

ロジクールMX Anywhere3 に変えた

普段からどうしてもほしいものはない、のですがマウスだけはずっと満足いかずたびたび探しています。

ずっと使っているのが、ロジクールMX Master / MX Anywhere シリーズで、職場と自宅などでシリーズ初めから使っています。 MX Master は大きいので、MX Anywhere が好みでここ数年は常に MX Anywhere 2を使ってました。

ふと、マウスに不満を覚えて探して比較するものの、結局 MX Anywhere に戻ってきてしまう。 それは今回もそうなのでした。

MX Anywhere 3

MX シリーズは、大きめサイズでハイエンドな性能詰め込みました的な MX Master と 小さめサイズでなんでもできるけど人間工学的なデザインがない MX Anywhere があります。

f:id:guitarrapc_tech:20210703015101p:plain
https://www.logicool.co.jp/ja-jp/products/mice/mx-anywhere-3.910-006005.html より

youtu.be

youtu.be

私が愛用しているのは MX Anywhere です。

www.logicool.co.jp

以前はUnity 使うときは MX Master で、それ以外は MX Anywhere だったのですが、MX Anywhere を布教しまくってどこでも使ってたら慣れてしまいました。

MX Anywhere 3 は、MX Anywhere の最新版です。 for Mac もあって、こっちは Unifying レシーバーがなく Bluetooth接続対応のみになります。Mac / Windows 両方使うのですが、Macは専用でなくても十分安定しているので通常のを選びました。

Bluetooth 接続は、Windows/macOS のいずれにおいても負荷が極めて高い時に安定性に欠ける一方で、Unifying は安定しており UEFI でも問題ない& USB切り替えで複数接続が気にならないというのがポイントです。(使い込むほどUnifying 一択)

MX Anywhere 2 からの変更点

MX Anywhere 2 との変更点は書いてる人はたくさん書いてるのでそれを見てください。

news.mynavi.jp

k-tai.watch.impress.co.jp

MX Anywhere 3 にしたのは、MX Anywhere 2 の不満が解消されていたからです。 MX Anywhere 2 には次の不満がありました。

  • ミドルクリックがマウスホイールクリックではなく、ホイール下の小さなボタン
  • 横スクロールのホイール横倒しがしんどい
  • 充電ケーブルが micro-USB
  • 充電しても電池切れ期間が短くなってきた

改善点1. ミドルクリック

一番厄介で、慣れてしまうまで MX Master にしていた最大の理由が、「ミドルクリックがマウスホイールクリックではない」ということです。通常ミドルボタンは、マウスホイールであることが多いかと思いますが、MX Anywhere 2 までは違いました。

MX Anywhere 2の マウスホイールクリックは「スムーズスクロールの切り替え」です、意味が分かりません。MX Masterのように、高速スクロールで自動的にフリースピンにシフトすればいいじゃない。

私はWeb画面やコードはミドルボタンで自動スクロールさせて読むので、ミドルクリックを多用する割に使いにくい配置で最悪だと思ってます。

f:id:guitarrapc_tech:20210703014647p:plain
https://www.logicool.co.jp/ja-jp/manuals/11892 より

MX Anywhere 3では、ついにマウスホイールでミドルクリックできるようになりました。高速スクロールで自動的にフリースピンにシフトするし、普通になった。

f:id:guitarrapc_tech:20210703015407p:plain
https://www.logicool.co.jp/ja-jp/products/mice/mx-anywhere-3.910-006005.html#specs より

改善点2. 横スクロールがスクロールホイールになった

MX Anywhere 2 も MX Master もマウスホイールを横に倒すと横スクロールができます。 これ、一見すると直感的でいいんですが、スクロール量の調整ができなくて私には使いにくいです。

MX Anywhere では、ホイールは横に倒せなくなり、代わりに親指付け根あたりの左/右のどちらかのボタンを押しながらスクロールで横スクロールになります。 スクロール量の調整は普通のスクロールと同じなので普通に使いやすいです。普通に使える、大事。

f:id:guitarrapc_tech:20210703015407p:plain
https://www.logicool.co.jp/ja-jp/products/mice/mx-anywhere-3.910-006005.html#specs より

改善点3. 充電ケーブルが USB-C になった

micro-USB がそろろそ根絶したい今日この頃ですがそうもいかないのが続きます。 MX Anywhere 2 もその一つだったのですが、ついに USB-C になったのでまた一つ減りました。普通、大事。

充電を取り戻す

MX Anywhere 2 は毎日ずっと使っているので、一月程度で充電が必要になってきました。

MX Anywhere 3 に変えることでリセットです。

電池寿命: 1回のフル充電で最大70日使用可能。1分間の急速充電で3時間使用可能

まとめ

MX Anywhere のこれまでの良さである、軽くて小さい、マルチペアリングと切り替え、Unifyingによる高い安定性などのメリットはそのまま。 MX Anywhere 2 の欠点をカバーしてきて、満足度とおすすめできる度が高くなりました。

残りは静音性が次にほしくなる時の鍵になりそうですね。(クリックを静かにしたい欲)