tech.guitarrapc.cóm

Technical updates

AWSのmyApplicationをTerraformで構築する

AWSにはmyApplicationというサービスがあります。以前からTerraformでmyApplicationをどのように構築するのがいいかと悩んでいましたが、納得感のあるパターンが決まったのでメモです。

myApplicationとは

myApplicationは、AWSリソースをアプリケーション単位でグループ化して、リソースの状態やコストを網羅的に把握できるようにするサービスです。特にリージョンを跨いでリソース表示できるのがAWSにおいて貴重な機能です。 無料ですし、タグでリソースを紐づけるだけで使えるので使い勝手もいいです。

AWSコンソールにmyApplicationが表示される

myApplicationの画面は紐づけられているリソースやコストが表示されます。特にリソースは、東京リージョンで見ているのにGlobalとかも見えていますね。

myApplicationの表示

myApplicationのリソース

myApplicationとAWSリソースの関係

AWSリソースを特定のmyApplicationに関連付けるためには、myApplicationのリソースIDをAWSリソースのタグに設定する必要があります。このmyApplicationのリソースIDは次のような形式で、ランダムな文字列が含まれています。

# myApplicationのリソースIDパターン
arn:aws:resource-groups:{リージョン}:{アカウントID}:group/{myApplication名}/{ランダムな文字列}

# 例
arn:aws:resource-groups:ap-northeast-1:123456789012:group/foo/km3hcsjf2h5izwou4jxqdvt7cz

AWSリソースのタグに「キーにawsApplication、値にmyApplicationのリソースID」を設定するとそのリソースをmyApplicationに関連付けることができます。

awsApplication: <myApplicationのリソースID>

TerraformでmyApplicationを構築する

厄介なポイントはmyApplicationのリソースIDがランダムな文字列を含むため、myApplicationを作成した後にそのリソースIDを取得して、AWSリソースのタグに設定する必要がある点です。 Terraformで管理しやすい方法は2つ考えられます。

  • myApplicationを作成するtfstateとAWSリソースを作成するtfstateを分ける
  • myApplicationを作成してからdefault_tagsにmyApplicationのリソースIDをハードコードで設定する
  • awscc_resourcegroups_tag_sync_taskを使って自動同期させる

myApplicationを作成するtfstateとAWSリソースを作成するtfstateを分ける

tfstateを分けることで、myApplicationのリソースIDを取得してからAWSリソースのタグに設定できるのでランダムなIDを意識せずに済みます。 例えば次のようにmyApplicationを作成するtfstateとAWSリソースを作成するtfstateを分けて、myApplicationのリソースIDを取得してからAWSリソースのタグに設定します。

resource "aws_servicecatalogappregistry_application" "main" {
  name        = "foo"
  description = "Terraform managed."
}

outputs "my_application_id" {
  description = "myApplicationのリソースID"
  value = aws_servicecatalogappregistry_application.main.id
}

利用側で、ステートからmyApplicationのリソースIDを取得して、AWSリソースのタグに設定します。

data "tfe_outputs" "foo" {
  organization = "my-org"
  workspace = "my-workspace"
}

provider "aws" {
  region     = "ap-northeast-1"
  default_tags {
    tags = {
      awsApplication = data.tfe_outputs.foo.values.my_application_id
      # 他のタグ
    }
  }
}

myApplicationを作成してからdefault_tagsにmyApplicationのリソースIDをハードコードで設定する

default_tagsにmyApplicationのリソースIDをハードコードで設定します。タグは影響度が低いので、myApplicationのリソースIDは割り切ってしまっていい判断です。 設定がシンプルかつ、やっていることが一目瞭然で管理しやすいのがメリットです。 一方で、もしmyApplicationを作り直すときは、myApplicationのリソースIDを手動で変更する必要があります。

provider "aws" {
  region     = "ap-northeast-1"
  default_tags {
    tags = {
      awsApplication = "arn:aws:resource-groups:ap-northeast-1:123456789012:group/foo/km3hcsjf2h5izwou4jxqdvt7cz"
      # 他のタグ
    }
  }
}

awscc_resourcegroups_tag_sync_taskを使って自動同期させる

AWSコンソールからmyApplicationを作成するときAutomatically add resources using tagsというオプションがあり、これで作ると指定したタグを持つAWSリソースに対してmyApplicationのタグを自動的に設定してくれます。仕組み的には、EventBridgeとIAM Roleを組み合わせています。

Automatically add resources using tags

これと同じことをterraformで実現するのがawscc_resourcegroups_tag_sync_taskです。 残念ながらawsプロバイダーでは提供されておらず、awsccプロバイターを使う必要がありますが、人によってはこれも便利でしょう。ただし、TerraformリソースのデフォルトタグにmyApplicationのリソースIDを設定できないので、ignore_tagsを設定する必要があります。

# managedBy: terraformというタグを持つリソースをmyApplication fooに紐づける
resource "awscc_resourcegroups_tag_sync_task" "main" {
  group = "foo"
  role_arn = "IAM RoleのARN"
  tag_key = "managedBy"
  tag_value = "terraform"
}

# デフォルトタグにawsApplicationを設定できないので、ignore_tagsを設定する
provider "aws" {
  region     = "ap-northeast-1"
  ignore_tags = {
    keys = ["awsApplication"]
  }
}

まとめ

私はmyApplicationを作成してからdefault_tagsにmyApplicationのリソースIDをハードコードで設定する方法を主に使っています。 tfstateを参照する方法は、やりたいことに対して大げさすぎるんですよね。myApplicationを作るのは1回だけでしょうし、タグに紐づけたいだけなのでハードコードでも十分メンテできる判断です。

myApplicationのリソースIDからランダム文字列が消えれば扱いやすいのですが残念です。

参考

はてなブログワークフロー利用の課題と暫定対応

以前、GitHubとはてなブログを連動させているを書きました。はてなブログ提供のボイラーテンプレートを今も使っていますが、はてなワークフローは運用していく中で直接使わないケースも出てきたのでメモです。

はてなブログのワークフロー想定と使い勝手

はてなブログのボイラーテンプレートは、はてなが提供しているReusable Workflow hatena/hatenablog-workflowsを使っています。はてな管理のワークフローが実処理を担っているため、利用者はボイラーテンプレートを使うだけで済むという方針ですね。

実際割とよく機能するのですが、私はいくつかのワークフローはコピー&ペーストしてきて自前管理で運用しています。

はてなワークフローを使い続ける課題

