Dependabot CLI がローカルディレクトリをサポートした

Dependabot のテストやデバッグに使用する dependabot/cli は、これまで GitHub 等のレポジトリを指定して、依存関係の更新ジョブを出力することしかできなかったが、新たにローカルディレクトリの指定が可能になった。

github.com

毎回 GitHub レポジトリにプッシュする必要がなくなり、テストが容易になる。

現時点で Usage に反映されていないが、以下のとおり --local とパスを指定する。レポジトリ名がないとエラーになるが、適当な文字列で問題なかった。

$ dependabot update terraform dummy --local . -o job.yaml

これを利用して、Dependabot が、特定の条件で Terraform モジュール内のバージョン制約を更新しない挙動を、いくつか確認した。Dependabot は、与えられたディレクトリを起点に *.tf ファイルの内容を読み取って、参照されているモジュールのみをたどるアプローチを採用している。

ひとつは JSON Configuration Syntax で記述された *.tf.json ファイルから参照されたモジュールが更新されない問題で、Add Terraform JSON support by melendezd · Pull Request #5293 · dependabot/dependabot-core · GitHub で解決すると思われる。

もうひとつは、モジュールの参照が main.tf -> a/main.tf -> a/b/main.tf -> a/b/c/main.tf のように 3 回以上になる場合、更新されない問題があった。

どちらも、ワークアラウンドとしては、更新されないモジュールのディレクトリを dependabot.yml ファイルに追加すればよい。

version: 2

updates:
  - package-ecosystem: terraform
    directory: /
    schedule:
      interval: monthly
  - package-ecosystem: terraform
    directory: /a/b/c
    schedule:
      interval: monthly

ここに terraform graph があるじゃろ?これをこうして…こうじゃ。

github.blog

GitHub のマークダウンファイルにおいて、Mermaid.js が使えるようになったので、練習のために terraform graph の出力を、わかりやすく Mermaid 構文に変換して、README.md ファイル等に埋め込む GitHub Action を作成しました。

サンプルとして、以下のような Terraform AWS provider の使用例 の出力で説明します。

[root] aws_elb.web (expand) aws_elb.web [root] aws_instance.web (expand) aws_instance.web [root] aws_elb.web (expand)->[root] aws_instance.web (expand) [root] aws_security_group.elb (expand) aws_security_group.elb [root] aws_elb.web (expand)->[root] aws_security_group.elb (expand) [root] aws_key_pair.auth (expand) aws_key_pair.auth [root] aws_instance.web (expand)->[root] aws_key_pair.auth (expand) [root] aws_security_group.default (expand) aws_security_group.default [root] aws_instance.web (expand)->[root] aws_security_group.default (expand) [root] aws_subnet.default (expand) aws_subnet.default [root] aws_instance.web (expand)->[root] aws_subnet.default (expand) [root] var.aws_amis var.aws_amis [root] aws_instance.web (expand)->[root] var.aws_amis [root] aws_internet_gateway.default (expand) aws_internet_gateway.default [root] aws_vpc.default (expand) aws_vpc.default [root] aws_internet_gateway.default (expand)->[root] aws_vpc.default (expand) [root] provider["registry.terraform.io/hashicorp/aws"] provider["registry.terraform.io/hashicorp/aws"] [root] aws_key_pair.auth (expand)->[root] provider["registry.terraform.io/hashicorp/aws"] [root] var.key_name var.key_name [root] aws_key_pair.auth (expand)->[root] var.key_name [root] var.public_key_path var.public_key_path [root] aws_key_pair.auth (expand)->[root] var.public_key_path [root] aws_route.internet_access (expand) aws_route.internet_access [root] aws_route.internet_access (expand)->[root] aws_internet_gateway.default (expand) [root] aws_security_group.default (expand)->[root] aws_vpc.default (expand) [root] aws_security_group.elb (expand)->[root] aws_vpc.default (expand) [root] aws_subnet.default (expand)->[root] aws_vpc.default (expand) [root] aws_vpc.default (expand)->[root] provider["registry.terraform.io/hashicorp/aws"] [root] output.address output.address [root] output.address->[root] aws_elb.web (expand) [root] var.aws_region var.aws_region [root] provider["registry.terraform.io/hashicorp/aws"]->[root] var.aws_region [root] provider["registry.terraform.io/hashicorp/aws"] (close) [root] provider["registry.terraform.io/hashicorp/aws"] (close) [root] provider["registry.terraform.io/hashicorp/aws"] (close)->[root] aws_elb.web (expand) [root] provider["registry.terraform.io/hashicorp/aws"] (close)->[root] aws_route.internet_access (expand) [root] root [root] root [root] root->[root] output.address [root] root->[root] provider["registry.terraform.io/hashicorp/aws"] (close)

