tech.guitarrapc.cóm

Technical updates

GitHub Actions が OpenId Connect に対応したので AWS OIDC Provider と連携する

前々から言われていた GitHub Actions で OpenID Connect 経由で、各種Cloud Provider の認証を得るのが GA しました。 めでたい。

github.blog

これにより、aws-actions/configure-aws-credentials のみで認証が組めるようになったので見てみましょう。

https://github.com/marketplace/actions/configure-aws-credentials-action-for-github-actionsgithub.com

tl;dr;

  • GitHub Actions でAWS操作をするために、IAM User が不要になるので神
  • セルフホストな GitHub Actions を EC2 で動かしているなら、素直に Instance Role を使うので十分というのもある。(何か事情があれば OpenId Connector でもいい)
  • AWS アカウントで OpenId Connect Provider は 一意です。1 AWSアカウントで複数環境を持っている & 環境ごとに IAM Role を持っている場合は、環境ごとに OpenId Conect Provider を作ろうとして失敗しないように
  • 並列度高く認証を取得しようとすると失敗することが多いので注意

動作例

設定がただしければ、必要な IAM Role Arn を role-to-assume に指定するだけで、その Role 権限で 操作ができるようになります。

これで

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@master
        with:
          aws-region: ap-northeast-1
          role-to-assume: "作ったIAM Role の Arn を入れる"
          role-session-name: "適当な名前"

こうなる。

GitHub OpenId Conector と AWS OIDC Provider でAWS の操作ができる

基本

GitHubから公式にドキュメントが公開されたのでこれに従いましょう。先日までuses: aws-actions/configure-aws-credentials@v1 になってたせいでドキュメントが嘘つきでしたが @master に修正されています。

docs.github.com

なお、https://github.com/aws-actions/configure-aws-credentials は、uses: aws-actions/configure-aws-credentials@v1 になっているので動きません。uses: aws-actions/configure-aws-credentials@master に読み替えましょう。

Terraform で AWSを用意する。

用意する必要があるのは、OIDC Provider と IAM Role です。

  • OIDC Provider は、GitHub OIDC と信頼関係を結ぶのに必要です
  • IAM Role は OIDC Provider 経由で GitHub OIDC でリクエストされたときに、リクエスト元のリポジトリオーナー/リポジトリ名:ブランチ名 を検証し条件にマッチしたらそのRoleを Assume して利用できるようにします。つまり、このRoleに、リポジトリの制約と必要な IAM Policyを振ればok

全体を示します。

gist.github.com

OIDC Provider を用意する

値は固定なので単純です。terraform ならこうなります。

// oidc provider
resource "aws_iam_openid_connect_provider" "main" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["a031c46782e6e6c662c2c87c76da9aa62ccabd8e", "6938fd4d98bab03faadb97b34396831e3780aea1"] // 2022/1/11 に 中間証明書更新された
}

thumbprint に関しては 中間証明書の寿命が1年なので、毎年1/11 に代わっていくはずです。 thumbprint はコマンドでとれるので、更新直後などドキュメントが間に合ってないときは適当に対応しましょう。

Get Thumbprint of GitHub OIDC, updated on 2022/01/13. · GitHub

client_id_list がGA前と後で変わっています。古いバージョンではここで、リクエスト元のリポジトリURL を指定していましたが、今は sts.amazonaws.com といつものになりました。標準に沿ってくれてよかった。

GitHub の Endpoint が確認できますね。

https://token.actions.githubusercontent.com/.well-known/openid-configuration