はてなワークフローを使ってみると、いくつかの課題が見えてきました。以下に挙げるものは、私が実際に運用していて感じたことです。

ワークフローがタグ指定

hatena/Hatena-Blog-Workflows-Boilerplateとhatena/hatenablog-workflowsは、共にReusable Workflowやアクションをタグ指定しています。昨今のSHA指定が推奨される流れを考えると、タグ指定はセキュリティやメンテナンスの観点で課題があります。ボイラーテンプレートのREADMEを見るとworkflowの変更は原則 Reusable workflows を変更するため基本的には更新は不要ですと書いてあり、v1タグを指定している意図が分かります。

はてなブログのバージョン

タグ指定は、例えば次の利用シーンで課題があります。

  1. Reusable Workflowが使っているアクションで脆弱性があった時にユーザーは気づけず回避が難しい
  2. はてなブログワークフロー自身がSHA固定するプラクティスから外れている

先日のtj-actions/changed-filesの脆弱性CVE-2025-30066が発表されたとき、はてなブログのボイラーテンプレートはこれをタグで指定したため影響を受ける状況でした。脆弱性発生が土曜、SHAに切り替える修正は月曜に入ったのですが、はてなが管理しているため土日は修正されないままでした。Issueで脆弱性共有はあげたものの、PRをあげても週末にマージされないのは分かっていたためこのタイミングで私は自前ワークフローへの変更に切り替えました。

サードパーティアクションはSHA指定されたものの、はてなブログのボイラーテンプレートからワークフロー参照はv1タグを使っておりSHA指定されていません。このため、はてなブログのワークフロー自身が同様のCVE書き換えされた場合、ボイラーテンプレートを使っているユーザーは影響を受けることになります。

ワークフローやアクションの修正

hatena/hatenablog-workflowsの処理に手を入れたいとき、はてな側の方針とやりたいことが一致なければ変更が受け入れられないでしょう。例えば私はtj-actionsを使わないポリシーに変えましたが、はてなブログのボイラーテンプレートはtj-action/changed-filesをそのまま使う方針です。こういった方針の違いは当然ありますし、その場合はフォークするか自前でワークフローを持つしかないでしょう。それはそういうものです。

ボイラーテンプレートでの利用以上の利用想定がなさそう

これはAPI設計の話ですが、はてなブログのワークフローはボイラーテンプレートの利用しか想定されてなさそうです。

これは、ボイラーテンプレートにないワークフローの再利用は試されていなさそうな空気を修正PRから感じています。 この修正PRは、create-draftワークフローがcreate-draft-pull-requestアクションを呼び出す際の修正に関するものです。create-draft-pull-requestアクションはinputtitleを受け取りますが、ボイラーテンプレートではgithub.event.inputs.titleを指定していました。このため、アクションのtitleは使われず、呼び出し元のcreate-draftワークフロー呼び出し時のtitleが使用される仕様になっていました。これは、設計上の意図と異なる挙動である可能性が高いと考えられます。他のワークフローも呼び出しやinputsが柔軟ではないことから、ボイラーテンプレートの利用しか想定されていないと推測されます。

uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
env:
  OWNER_NAME: ${{ steps.set-owner.outputs.OWNER_NAME }}
  ENTRY_ID: ${{ steps.set-entry-variables.outputs.ENTRY_ID }}
  PREVIEW_URL: ${{ steps.set-entry-variables.outputs.PREVIEW_URL }}
with:
  title: ${{ github.event.inputs.title }}
  branch: draft-entry-${{ env.ENTRY_ID }}
  body: |
    ## ${{ github.event.inputs.title }}

    省略

アクションに渡したtitleを使うには次のようにinputs.titleを指定する必要があります。

    - name: create draft pull request
      uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7
      env:
        OWNER_NAME: ${{ steps.set-owner.outputs.OWNER_NAME }}
        ENTRY_ID: ${{ steps.set-entry-variables.outputs.ENTRY_ID }}
        PREVIEW_URL: ${{ steps.set-entry-variables.outputs.PREVIEW_URL }}
      with:
        title: ${{ inputs.title }}
        branch: draft-entry-${{ env.ENTRY_ID }}
        body: |
          ## ${{ inputs.title }}

          省略

暫定対応

私の運用では、ボイラーテンプレートをフォークして運用するのではなく、コピー&ペーストしてファイル名の先頭に_をつけて運用しています。自分のポリシーと合わないものだけをコピーして修正、あとはボイラーテンプレートのものを使うという運用です。ポリシーは次の通りです。

  • 自分が使いたくないアクションを差し替える
  • できないことをできるようにする
  • hatenablog-workflowsもSHA指定しつつ自動更新する

自分が使いたくないアクションを差し替える例

例えば、hatenablog-workflowsから.github/workflows/push-published-entries.yamlワークフローをもってきて.github/workflows/_push-published-entries.yamlとして保持しています。中で他のワークフローを参照していたらそれももって来る感じです。もともと参照していたワークフローはコメントアウトして、いつでも戻せるようにしておきます。

# BEFORE: hatena/hatenablog-workflows | .github/workflows/push-published-entries.yaml
name: "[Reusable workflows] push published entries"

on:
  workflow_call:
    secrets:
      OWNER_API_KEY:
        required: true

jobs:
  upload-images:
    if: github.event.pull_request.merged == false
    uses: hatena/hatenablog-workflows/.github/workflows/upload-images.yaml@4cb2032c9665ad3b0eba9835182e2d23a1d49a81 # v1
    secrets:
      OWNER_API_KEY: ${{ secrets.OWNER_API_KEY }}



# AFTER: guitarrapc/blog | _push-published-entries.yaml
name: "[Reusable workflows] push published entries"

on:
  workflow_call:
    secrets:
      OWNER_API_KEY:
        required: true

jobs:
  upload-images:
    if: github.event.pull_request.merged == false
    # uses: hatena/hatenablog-workflows/.github/workflows/upload-images.yaml@v1
    uses: ./.github/workflows/_upload-images.yaml
    secrets:
      OWNER_API_KEY: ${{ secrets.OWNER_API_KEY }}

これはupload-images.yamlがtj-actions/changed-filesを使っているため、私のポリシーに合わないからです。変更した際は、外部アクションをすべてSHAに変更しつつ、tj-actions/changed-filesからdorny/paths-filterにしていました。以下は、現在(左)とhatenablog-workflows(右)の差分です。

upload-images.yaml