初手、出力された GraphViz の DOT フォーマットを、単純に Mermaid 構文に置き換えます。

flowchart LR
n0["aws_elb.web"]
n1["aws_instance.web"]
n2["aws_internet_gateway.default"]
n3["aws_key_pair.auth"]
n4["aws_route.internet_access"]
n5["aws_security_group.default"]
n6["aws_security_group.elb"]
n7["aws_subnet.default"]
n8["aws_vpc.default"]
n9(["output.address"])
na[/"provider<br/>[&quot;registry.terraform.io/hashicorp/aws&quot;]"\]
nb(["var.aws_amis"])
nc(["var.aws_region"])
nd(["var.key_name"])
ne(["var.public_key_path"])
n0-->n1
n0-->n6
n1-->n3
n1-->n5
n1-->n7
n1-->nb
n2-->n8
n3-->na
n3-->nd
n3-->ne
n4-->n2
n5-->n8
n6-->n8
n7-->n8
n8-->na
n9-->n0
na-->n0
na-->n4
na-->nc

各リソースと値を、種類ごとに subgraph にまとめ、スタイルを適用して、リソースを強調します。

%%tfmermaid
%%{init:{"theme":"default","themeVariables":{"lineColor":"#6f7682","textColor":"#6f7682"}}}%%
flowchart LR
classDef r fill:#5c4ee5,stroke:#444,color:#fff
classDef v fill:#eeedfc,stroke:#eeedfc,color:#5c4ee5
classDef ms fill:none,stroke:#dce0e6,stroke-width:2px
classDef vs fill:none,stroke:#dce0e6,stroke-width:4px,stroke-dasharray:10
classDef ps fill:none,stroke:none
classDef cs fill:#f7f8fa,stroke:#dce0e6,stroke-width:2px
subgraph "n0"["ELB Classic"]
n1["aws_elb.web"]:::r
end
class n0 cs
subgraph "n2"["EC2 (Elastic Compute Cloud)"]
n3["aws_instance.web"]:::r
n4["aws_key_pair.auth"]:::r
end
class n2 cs
subgraph "n5"["VPC (Virtual Private Cloud)"]
n6["aws_internet_gateway.default"]:::r
n7["aws_route.internet_access"]:::r
n8["aws_security_group.default"]:::r
n9["aws_security_group.elb"]:::r
na["aws_subnet.default"]:::r
nb["aws_vpc.default"]:::r
end
class n5 cs
subgraph "nc"["Output Values"]
nd(["output.address"]):::v
end
class nc vs
ne[/"provider<br/>[&quot;registry.terraform.io/hashicorp/aws&quot;]"\]
subgraph "nf"["Input Variables"]
ng(["var.aws_amis"]):::v
nh(["var.aws_region"]):::v
ni(["var.key_name"]):::v
nj(["var.public_key_path"]):::v
end
class nf vs
n1-->n3
n1-->n9
n3-->n4
n3-->n8
n3-->na
n3--->ng
n6-->nb
n4-->ne
n4--->ni
n4--->nj
n7-->n6
n8-->nb
n9-->nb
na-->nb
nb-->ne
nd--->n1
ne-->n1
ne-->n7
ne--->nh

provider は 1 個しかなく、役割は自明なので省略します。