{"issuer":"https://token.actions.githubusercontent.com","jwks_uri":"https://token.actions.githubusercontent.com/.well-known/jwks","subject_types_supported":["public","pairwise"],"response_types_supported":["id_token"],"claims_supported":["sub","aud","exp","iat","iss","jti","nbf","ref","repository","repository_owner","run_id","run_number","run_attempt","actor","workflow","head_ref","base_ref","event_name","ref_type","environment","job_workflow_ref"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["openid"]}

IAM Role を用意する

順番に解説します。

重要なのは、Assume Role です。 Assume Role で OIDC Provider からのリクエストを検証しています。

  • principal は、認証を委譲されて受けるので type: "Federated" + 先ほど作った OIDC Provider の arn を指定 します
  • condition で、リクエスト元が repo:<GitHubOwner>/<Repositry>:Branch の条件とマッチするか検証します

OIDC 経由で、許可した「リポジトリ、かつブランチ」だった場合に IAM Role をsts:AssumeRoleWithWebIdentity としてAssume できるようにします。

さて、複数のリポジトリで同じロールを使いたいケースはどうすればいいでしょうか?

IAM Policy の condition は、ワイルドカード一致するかを StringLike で検証できますが、これは value が1要素 (=単一リポジトリ) のときにしか機能しません。 value が複数要素(=複数リポジトリ)でもワイルドカードで一致するかを見る場合は、ForAnyValue: + StringLike を用います。

今回は 分岐させましたが、別に ForAnyValue で初めから書いてもいいでしょう。

data "aws_iam_policy_document" "github_oid_assume_role_policy" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]
    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.main.arn]
    }
    # aud があるとはじかれてるので、aud の値がおかしいっぽい。 aws-actions/configure-aws-credentials の仕組みからすると、sts.amazonaws.com が来るはず。 <- github.com/aws-actions/configure-aws-credentials ではなしになってる :(
    # condition {
    #   test     = "StringEquals"
    #   variable = "token.actions.githubusercontent.com:aud"
    #   values   = ["https://github.com/${var.github_owner}"]
    # }
    condition {
      test     = length(var.github_oidc_repo_names) == 1 ? "StringLike" : "ForAnyValue:StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = [for item in var.github_oidc_repo_names : "repo:${var.github_owner}/${item}:*"]
    }
  }
}

AssumeRole ができてしまえば IAM Roleを作るだけです。今回は、aws sts get-caller-identity を実行できるようにしてみましょう。

特にいうことはないですね。終わり。

data "aws_iam_policy_document" "github_actions" {
  // allow running `aws sts get-caller-identity`
  statement {
    effect    = "Allow"
    actions   = ["sts:GetCallerIdentity"]
    resources = ["*"]
  }
}

resource "aws_iam_policy" "github_actions" {
  name        = "githubactions_policy"
  path        = "/"
  description = "Policy for GitHubActions"
  policy      = data.aws_iam_policy_document.github_actions.json
}
resource "aws_iam_role" "test_role" {
  name               = "githubactions-oidc-role"
  path               = "/"
  assume_role_policy = data.aws_iam_policy_document.github_oid_assume_role_policy.json
  policy_arns = [
    aws_iam_policy.github_actions.arn
  ]
}

リソースを作成

さて、これで AWS にリソースを作るとこんな感じになります。

今回は私は、guitarrapc/githubactions-lab と guitarrapc/infrastructure の 2リポジトリから GitHub Actions 経由で認証を受けられるようにしました。 IAM Role の Trust relationships > Conditions で2リポジトリが ForAnyValue:StringLike で指定されているのがわかりますね。

IAM > Identity providers > token.actions.githubusercontent.com

IAM > Roles > githubactions-oidc-role

ここまでのどれかにミスがあると Assume Role されません。 うまくいかない場合は何度も見直すことになるでしょう。

GitHub Actions の構成

Workflow を用意します。 キーポイントは3つです。

  • permissions で、id-token: write で書き込み権限が必要です。 permissions: write-all でもいいのですが、現状では permissions を指定しないとうごかないので注意です
  • role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}secrets.AWS_ROLE_TO_ASSUME は、GitHub Secret に AWS_ROLE_TO_ASSUME という名前で先ほど作った IAM Role の Arn を仕込んであります。Arnのフォーマットは、今回の例なら arn:aws:iam::xxxxxxxxxxxxx:role/githubactions-oidc-role というフォーマットになります
  • role-session-name で、CloudTrail イベントにユーザー名が出るのでいい感じの名前にしましょう
  • uses: aws-actions/configure-aws-credentials@master を指定します。 uses: aws-actions/configure-aws-credentials@v1 でないので注意してください