できないことをできるようにする

例えば、.github/actions/create-draft-pull-request/action.yamlはPRボディが固定文字になっています。PULL_REQUEST_TEMPLATE.mdを指定して持ってくるか、文章を変更するかで悩みましたが、後者にしました。本家の1.3.8でPULL_REQUEST_TEMPLATE.mdがあれば指定できるようになるPRが取り込まれているので、1.3.8が出たら戻す予定です。

hatenablog-workflowsもSHA指定しつつ自動更新する

ユーザーがhatenablog-workflowsワークフローを固定するときも、v1タグで運用する想定なのはちょっと微妙です。初めからSHA指定にしておいてDependabotで自動更新するようにテンプレート展開すれば、ユーザーは自分で更新する手間を最小にしつつワークフローを更新するか選択できます。

私は次のようにして、npmとGitHub Actionsの両方をDependabotで更新するようにしています。これにより、SHA指定しつつも自動更新が可能になります。ボイラーテンプレートでもGitHub Actionsの更新をする.github/dependabot.yamlが追加されればタグ指定からSHA指定に変更できそうですが、現状はSHA指定のままです。

# ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly" # Check for updates to GitHub Actions every week
    ignore:
      # I just want update action when major/minor version is updated. patch updates are too noisy.
      - dependency-name: "*"
        update-types:
          - version-update:semver-patch
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly" # Check for updates to GitHub Actions every week
    ignore:
      # I just want update action when major/minor version is updated. patch updates are too noisy.
      - dependency-name: "*"
        update-types:
          - version-update:semver-patch

まとめ

hatena/hatenablog-workflowsは多くの人が使っていて、ワークフローとして想定があるのでなかなか修正を出しにくいものもあります。API的に問題ない場合はこれからも提示していきますが、合わない割り切って手元管理でいい気持ちもあります。徐々に良くなってきているので、今後も貢献できるものはしたいです。

参考

GitHub ActionsのCVE-2025-30066を受けたワークフローの変化

2025年3月28日にあったtj-actionやreviewdogのセキュリティインシデントCVE-2025-30066を受けて、GitHub Actionsワークフローの書き方が変えてしばらく経ちました。その対応メモを残します。

CVE-2025-30066の概要

CVE-2025-30066の詳細はセキュリティインシデントに詳しいですが、簡単に説明するとspotbugs/sonar-findbugsを起点にreviewdog/action-setupを経由してtj-action/changed-filesのリリースのほとんどが改ざんされました。改ざんコミットは、base64エンコードされたPythonのエクスプロイトコードを取得/実行するもので、GitHub Actionsのランナーで実行されるとアクションシークレットを取得してBASE64エンコーディングしてログ出力します。攻撃者の狙いはcoinbase/agentkitだったようですが、tj-action/changed-filesの利用者が多く、影響範囲が広がりました。

ユーザーインパクトは、tj-action/changed-filesをタグ指定で利用しているGitHub ActionsワークフローでアクションシークレットがActionログに出力されます。パブリックリポジトリはビルドログを第三者が確認できるため、GitHub Actionsのシークレットが漏洩します。プライベートリポジトリはCIログを外部から確認できないため影響は限定的です。

なお、CVE発生時にGitHubは次の対応をしています。プラットフォーマーのアクションも興味深いです。

  • tj-action/changed-filesのリポジトリ/アクションを削除してワークフロー使用できないように変更
  • アクションのすべてのリリースがクリーンアップ、悪意あるコードが含まれなくなってからリポジトリを復旧
  • 悪意のあるユーザーjurkaofavak、randolzfowの削除
  • Gistに公開されていたエクスプロイトで利用されたPythonコードの削除1
  • 各ユーザーのGitHub PATを期限切れに設定 (各ユーザーはPATを発行しなおしたはずです)

CVE-2025-30066の影響を軽減するにはどうすればよいのか

CVE-2025-30066のようなGitHub Actionsのインシデントは今後も起こる可能性が高いです。GitHub Actionsはオープンソースであり誰でもアクションを作成・コントリビュートできますし、メンテナのPATが漏洩して意図せず変更される可能性もあるでしょう。私が利用者として改めて認識したポイントは次の通りです。

  1. アクションのタグ/リリースは書き換えられる可能性がある
  2. 利用者のPersonal Access Token (PAT)やGITHUB Appはアクションで漏れる可能性がある
  3. ワークフローやジョブに設定した環境変数/シークレットはアクションを使ってCIログに書き出せる
  4. ワークフローやジョブの権限が高いほどインパクトが大きい
  5. パブリックリポジトリのpull_request_targetイベントの利用はリスクが高い
  6. コミットは別ユーザーで偽れる
  7. 利用できるアクションを限定するのはある程度有効

ポストモーテムで得られる教訓を並べてみましょう。このうちA, C, D, F, Gは静的解析ツールで自動検知できるので優先度高めに対応します。Bはユーザー/オーガニゼーションで一度設定すればOKそうです。E, H, I,Jはリポジトリごとに設定が必要です。Kはアクションを採用するときの判断基準です。

  • A: アクションはタグ/リリースではなくコミットSHAを指定する (pinactで対応可能)
  • B: ワークフローに指定するPATやGITHUB Appは権限を最小限にする
  • C: シークレットを環境変数に設定する場合、ステップの環境変数に設定する (ghalintで検出可能)
  • D: ジョブごとに最小権限を設定する (ghalintで検出可能)
  • E: リポジトリの自動アクショントークンのデフォルト権限をwriteからreadに設定2
  • F: パブリックリポジトリでpull_request_targetイベントは極力避ける (ghalint/zizmorで検出可能)
  • G: ワークフローの記法をそろえる (actionlintで検出可能)
  • H: Rulesetでコミット署名の必須化を検討する
  • I: Rulesetでタグの更新・削除を禁止
  • J: Rulesetでタグの作成可能なユーザーをorganization adminやrepository adminに限定する
  • K: アクションの利用前に処理内容を把握、管理する

静的解析ツールで設定の確認/調整/自動化、次いでユーザー/オーガニゼーションの設定見直し、リポジトリごとの設定、という順番で対応してみましょう。

私は今回のCVE経過中の対応を見て、tj-actionとreviewdogは使わないことにしました。

静的解析ツールと自動化