%%tfmermaid
%%{init:{"theme":"default","themeVariables":{"lineColor":"#6f7682","textColor":"#6f7682"}}}%%
flowchart LR
classDef r fill:#5c4ee5,stroke:#444,color:#fff
classDef v fill:#eeedfc,stroke:#eeedfc,color:#5c4ee5
classDef ms fill:none,stroke:#dce0e6,stroke-width:2px
classDef vs fill:none,stroke:#dce0e6,stroke-width:4px,stroke-dasharray:10
classDef ps fill:none,stroke:none
classDef cs fill:#f7f8fa,stroke:#dce0e6,stroke-width:2px
subgraph "n0"["ELB Classic"]
n1["aws_elb.web"]:::r
end
class n0 cs
subgraph "n2"["EC2 (Elastic Compute Cloud)"]
n3["aws_instance.web"]:::r
n4["aws_key_pair.auth"]:::r
end
class n2 cs
subgraph "n5"["VPC (Virtual Private Cloud)"]
n6["aws_internet_gateway.default"]:::r
n7["aws_route.internet_access"]:::r
n8["aws_security_group.default"]:::r
n9["aws_security_group.elb"]:::r
na["aws_subnet.default"]:::r
nb["aws_vpc.default"]:::r
end
class n5 cs
subgraph "nc"["Output Values"]
nd(["output.address"]):::v
end
class nc vs
subgraph "ne"["Input Variables"]
nf(["var.aws_amis"]):::v
ng(["var.aws_region"]):::v
nh(["var.key_name"]):::v
ni(["var.public_key_path"]):::v
end
class ne vs
n1-->n3
n1-->n9
n3-->n4
n3-->n8
n3-->na
n3--->nf
n6-->nb
n4--->nh
n4--->ni
n7-->n6
n8-->nb
n9-->nb
na-->nb
nd--->n1

矢印は、依存元から依存先を指しているのですが、値の代入元から代入先を向くほうが直感的なので、逆にします。

%%tfmermaid
%%{init:{"theme":"default","themeVariables":{"lineColor":"#6f7682","textColor":"#6f7682"}}}%%
flowchart LR
classDef r fill:#5c4ee5,stroke:#444,color:#fff
classDef v fill:#eeedfc,stroke:#eeedfc,color:#5c4ee5
classDef ms fill:none,stroke:#dce0e6,stroke-width:2px
classDef vs fill:none,stroke:#dce0e6,stroke-width:4px,stroke-dasharray:10
classDef ps fill:none,stroke:none
classDef cs fill:#f7f8fa,stroke:#dce0e6,stroke-width:2px
subgraph "n0"["ELB Classic"]
n1["aws_elb.web"]:::r
end
class n0 cs
subgraph "n2"["EC2 (Elastic Compute Cloud)"]
n3["aws_instance.web"]:::r
n4["aws_key_pair.auth"]:::r
end
class n2 cs
subgraph "n5"["VPC (Virtual Private Cloud)"]
n6["aws_internet_gateway.default"]:::r
n7["aws_route.internet_access"]:::r
n8["aws_security_group.default"]:::r
n9["aws_security_group.elb"]:::r
na["aws_subnet.default"]:::r
nb["aws_vpc.default"]:::r
end
class n5 cs
subgraph "nc"["Output Values"]
nd(["output.address"]):::v
end
class nc vs
subgraph "ne"["Input Variables"]
nf(["var.aws_amis"]):::v
ng(["var.aws_region"]):::v
nh(["var.key_name"]):::v
ni(["var.public_key_path"]):::v
end
class ne vs
n3-->n1
n9-->n1
n4-->n3
n8-->n3
na-->n3
nf--->n3
nb-->n6
nh--->n4
ni--->n4
n6-->n7
nb-->n8
nb-->n9
nb-->na
n1--->nd

左から、リソースが作成されていく流れが俯瞰しやすくなったように思います。Terraform 設定ファイルを含む Github リポジトリにおいて、次のとおりに Github Action を使用することで、このようなグラフが自動的に README.md ファイルに埋め込まれます。

github.com

  • 下記の内容の .github/workflows/tfmermaid.yml ファイルを作成する
name: tfmermaid
on:
  push:
jobs:
  generate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: asannou/tfmermaid-action@v1
        with:
          file: README.md
      - name: commit
        run: |
          git add README.md
          if ! git diff --cached --quiet --exit-code
          then
            git config user.name "github-actions[bot]"
            git config user.email "github-actions[bot]@users.noreply.github.com"
            git commit -m "generated"
            git push
          fi
  • README.md ファイルの埋め込む場所に %%tfmermaid とコメントされた mermaid コードブロックを配置する
 ```mermaid
 %%tfmermaid
 ```

状況に合わせて、provider を表示したり、var 等を省略したり、矢印を順方向にしたり、グラフ全体の向き を変更する等のオプションも用意しています。

      - uses: asannou/tfmermaid-action@v1
        with:
          file: README.md
          include: provider
          exclude: var,local,output,data
          arrow-direction: forward
          orientation: RL

その他の例は https://github.com/asannou/tfmermaid-action/blob/v1/README.md#examples で確認してください。

Docker のベースイメージを GitHub で自動的に更新する


かつて Docker Hub にベースイメージが更新されたタイミングで自動的にビルドする機能があったと記憶しているのですが、Autobuilds が free plan で使えなくなったため、GitHub Actions と Dependabot を利用して更新する方法を検討しました。

テスト環境として nginx:stable をベースイメージとする Dockerfileビルドして Docker Hub にプッシュするワークフローを用意します。そのまま、このワークフローの定期実行をスケジュールすることで、最新のベースイメージが反映されたイメージを維持できますが、 更新がない時の不要なビルドによるコストと、下記の自動的な無効化を回避するために、Dependabot を採用します。

パブリックリポジトリでは、60日間にリポジトリにアクティビティがなかった場合、スケジュールされたワークフローは自動的に無効化されます。

使用制限、支払い、管理 - GitHub Docs

最初に、Dockerfile を監視する dependabot.yml を用意します。nginx:stable は、現在の stable バージョンである nginx:1.20.2 と同じイメージを示しており、バージョンアップに合わせて、新しいイメージに変更されていきます。nginx 自体はバージョン番号が同じであれば内容も同一ですが、Docker イメージの場合、ベースイメージの debian や、パッケージ等の nginx 以外の更新によっても、イメージが更新される点に注意してください。Dependabot には、その時点で指しているイメージを教える必要があるため、次のようにダイジェスト値を追加します。

github.com

ダイジェスト値の取得は CLI です。

$ docker inspect --format='{{index .RepoDigests 0}}' nginx:stable
nginx@sha256:cba27ee29d62dfd6034994162e71c399b08a84b50ab25783eabce64b1907f774

また、nginx:1.20 のようにバージョンを指定する場合も同様にダイジェスト値を追加できます。

以上で、Dependabot がベースイメージのバージョンアップを検知した際に、自動的にプルリクエストを作成するようになります。

github.com

しかしながら、この状態だとプルリクエストができる度に人間がマージをする必要があります。

仮に、ベースイメージとして nginx:1.20 を指定した場合、1.20.x の最新のバージョンが使用されますが、nginx はセマンティックバージョニングに従っていると思われるので、後方互換性を伴うバグ修正によるパッチバージョンの変更は、自動的にマージすることにします。

自動マージのワークフロー例を参考にして、以下のファイルを作成します。steps.dependabot-metadata.outputs.update-type は定義されないようなので、条件から外しています。

github.com

結果、Dependabot によるプルリクエストと同時に、ワークフローが実行され、自動マージされました。

github.com

検証が不十分なのですが、自動マージの場合、ビルドをするワークフローが push や pull_request イベントではトリガーされなかったため、workflow_run イベントを追加しています。

ベースイメージの自動的な更新は完了しましたが、Dockerfile で apt や yum 等の Dependabot で監視できないパッケージをインストールしている場合は、それらのバージョンアップに気づくことができません。

github.com

そのようなイメージを確実に更新し続けるためには、前述のとおりビルドのワークフローを定期実行し、何らかの方法でスケジュールの無効化を回避する必要があると考えます。