github.com

name: aws oidc credential

on:
  workflow_dispatch:
  push:
    branches: ["main"]

# allow use id-token
permissions:
  id-token: write # required!
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Configure AWS Credentials
        # must use "master", not "v1". v1 is not yet released to use latest role-to-assume.
        # Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers
        uses: aws-actions/configure-aws-credentials@master
        with:
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-session-name: GitHubActions-${{ github.run_id }}
          role-duration-seconds: 900 # minimum: 900sec, maximum: iam role session duration
      - name: get-caller-identity is allowed to run on role.
        run: aws sts get-caller-identity

実行してみると、冒頭のスクショのように成功するはずです。

$ aws sts get-caller-identity
{
    "UserId": "AROASJXUOK5UM7XZKRYTB:GitHubActions-1420393244",
    "Account": "***",
    "Arn": "arn:aws:sts::***:assumed-role/githubactions-oidc-role/GitHubActions-1420393244"
}

うまくできましたか? おめでとうございます!

ダメでした? 私も失敗を繰り返したので、経験したエラーと対処をFAQに載せておきます。

FAQ

GitHub Actionsで実行してみたら動作しない。

エラーメッセージ Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers

この場合、AWS OIDC Provider の設定がおかしい or GitHub Actions の permissions が抜けています。

  • (AWSの設定ミス) OIDC Provider の client_id_list に "sts.amazonaws.com" ではなく、リポジトリのURL を指定している。(古いブログが記事がそうなっている)
  • (GitHub Actions Workflowの設定ミス) Workflow で、permissions がない。デフォルトの GitHub Actionsは write-all のはずですが、まだ id-tokens は含まれていないようです。明示的に permissions: write-all を指定するか、id-token: write を指定しましょう

GitHub Actionsで実行してみたら動作しない。

エラーメッセージ Not authorized to perform sts:AssumeRoleWithWebIdentity

このケースは、OIDC Provider は問題なく、AssumeRole の設定にミスがあります。

  • (AWSの設定ミス) AssumeRole の Action が "Action": "sts:AssumeRoleWithWebIdentity" になっていない
  • (AWSの設定ミス) 複数リポジトリなのに ForAnyValue:StringLike ではなく StringLike で判定している
  • (AWSの設定ミス) AWS の IAM Role の Assume Policy が設定しようとしている GitHub Owner/Repository:ブランチ と内容と一致していない

過去にある類似記事との差分

GA前の内容で、結構ずれています。GA前の記事の内容でやると失敗するので注意です。(aws-actions/configure-aws-credentials が v1 で OIDC 変更を反映すればいける)

dev.classmethod.jp

zenn.dev

github.com

Git GUIクライアントと Fork

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

普段私は、GitKraken をメインにしていますが、サイズの大きなリポジトリでは Fork を利用しています。 しばらくForkをメイン気味に使っていた中で、私がForkに感じた良さと苦手なことをメモしておこうと思います。

tl;dr;

Git GUI クライアント、まだまだ全然決定版がないですね。

  • GitHub.com や GHE、GitLab など複数のVCSを同時に扱うなら、GitKraken 一択です
  • 現在のブランチにいつつ、別のブランチの操作、ツリーのハンドリングなどgitの使い勝手は GitKraken が最高です
  • SourceTree使ってたなら Fork がいいでしょう。すべての動作が高速な上位互換です
  • あるいは10GB 超えるようなリポジトリも Fork は高速で使いやすいと思います
  • GitKraken で git操作が遅いと感じるなら Fork はgit操作が高速でUIもロックされず快適です。一方で、Fork のgit操作、UIコンセプトはGitKrakenより手間がかかるのでストレスです。1