真っ先に取り組むべきは静的解析ツールによる自動化です。リポジトリを複数管理していると手動でワークフローを調整するのは無理です。時間は有限なので、できるだけ自動化して見落としを防ぎつつ精度を高めましょう。ツールには静的解析ツールとセキュリティプラクティスをワークフローに自動適用するヘルパーツールがあります。

使っているワークフローの静的解析ツールは次の通りです。これだけだと指摘が多すぎると対応が困難になるため、ワークフローを自動修正するヘルパーツールも併用します。

ツール名 説明
actionlint ワークフローの構文チェックとShellcheckなど
ghalint ワークフローの簡易的なセキュリティチェック
zizmor ワークフローのより厳しいセキュリティチェック

セキュリティプラクティスをワークフローに自動適用するヘルパーツールは、静的解析で指摘されるいくつかを修正してくれます。残念ながら限定的な修正ですが、まずはこれを実行してから静的解析して修正するかどうか決めるといいでしょう。

ツール名 説明
pinact タグやブランチ指定されたアクションをSHAに変換する
disable-checkout-persist-credentials checkoutアクションのpersist-credentialsを無効化する

私は次の流れで自動化を進めました。順に見ていきましょう。

  1. pinactでSHAに変換する
  2. disable-checkout-persist-credentialsでcheckoutアクションのpersist-credentialsを無効化する
  3. Dependabotでアクション更新を自動化
  4. actionlintを実行してワークフロー構文を標準化する
  5. ghalintでセキュリティチェックを実行して、出てきたエラー/警告を修正する
  6. (Optional) zizmorでセキュリティチェックを実行して、出てきたエラー/警告を修正する
  7. GitHub Actionsで静的解析を自動化する

アクションをSHAで固定する(pinact)

pinactはGitHub ActionsのタグやブランチをSHAに一括変換するツールです。SHAに変換することで、今回のインシデントのようにタグに悪意のあるコードが仕込まれた場合でも影響を受けません。3

後述するghalintでSHA指定していないのを検出できるのですが、pinactコマンドを叩くだけで修正できます。

ghalintのエラー例

$ pinact run

嬉しいポイント

コマンドを実行するだけでSHAに変換されるので、手作業より圧倒的に簡単、確実です。SHAにしたいならpinactで99%解決します。コンフィグファイルもありますが、おおむね不要でしょう。

# ❌: tag指定
- uses: actions/checkout@v4

# ⭕: SHA指定
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

ドキュメントもアップデート対象にできますが、これはうまくいかないケースもあるようなのでうまくいったら便利程度でいいでしょう。

つらいポイント

Organization内部で常にブランチを見たいケースでSHAに変換されるのは邪魔です。先ほどの1%に該当するので都度対処でいいです。 設定ファイル.pinact.yamlignore_actionsに指定するとSHAに変換されないようにできます。

# 意図的にブランチを指定したいケースでSHAに変換されると面倒
- uses: foo-organization/bar-action@main

actions/checkoutのpersist-credentialsをfalseに変換する(disable-checkout-persist-credentials)

GitHub Actionsでチェックアウトするときはactions/checkoutを使うのが定番です。ただデフォルトでpersist-credentials: trueになっており、悪意あるアクションがリポジトリにコミット障壁がpermissionだけです。明示的にpersist-credentials: falseを設定することで、チェックアウト後のGit認証情報を無効してGit認証情報にアクセスできるステップを限定できます。

後述するghalintzizmorでfalseにしていないのを検出できるのですが、disable-checkout-persist-credentialsコマンドを叩くだけで修正できます。

ghalintのエラー例

zizmorのエラー例

$ disable-checkout-persist-credentials

嬉しいポイント

コマンドを実行するだけでpersist-credentials: falseが設定されるので、手作業より圧倒的に簡単、確実です。

# ❌: persist-credentialsがtrueだとghalintでエラーが出る
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
  with:
    persist-credentials: true

# ❌: persist-credentialsが無指定(true相当)だとghalintとzizmorでエラーが出る
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

# ⭕: persist-credentialsがfalseを指定する
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
  with:
    persist-credentials: false

つらいポイント

persist-credentials: falseにすると、後続のステップでプッシュできなくなります。地味にめんどくさいですが定形パターンで対応できます。

fatal: could not read Username for 'https://github.com': No such device or address
Failed to push, try 'git pull --rebase' to resolve ...

GitHub Actionsでプッシュするには、git remote set-urlで認証付きリモートURLを設定します。コミット前にリモートリポジトリ情報をセットしてあげればpushできるようになります。

# ❌: git pushができない
- name: git push
  run: git push

# ⭕: コミット前にリモートリポジトリ情報をセットする
- name: update current git to latest & Push changes
  shell: bash
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    git remote set-url origin "https://github-actions:${GITHUB_TOKEN}@githb.com/${{ github.repository }}"
    git config user.name  "github-actions[bot]"
    git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: git push
  run: git push

Actionsの自動更新を行う(Dependabot)

リポジトリにDependabot4の設定ファイル.github/dependabot.yamlを用意しておくとDependabotによるGitHub Actionsのバージョン更新が自動化できます。幸いにしてSHAで指定した場合でもDependabotはSHAを更新してくれます。

# 元がSHA指定でも
- uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1

# Dependabotは更新してくれる
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0

更新頻度を調整する

頻繁すぎる更新は好ましくないと考えているので、週一更新かつパッチバージョンは更新しないようにしています。普段使っている設定は次の通りです。

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly" # Check for updates to GitHub Actions every week
    ignore:
      # I just want update action when major/minor version is updated. patch updates are too noisy.
      - dependency-name: '*'
        update-types:
          - version-update:semver-patch

アクションの静的解析を行う(actionlint)

ここからは静的解析ツールです。

actionlintはGitHub Actionsのワークフロー/アクションの構文チェックを行うツールです。構文チェックだけでなく、Shellcheckやpyflakeなども実行してくれます。YAMLの書き方が人それぞれなのはしょうがないですが、actionlintを使うことで書き方をある程度揃えられます。

actionlintを使うと、次のような状況を改善できます。

  • GitHub Actionsのワークフローの妥当性検出
  • GitHub Actionsの${{ }}構文の妥当性を検出
  • runs-onに指定された不明なランナーを検出
  • githubコンテキストでリスクが高いパラメーターをrunsに直接埋め込んでいると検出
  • Composite Actcionsのシェルがないと検出

実行も簡単です。actionlintが何かしら検出すると終了コードが1になるので、CI/CDで使いやすいです。後述しますが、私はCI/CDでactionlintを用いています。

$ actionlint

actionlintの検出例

# ❌: runs-onがないのでエラー
jobs:
  dump-context:
    timeout-minutes: 5
    steps:
      - run: echo foo

# ⭕: runs-onを指定する
jobs:
  dump-context:
    timeout-minutes: 5
    runs-on: ubuntu-24.04
    steps:
      - run: echo foo

actionlintでruns-onがないのを検出

除外ルール

なかにはルールを除外したい場合もあるでしょう。.github/actionlint.yaml除外ルールを書くことで検出から除外できます。特に長年shellcheckはコメントでしか除外できなかったのが改善して最高です。

例えば次のstepを用意します。

- name: action
  run: echo $GITHUB_ACTION
  env:
    GITHUB_ACTION: ${{ github.action }}

actionlintを実行すると、shellcheckのSC2086に引っかかります。

$ actionlint
.github\workflows\context-github.yaml:53:9: shellcheck reported issue in this script: SC2086:info:1:6: Double quote to prevent globbing and word splitting [shellcheck]
   |
53 |         run: echo $GITHUB_ACTION
   |         ^~~~

SC2086は有益なことも多いのですが微妙なことも多いので、.github/actionlint.yamlに除外ルールを設定しておきます。これで引っかからなくなります。

paths:
  .github/workflows/**/*.{yml,yaml}:
    ignore:
      - 'shellcheck reported issue in this script: SC2086:.+'

嬉しいポイント

actionlintを使う最大のメリットは、記法が一定になることです。例えばGitHub Actionsの構文はif: 式if: ${{ 式 }}の2つの書き方があります。actionlintを使うことで後者に統一されるので、VS Codeの拡張でも式がコードハイライトされるのでよいです。 記法の統一は、GitHub Actionsをチームでハンドリングするにあたって地味に大事です。

つらいポイント

shellcheckを除外するのが正直面倒です。例えば$GITHUB_ENVとかも"$GITHUB_ENV"のようにダブルクォートで指定する必要があるので、除外ルールに書きたくなります。

actionlint自体の良さと、bash自体に疲弊するバランスが難しい。というか、shellcheckが割とやりすぎている感じがあってactionlintの印象がshellcheckに引っ張られている気がします。

アクションのセキュリティチェックを行う(ghalint)

ghalintはGitHub Actionsのワークフロー/アクションの簡易的なセキュリティチェックを行うツールです。 一番気になるところをカバーしてくれ、Goで書かれていてOS問わず動かしやすいため、まず検討するのがおすすめです。 野良のワークフローはほぼghalintの指摘をできていないので、どこを見るといいか目を養えます。

ghalintは次のようなことを検出してくれます。

  • permissionsの指定をしていないと検出
  • read-allwrite-allの過剰な権限を検出
  • アクションの指定をフルSHAにしていないと検出
  • persist-credentials: falseがないactions/checkoutを検出
  • secrets: inheritを検出
  • secretsのワークフローレベル環境偏すへの設定を検出
  • secretsのジョブレベル環境変数への設定を検出
  • コンテナジョブのイメージにlatestを指定していると検出
  • GitHub Appをリポジトリ限定していないと検出
  • ジョブのタイムアウト指定がないと検出

実行も簡単です。ghalintが何かしら検出すると終了コードが1になるので、CI/CDで使いやすいです。後述しますが、私はCI/CDでghalintを用いています。

# ワークフローへのghalint実行
$ ghalint run

# アクションへのghalint実行
$ ghalint run-action

例えば次のようなワークフローはghalintで検出されます。

name: foo bar
on:
  push:
    branches: ["main"]

# ❌: permissionsがジョブレベルにないのでエラー (複数ジョブの時だけ検出)
permissions:
  contents: read

# ❌: secretsをワークフローレベルの環境変数に指定しているのでエラー
env:
  BAR: ${{ secrets.BAR }}

jobs:
  action:
    # ❌: secretsをジョブレベルの環境変数に指定しているのでエラー
    env:
      FOO: ${{ secrets.FOO }}
    runs-on: ubuntu-24.04
    # ❌: timeout-minutesがないのでエラー
    steps:
      # ❌: actions/checkoutがSHA指定されていないのでエラー
      # ❌: persist-credentials=falseがないのでエラー
      - uses: actions/checkout@v4.2.2
      - run: echo "FOO=$FOO"

  action-2:
    runs-on: ubuntu-24.04
    timeout-minutes: 3
    steps:
      - run: echo "BAR=$BAR"

ghalintの検出例

メッセージにエラー原因や修正方法が書かれているので従いましょう。

name: foo bar
on:
  push:
    branches: ["main"]

jobs:
  action:
    # ⭕: permissionsをジョブレベルに指定する
    permissions:
      contents: read
    runs-on: ubuntu-24.04
    # ⭕: timeout-minutesを指定する
    timeout-minutes: 3
    steps:
      # ⭕: actions/checkoutをSHA指定する
      # ⭕: persist-credentials=falseを指定する
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          persist-credentials: false
      # ⭕: secretsをステップレベルの環境変数に指定する
      - run: echo "FOO=$FOO"
        env:
          FOO: ${{ secrets.FOO }}

  action-2:
    # ⭕: permissionsをジョブレベルに指定する
    permissions:
      contents: read
    runs-on: ubuntu-24.04
    timeout-minutes: 3
    steps:
      # ⭕: secretsをステップレベルの環境変数に指定する
      - run: echo "BAR=$BAR"
        env:
          BAR: ${{ secrets.BAR }}

除外ルール

なかにはルールを除外したい場合もあるでしょう。.ghalint.yamlに除外ルールを書くことでルール検出から除外できます。検出はエラーメッセージのログから作ることができて便利です。

例えば次のようなワークフローを用意します。自分のリポジトリのワークフローなのでブランチ指定を許可したいのですが、SHA指定じゃないので当然エラーになります。また自リポジトリのReusable Workflowはsecrets: inheritを許可したいのですが、inheritはエラーになります。

jobs:
  call-workflow-passing-data:
    permissions:
      contents: read
    uses: guitarrapc/githubactions-lab/.github/workflows/_reusable-workflow-called.yaml@main # ghalintでエラーになる

  call-workflow-passing-data2:
    permissions:
      contents: read
    uses: ./.github/workflows/_reusable-workflow-nest.yaml
    with:
      username: "foo"
      is-valid: true
    secrets: inherit # ghalintでエラーになる