クライアント 価格 認証 複数VCS対応 備考
Fork $49.99 (買い切り) OAuth × 試用は無料
GitKraken $4.95 user/month (サブスク) OAuth OSS利用は無料
SourceTree 無料 PAT/SSH ×

自分のgit利用ケース

私がgitで利用するのは、主にコードのコミット (Unity や C#、Go、React 他)、並びに Git LFS です。

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

git GUI クライアントについて

Forkだけ評価しても一方的なので、自分が使っている GitKraken も評価します。

Fork

git-fork.com

Fork は、買い切り $49.99 (One-time purchase) の Gitクライアントです。2

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

Git操作が高速なのが特徴で、例えば10GB越えのリポジトリでUnity で3DモデルやLFSを使っていても、checkout、diff、stage、commit、push、pullのいずれも重いと感じることなく操作ができます。

GitKraken

www.gitkraken.com

GitKraken は、サブスクリプション購入 $4.95 per user/month (paid annually) のGitクライアントです。3

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

個人的に最も好きな GUI Git クライアントです。

ツリーの見やすさ、マージコンフリクトの解消のしやすさ、いちいちブランチを切り替えずGit操作ができる快適さは他の追随を許しません。また、複数のVCSと接続できてプロファイル概念があるので、1つですべてができる万能さが特徴です。

Git操作が重くなりがちで、1つの操作中はほかの操作がロックされる欠点があります。4 いろいろ GUI Git を使ってきていますが、最も操作しやすく、UIとツリーが洗練されていると感じます。

Fork と SourceTreeの比較

Fork の操作やUIはSourceTree と非常に似通っているので、SourceTreeを使っていたら違和感なく移行できるでしょう。 また、SourceTreeのストレスが解消されているので気持ちいいぐらい使いやすいと思います。

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

Fork の良い点

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

Fork の懸念点

  • 有料
  • OAuth 認証が必要

特筆点

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

ForkとGitKrakenの比較

Fork のgit処理は、GitKraken に比べて非常に高速なので、GitKraken で重いと感じているならFork は快適に感じるでしょう。

一方で、UIが違いすぎるのがネックで、GitKraken の UIに慣れていると Forkは1つ一つの操作で目線を移動する必要があり、操作の一貫性がなくブランチを跨いだ操作ができず使いにくいと感じます。

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

Fork の良い点

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

Fork の懸念点

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

特筆点

GitKraken とはコンセプトの違いはあると思いますが、油と水ぐらい使い勝手が違います。

Fork は操作の視点移動が多く、コミット操作中にツリーが隠れるなど、Fork は触って使いにくく感じます。

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

上 Fork の ツリー、下 GitKraken の ツリー

Fork の欠点と対処

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

Forkが GitKraken のように操作ごとの視点の動きを考慮したUIになれば好ましいですが、そんな未来は来ないと思います。 ということで、ペイン配置で何とかできないでしょうか。

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


  1. GitKraken が Fork の早さをもって、ファイルロックが起こらなければ最強といえるんですがそんなものはない
  2. 試用のみ無料ですが、無料版は現在存在しないと明言されています。その上の発言と一貫性取れてない気がするけど。有料の価値はあるいい製品です。
  3. 無料版もあり、OSSのみ + 接続できるVCS が github.com というだったり制約があります。
  4. リポジトリが小さい限りははほぼ発生することはありません。

LinqPad の設定

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

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

tl;dr;

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

before & after

before

after

設定一覧

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

Edit > Preference > Editor

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

Edit > Preference -> Editor

Edit > Preference > Query

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

Edit > Preference -> Query

Edit > Preference > Advanced

  • Convert tabs to spaces: True

Edit > Preference -> Advanced

LinqPad はいいぞ

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

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

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で実際にやる処理1つ一つを 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と付き合っていくうえでめんどくさい側面の1つだ。

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

来てほしい機能

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