ghalintを実行すると次のようなエラーが出ます。action_ref_should_be_full_length_commit_shaポリシーに引っかかっているのが分かります。

$ ghalint run
ERRO[0000] the job violates policies                     error="`secrets: inherit` should not be used. Only required secrets should be passed explicitly" job_name=call-workflow-passing-data2 policy_name=deny_inherit_secrets program=ghalint reference="https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/policies/004.md" version=1.2.3 workflow_file_path=".github\\workflows\\reusable-workflow-public-caller.yaml"
ERRO[0000] the job violates policies                     action=guitarrapc/githubactions-lab/.github/workflows/_reusable-workflow-called.yaml error="action ref should be full length SHA1" job_name=call-workflow-passing-data policy_name=action_ref_should_be_full_length_commit_sha program=ghalint reference="https://github.com/suzuki-shunsuke/ghalint/blob/main/docs/policies/008.md" version=1.2.3 workflow_file_path=".github\\workflows\\reusable-workflow-public-caller.yaml"

自分のリポジトリのactionは除外したいので、.ghalint.yamlに除外ルールを書きましょう。action_ref_should_be_full_length_commit_shaaction_nameを求めるのに注意です。ワイルドカードを指定できるのでaction_nameでワイルドカード指定、workflow_file_pathを省略すれば自分のリポジトリのReusable Workflowをまとめて除外もできます。

excludes:
  - policy_name: action_ref_should_be_full_length_commit_sha
    action_name: guitarrapc/githubactions-lab/.github/workflows/*
    workflow_file_path: .github/workflows/reusable-workflow-public-caller.yaml
  - policy_name: deny_inherit_secrets
    workflow_file_path: .github/workflows/reusable-workflow-public-caller.yaml
    job_name: call-workflow-passing-data2

嬉しいポイント

ghalintを使うことで、これはまずいのか、まずくないのか、という部分が定形的に検出されて修正する方向に力が働きます。指摘の程度もあまりに厳しいものはないので、使い勝手のバランスがいいです。

再利用可能なワークフローでsecrets: inheritをエラーにするのは割といい指摘に感じます。ついやっちゃいがちですが、パブリックなリポジトリではsecretsをちゃんと明示するほうがいいでしょう。

# ❌: secretsをinheritにするとエラー
jobs:
  call-workflow-passing-data:
    uses: ./.github/workflows/_reusable-workflow-called.yaml
    with:
      username: ${{ inputs.username }}
      is-valid: ${{ inputs.is-valid }}
    secrets: inherit

# ⭕: secretsを明示する
jobs:
  call-workflow-passing-data:
    uses: ./.github/workflows/_reusable-workflow-called.yaml
    with:
      username: ${{ inputs.username }}
      is-valid: ${{ inputs.is-valid }}
    secrets:
      APPLES: ${{ secrets.APPLES }}

actions/checkoutでpersist-credentials: falseが指定されていないとエラーにするのもいい感じです。

# ❌: persist-credentialsが未指定やtrueだとエラーが出る
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

# ⭕: persist-credentialsがfalseだとエラーにならない
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
  with:
    persist-credentials: false

つらいポイント

記法に依存した指摘があるのは微妙です。例えば、ジョブが1つならワークフローレベルのpermissionsを指定しても問題ないのですが、ジョブが複数ある場合はワークフローレベルのpermissionsを指定してもエラーになります。これは書き方に依存する5ので、ジョブに一本化したほうがいいんじゃないのかなぁと感じます。

ghalintには現在、pinactやdisable-checkout-persist-credentialsのような自動修正機能はありませんが、これらが追加されると今どきのlintツールらしく使えそうです。良いかどうかは分かりませんが。

アクションのより細かいセキュリティチェックを行う(zizmor)

zizmorはアクションのセキュリティチェックを行うツールで、ghalintよりもチェックが厳し目です。ghalintで検出されないものもzizmorで検出されるので、ghalintで修正した後にzizmorを実行するのがいいでしょう。個人的にはghalintかzizmorのどちらかで十分だと考えているので、zizmorはオプショナルとしました。

Rustで書かれているのですが、各プラットフォーム向けのバイナリインストールは手薄なため、Docker経由で実行しています。

# リポジトリを/githubにマウントしてmedium以上のエラーを検出する
docker run -t -v .:/github ghcr.io/woodruffw/zizmor:1.5.2 /github --min-severity medium

例えば次のようなワークフローはzizmorで検出されます。

on:
  # ❌: pull_request_targetはdangerous-triggersなのでzizmorでエラーになる
  pull_request_target:
    branches: ["main"]

on:
# ⭕: pull_requestはdangerous-triggersじゃないのでzizmorでエラーにならない
  pull_request:
    branches: ["main"]

除外ルール

zizmorの除外ルールはコメントベースとコンフィグがあります。コンフィグは行指定なので使いにくく、コメントベースのほうが使いやすいです。

      - run: echo "org:${{ matrix.org }} secret:${SECRET}"
        env:
          SECRET: ${{ secrets[matrix.secret] }} # zizmor: ignore[overprovisioned-secrets]

嬉しいポイント

ghalintよりも厳しいチェックをしてくれるので、セキュリティ的に安心です。ルールが一覧化されているのでドキュメントが分かりやすいのもいいですね。

つらいポイント

ghalintと重複している項目が多く、ghalintで除外する場合zizmorでも除外する必要があります。ghalintで十分ではという気分になるのは否めません。

zizmorを使っていて、修正がちょっと厄介に感じたものを紹介します。inputsは不正な文字列を差し込まれる可能性があるので環境変数を使えという指摘です。booleanなどinputs次第で問題ない可能性があるにも関わらずinputsに対して一律厳しいのはめんどくさく感じました。

# ❌: runでinputsを直接使おうとするとhigh levelのエラーが出る
- name: Output foo input
  shell: bash
  run: echo "foo is ${{ inputs.foo != '' && inputs.foo || env.FOO }}"

# ⭕: 環境変数にいれてから環境変数をrunで指定する。
- name: Output foo input
      shell: bash
      run: echo "foo is ${VALUE}"
      env:
        VALUE: ${{ inputs.foo != '' && inputs.foo || env.FOO }}

githubコンテキストも不正な文字列を差し込まれる可能性があるので環境変数を使えという指摘です。ブランチ名やIssueタイトルなど、コミッターが自由にいじれるものはインジェクション余地があるのでまずいのは同意ですが、event_nameなどGitHubコンテキストすべてに一律厳しいのはめんどくさく感じました。

# ❌: event_nameを直接使おうとするとhigh levelのエラーが出る
- name: file names
  id: file
  run: echo "name=${{ github.event_name }}${{ github.event.action != '' && format('_{0}', github.event.action) || ''}}${{ github.event_name == 'push' && format('_{0}', github.ref_type) || ''}}" | tee -a "$GITHUB_OUTPUT"

# ⭕: 環境変数にいれてから環境変数をrunで指定する。
- name: file names
  id: file
  env:
    EVENT_NAME: ${{ github.event_name }}
    EVENT_ACTION: ${{ github.event.action }}
    REF_TYPE: ${{ github.ref_type }}
  run: |
    ACTION_PART=""
    if [ -n "$EVENT_ACTION" ]; then
      ACTION_PART="_${EVENT_ACTION}"
    fi

    REF_PART=""
    if [ "$EVENT_NAME" = "push" ]; then
      REF_PART="_${REF_TYPE}"
    fi

    FILENAME="${EVENT_NAME}${ACTION_PART}${REF_PART}"
    echo "name=$FILENAME" | tee -a "$GITHUB_OUTPUT"

GitHub Actionsで静的解析を自動化する

静的解析ツールをGitHub Actionsで自動実行すると、意図しないミスを素早く検出/修正できます。私はactionlint/ghalint/zizmorをGitHub Actionsで自動実行しています。

GitHub Actionsで同ツールを使うにあたり、aquaでツールバージョンを設定しておくとローカルとCI/CDで同じバージョンを簡単に利用できるのでオススメします。例えば次のようなaqua.yamlをリポジトリルートに設定しておきます。

---
# aqua - Declarative CLI Version Manager
# https://aquaproj.github.io/
# checksum:
#   enabled: true
#   require_checksum: true
#   supported_envs:
#   - all
registries:
- type: standard
  ref: v4.332.0 # renovate: depName=aquaproj/aqua-registry
packages:
- name: rhysd/actionlint@v1.7.7
- name: suzuki-shunsuke/ghalint@v1.2.3

GitHub Actionsワークフローを用意します。zizmorはdockerで実行しています。

name: actionlint
on:
  workflow_dispatch:
  pull_request:
    branches: ["main"]
    paths:
      - ".github/workflows/**"
  schedule:
    - cron: "0 0 * * *"

jobs:
  lint:
    permissions:
      contents: read
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          persist-credentials: false
      - uses: aquaproj/aqua-installer@e2d0136abcf70b7a2f6f505720640750557c4b33 # v3.1.1
        with:
          aqua_version: v2.43.1
      # github workflows/action's Static Checker
      - name: Run actionlint
        run: actionlint -color -oneline
      # checkout's persist-credentials: false checker
      - name: Run ghalint
        run: ghalint run
      # A static analysis tool for GitHub Actions
      - name: Run zizmor
        run: docker run -t -v .:/github ghcr.io/woodruffw/zizmor:1.5.2 /github --min-severity medium

GitHubユーザー/オーガニゼーションの設定

ワークフローに指定するPATやGITHUB Appは権限を最小限にするして、トークン漏洩時の影響を限定します。漏れたPATで他のリポジトリにも影響したのが今回のCVEのまずいポイントの1つです。

  • Legacy PATをやめてFine-grained PATを検討しましょう
  • Fine-grained PATはリポジトリや権限を絞りましょう
  • GITHUB Appはリポジトリや権限をを絞りましょう

GitHubリポジトリごとの設定

リポジトリの自動アクショントークンのデフォルト権限をwriteからreadに設定して、トークン漏洩時の影響を限定します。漏れたPATでWrite権限があり、アクションでも限定できていなかったのが今回のまずいポイントの1つです。

  • リポジトリ > Settings > Actions > General > Workflow permissionsReadに変更しましょう6

ワークフロー権限をRead権限に限定する

リポジトリのルールセットでタグの更新や削除を禁止することで、PATが漏れて権限があったとしてもアクションの過去リリース上書きを防げます。リリースできる人を限定できるなら、それもある程度有効でしょうが運営に影響するので微妙なラインです。もしコミッターが制限されていて、全員がGPG署名をしているなら、コミット署名の必須化も有効です。GitHub ActionsのコミットはGPG署名されないので、GPG署名を必須にしておくことでGitHub Actionsでのコミットを防げます。

ルールセットの設定例

まとめ

いろいろな対応があり、どれを採用するかはチームの考えに依存するでしょう。ただ、少なくとも権限を小さくして、まずい設定になっていないか自動検出だけはしましょう。それだけで、今回のケースは防げましたし、影響を他のリポジトリに伝播させることはありませんでした。

サプライチェイン攻撃といっても、リポジトリ単体で影響を抑えれば影響を波及しないことを再確認したCVEでした。

参考

インシデント報告

他の人の対応例

ツール一覧


  1. https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py
  2. 新しいリポジトリを作るとデフォルトreadですが、以前からのリポジトリはデフォルトwriteになっています
  3. コミットで同一SHAに衝突されるケースでは問題になる可能性がありますが、それを言い出すとGitという仕組み自体を考え直すことになるので考慮外とします
  4. もしRenovateを使っているならそれでもいいです。
  5. ジョブを増やしたらジョブレベルに書き方直す必要が出てくるので、最初からジョブレベルに書いておくのがいいでしょう
  6. 新しいリポジトリはデフォルトReadですが、以前からのリポジトリはデフォルトWriteになっています

NuGet Central Package Managementに移行するのにCopilotを使う

VS CodeのCopilotを使って、NuGet Central Package Managementに移行する体験がよかったのでメモです。

.NET Upgrade Assistantが機能しないケース

以前、.NETアップグレードアシスタントでNuGet Central Package Managementに移行する記事を紹介しましたが、中には移行ダイアログがグレーアウトして移行できないケースがあります。 そんな時でもCopilotを使うと簡単に移行できます。

※ グレーアウトするのは1プロジェクトだけNuGetパッケージ参照があるケースのようです。

グレイアウトして移行できない

Copilotで移行する

VS CodeにてCopilotをエージェントモード1で起動して、Directory.Packages.propsを作成するように指示します。

NuGet Central Package Managementを利用したいので、現在のNuGetパッケージを移行してください。

Copilotへの指示と結果

移行完了です。

Directory.Packages.propsの追加

既存csprojからパッケージを移行

Before

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="coverlet.collector" Version="6.0.2" />
    <PackageReference Include="GitHubActionsTestLogger" Version="2.4.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
    <PackageReference Include="NSubstitute" Version="5.3.0" />
    <PackageReference Include="xunit.v3" Version="2.0.1" />
    <PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\DotfilesLinker\DotfilesLinker.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="Xunit" />
  </ItemGroup>

</Project>

After

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="coverlet.collector" />
    <PackageReference Include="GitHubActionsTestLogger">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.NET.Test.Sdk" />
    <PackageReference Include="NSubstitute" />
    <PackageReference Include="xunit.v3" />
    <PackageReference Include="xunit.runner.visualstudio" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\DotfilesLinker\DotfilesLinker.csproj" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="Xunit" />
  </ItemGroup>

</Project>

Directory.Packages.props

<Project>
  <PropertyGroup>
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
  </PropertyGroup>

  <ItemGroup>
    <!-- Test packages -->
    <PackageVersion Include="coverlet.collector" Version="6.0.2" />
    <PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
    <PackageVersion Include="NSubstitute" Version="5.3.0" />
    <PackageVersion Include="xunit.v3" Version="2.0.1" />
    <PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
  </ItemGroup>
</Project>

まとめ

Copilotは、人の手でやるにはめんどくさく機械的にできることの精度が高いので、NuGet Central Package Managementでも利用できるのは便利でした。 Copilotがいればcsprojの管理もお任せできて便利。


  1. Claude 3.7 Sonnetはいい感じですがGPT-4oは微妙

Visual Studio 2022で使っている拡張機能 2025年版

以前Visual Studio 2022で使っている拡張機能を紹介しましたが、さすがにいろいろ変わりました。 2025年版です。

一覧

Visual Studio拡張一覧のスクショです。末尾で紹介しますが、拡張よりもGitHub Copilotのほうが重要です。

※ デフォルトで入っているLive Share 2022、Microsoft Library Manager、Test Adapter for Boost.Test、Test Adapter for Google Testは除きます。

Visual Studio拡張一覧

.NET Upgrade Assistant

.NET Upgrade Assistant - Visual Studio Marketplace

プロジェクトやソリューションの.NETをアップグレードする拡張機能です。よく.NET Frameworkから.NET 6+への移行と紹介されていますが、Central Package Managementへの移行にも使えます。入れていても普段の開発にトラブルは起こさないので、とりあえずいれておいてもいい代物と判断しています。

Copy Nice

Copy Nice - Visual Studio Marketplace

Visual Studioでコードをコピーして他に貼り付ける時、インデントをきれいにしてくれる拡張機能です。

例えばインデントのあるコードをVisual Studioでコピーして、VS Codeに貼り付けるとインデントが崩れます。これをきれいにしてくれます。地味に便利です。

インデントのあるコードをVisual Studioでコピー

// Copy Niceがないとこうなる
public void Success(string message)
    {
        WriteSuccess(message);
    }

// Copy Niceがあるとこうなる
public void Success(string message)
{
    WriteSuccess(message);
}

Git Diff Margin

Git Diff Margin - Visual Studio Marketplace

Gitのdiff状態がコードの左に表示されます。未保存が黄色、追加行が緑、変更が青、削除が赤で表示されています。 削除内容も赤い三角から見えるので、VSから動かずにどんな変更だったかを見るのに重宝します。

現在も欠かせない拡張です。VS Codeはデフォルトで入っているので体験が一致するのもいいですね。

Solution Error Visualizer 2022

Solution Error Visualizer 2022 - Visual Studio Marketplace

Visual Studioのエラーリストビューに表示されるエラーや警告、情報をSolution Explorerでハイライト表示してくれます。

gif

割とMessageは無視しがちなので、Messageは表示しないのがオススメです。

Messageは無視する

Time Stamp Margin 2022

Time Stamp Margin 2022 - Visual Studio Marketplace

出力ウィンドウのDebug出力で分以降のタイムスタンプを表示してくれます。地味に便利。

タイムスタンプが追加される

よくバッチ出力でもタイムスタンプ表示するのですが、同じ目的です。いつ何が出力されたか分かるのは大事です。

VSColorOutput64

VSColorOutput64 - Visual Studio Marketplace

出力ウィンドウのBuild出力で、色分け表示してくれます。今のdotnet buildは色表示してくれるので、Visual Studioのビルドはむしろ情報減って劣化してます。そろそろ拡張なしでもちゃんと表示してほしいです。

Buildの色分け

GitHub Copilot

Visual Studio拡張ではなく標準機能に入ったGitHub Copilotですが、どの拡張よりも重要なポジションと言えます。GitHubログインすることで、個人のCopilot ProやOrganizationのCopilot for Businessが適用されます。

GitHub Copilot Chatのウィンドウでチャットコンテキストを表示して、エージェントモードで動作させることができます。ユニットテストを書かせてもいいですし、コードのより良い書き方を相談しています。

GitHub Copilot Chat

ウィンドウ配置ですが、私は左にソリューションエクスプローラー、右にGitHub Copilot Chatを表示しています。VS Codeも併用するので、Visual StudioのレイアウトはVS Codeと似たものが好ましいです。

alt text

使わなくなった拡張

Open On GitHub

Open on GitHub - Visual Studio Marketplace

標準でコンテキストメニュー > Gitからパーマリンクをとれるようになったので、外しました。直接ブラウザで開くのが差別ポイントなので、もしブラウザで開きたいなら今も便利です。

標準のGitメニュー

Parallel Builds Monitor

Parallel Builds Monitor - Visual Studio Marketplace

外したくなかったんですが、Visual Studioの起動時のプロファイラーがしょっちゅう警告を出してくるので外しました。起動が遅くなった自覚あったので、しょうがない。

SwitchStartupProject for VS 2022

SwitchStartupProject for VS 2022 - Visual Studio Marketplace

Visual Studio 2022の標準機能でマルチスタートアッププロジェクトができるようになったので、外しました。デフォルトのStartup Projectの変更はできないので、そこだけは拡張のほうが便利です。

Visual Studio標準のマルチスタートアッププロジェクト

プレビューオプションでEnable Multi-Project Launch Profileを有効にしましょう。

Visual Studioプレビューオプション

まとめ

Visual Studioの標準機能も少しずつ強化されてきているので、拡張が必要なものは減ってきました。何よりCopilotが強力すぎて、一個一個の拡張は地味に便利なものを使っています。

参考