kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Digger で複数の AWS アカウントに terraform apply を実行する

Digger で複数の AWS アカウント(たとえば stg 環境と prd 環境)にデプロイする仕組みを作ってみたので簡単にまとめておこうと思う.基本的にはドキュメントに載っている通りに設定すれば OK だった👌

docs.digger.dev

動作イメージ

まず,適当に Terraform コードを変更してプルリクエストを出す.すると自動的に stg 環境と prd 環境に対して terraform plan が実行される💡

そして -p オプションでプロジェクトを指定して digger apply -p stg コマンドを実行すると,stg 環境に対して terraform apply が実行される❗️

同じように digger apply -p prd コマンドを実行すると,prd 環境に対して terraform apply が実行される❗️

問題がないことを確認できたらプルリクエストをマージすれば OK👌

ちなみに -p オプションでプロジェクトを指定しないと,同時に stg 環境と prd 環境に対して terraform apply が実行された.正直同時にデプロイする機会は少なそうだし,むしろ誤って実行しないように抑止できると良さそうだけど...😅

コード紹介

ディレクトリ構成

├── .github
│   └── workflows
│       ├── digger-run-prd.yml
│       └── digger-run-stg.yml
├── digger.yml
├── prd
│   ├── provider.tf
│   └── main.tf
└── stg
    ├── provider.tf
    └── main.tf

👾 digger.yml

プロジェクトとして stg 環境と prd 環境を作って,ディレクトリ名と GitHub Actions ワークフローファイル名を設定した👌

projects:
  - name: stg
    dir: stg
    workflow_file: digger-run-stg.yml
  - name: prd
    dir: prd
    workflow_file: digger-run-prd.yml

👾 .github/workflows/digger-run-stg.yml

基本的にドキュメントに載っているワークフロー設定を参考にしてるけど,AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY は設定したくなく,aws-role-to-assume で OIDC (OpenID Connect) で IAM Role を引き受けられるようにしている👌

docs.digger.dev

name: Digger Workflow (stg)

on:
  workflow_dispatch:
    inputs:
      spec:
        required: true
      run_name:
        required: false

run-name: '${{inputs.run_name}}'

jobs:
  digger-job:
    runs-on: ubuntu-latest
    environment: stg
    permissions:
      contents: write      # required to merge PRs
      actions: write       # required for plan persistence
      id-token: write      # required for workload-identity-federation
      pull-requests: write # required to post PR comments
      issues: read         # required to check if PR number is an issue or not
      statuses: write      # required to validate combined PR status

    steps:
      - uses: actions/checkout@v4
      - name: ${{ fromJSON(github.event.inputs.spec).job_id }}
        run: echo "job id ${{ fromJSON(github.event.inputs.spec).job_id }}"
      - uses: diggerhq/digger@v0.6.91
        with:
          digger-spec: ${{ inputs.spec }}
          setup-terraform: true
          terraform-version: v1.11.0
          setup-aws: true
          aws-role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          aws-region: ap-northeast-1
          cache-dependencies: true
        env:
          GITHUB_CONTEXT: ${{ toJson(github) }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

👾 .github/workflows/digger-run-prd.yml

namejobs.digger-job.environment 以外は同じ.

name: Digger Workflow (prd)

on:
  workflow_dispatch:
    inputs:
      spec:
        required: true
      run_name:
        required: false

run-name: '${{inputs.run_name}}'

jobs:
  digger-job:
    runs-on: ubuntu-latest
    environment: prd
    permissions:
      contents: write      # required to merge PRs
      actions: write       # required for plan persistence
      id-token: write      # required for workload-identity-federation
      pull-requests: write # required to post PR comments
      issues: read         # required to check if PR number is an issue or not
      statuses: write      # required to validate combined PR status

    steps:
      - uses: actions/checkout@v4
      - name: ${{ fromJSON(github.event.inputs.spec).job_id }}
        run: echo "job id ${{ fromJSON(github.event.inputs.spec).job_id }}"
      - uses: diggerhq/digger@v0.6.91
        with:
          digger-spec: ${{ inputs.spec }}
          setup-terraform: true
          terraform-version: v1.11.0
          setup-aws: true
          aws-role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          aws-region: ap-northeast-1
          cache-dependencies: true
        env:
          GITHUB_CONTEXT: ${{ toJson(github) }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

GitHub Environments

あとは GitHub Environments に stgprd を作って,シークレット AWS_ROLE_TO_ASSUME に IAM Role の ARN を設定すれば OK👌

まとめ

Digger を使えば比較的簡単に Terraform のブランチデプロイと IssueOps を実現できてイイ感じ \( 'ω')/

github.blog

関連記事

kakakakakku.hatenablog.com

kakakakakku.hatenablog.com

Bedrock のマルチエージェントコラボレーション機能に入門できる「わが家の AI 技術顧問」ワークショップを試した

builders.flash に公開されていた「わが家の AI 技術顧問」ワークショップを試してみた❗️

みのるんさんの寄稿記事で,Amazon Bedrock の「マルチエージェントコラボレーション機能」に入門できる.まだマルチエージェントコラボレーション機能を試せてなかったからとても勉強になったし(設定・プロンプトなど),Amazon Bedrock の入門コンテンツとしても良いと思う👌素晴らしいコンテンツだった.

aws.amazon.com

ちなみに Amazon Bedrock のマルチエージェントコラボレーション機能は2024年12月にリリース(プレビュー)されて,今月2025年3月に正式リリースになっている.試すなら今だ \( 'ω')/

aws.amazon.com

aws.amazon.com

構築したアプリケーション🤖

Streamlit で実装されたチャット画面に技術的な質問をすると回答してくれる「AI 技術顧問」を構築した👌内部的には Amazon Bedrock の専門知識を持ったサブエージェントと Tavily 経由でウェブ検索をするサブエージェントがいて,スーパーバイザーエージェントが質問内容によってどっちのサブエージェントを使うべきか判定してくれる感じ❗️

全体感は builders.flash 記事に載っているアーキテクチャ図を見るとイメージしやすいと思う.Amazon Bedrock の専門知識を持ったサブエージェント bedrock-master は Amazon Bedrock の「ナレッジベース」を使っていて,ベクターストアとしては Aurora Serverless v2 (PostgreSQL) を使う.そして,ナレッジベースのデータソースとしては Amazon Bedrock - User Guide (PDF) を使う📝

[質問] Amazon Bedrock の基盤モデルって何?

Amazon Bedrock の基盤モデルって何? って質問したら Amazon Bedrock の専門知識を持ったサブエージェント bedrock-master が呼び出された.最終的にスーパーバイザーエージェントから回答が返ってくる.各ステップの詳細も確認できて,マルチエージェントの流れをイメージしやすく作られていた.

[質問] 現在サポートされている Python の Lambda ランタイムは何?

現在サポートされている Python の Lambda ランタイムは何? って質問したら Tavily 経由でウェブ検索をするサブエージェント web-search-master が呼び出された.

作業メモ🤖

1. Python 3.12

手順では Tavily に接続する AWS Lambda 関数のランタイムと AWS Lambda レイヤーを Python 3.9 に変更していた.AWS CloudShell に Python 3.9 がプリセットされていて(古いよなぁ),環境構築がしやすいように工夫されていると思うけど,個人的には Amazon Bedrock のアクショングループから自動作成された Python 3.12 のまま実施したいな〜と思って,macOS で Python 3.12 の環境からデプロイした.

$ python --version
Python 3.12.4

$ mkdir python
$ pip install tavily-python --target python
$ zip -r layer.zip python

$ aws lambda publish-layer-version \
  --layer-name Tavily \
  --zip-file fileb://layer.zip \
  --compatible-architectures x86_64 \
  --compatible-runtimes python3.12 \
  --region us-east-1

2. dotenv

Streamlit を起動するときに以下のエラーが出た.

ModuleNotFoundError: No module named 'dotenv'

builders.flash 記事では pip install boto3 streamlit と書いてあったけど,追加で dotenv もインストールする必要があった.

$ pip install dotenv 

3. エラーメッセージ

builders.flash 記事に載っている Streamlit のコードと GitHub に公開されているコードでエラーメッセージに差分があった.動作にはまったく影響なし👌

github.com

 def show_error_popup(exeption):
     """エラーポップアップを表示する"""
     if exeption == "dependencyFailedException":
-        error_message = "【エラー】ナレッジベースのAurora DBがスリープしていたようです。しばらく待ってから、ブラウザをリロードして再度お試しください🙏"
+        error_message = "【エラー】ナレッジベースのAurora DBがスリープしていたようです。数秒おいてから、ブラウザをリロードして再度お試しください🙏"
     elif exeption == "throttlingException":
-        error_message = "【エラー】Bedrockのモデル負荷が高いようです。1分後にブラウザをリロードして再度お試しください🙏(改善しない場
合は、モデルを変更するか[サービスクォータの引き上げ申請](https://aws.amazon.com/jp/blogs/news/generative-ai-amazon-bedrock-handling-quota-problems/)を実施ください)"
+        error_message = "【エラー】Bedrockのモデル負荷が高いようです。1分待ってから、ブラウザをリロードして再度お試しください🙏(改善
しない場合は、モデルを変更するか[サービスクォータの引き上げ申請](https://aws.amazon.com/jp/blogs/news/generative-ai-amazon-bedrock-handling-quota-problems/)を実施ください)"
     st.error(error_message)

4. スロットリング

ワークショップではスロットリングが発生しにくくなるようにエージェントごとにモデルを変える工夫がされていて素晴らしかった👌

  • bedrock-master: Claude 3.5 Haiku
  • web-search-master: Claude 3.5 Sonnet v2
  • your-tech-advisor: Claude 3.5 Sonnet v1

しかし普段 Amazon Bedrock を使っていない AWS アカウントだとデフォルトのクォータ値が低く,エラーが出ることがあった.よって,時間があれば Service Quotas で On-demand InvokeModel requests per minute for Anthropic Claude xxx の緩和申請をしてからワークショップに取り組むと良さそう.今回はクォータ値が多少大きかった Claude 3 Sonnet(レガシー) に変更した.

まとめ🤖

Amazon Bedrock のマルチエージェントコラボレーション機能に入門できる素晴らしい記事だった👏

時間的にはスロットリングの試行錯誤も含めて1時間半で完走できた🏃‍♂️おすすめです〜

Dependabot の uv サポートを試す

Python プロジェクトで uv を使っていて,Dependabot で依存関係を自動アップデートできないという悩みがあった.ちなみに Renovate はもともと uv をサポートしているという背景があった👌現時点では uv のドキュメントには uv is supported by Renovate. / Support for uv is not yet available. と書いてある.

docs.astral.sh

Dependabot の uv サポート

2025年3月13日に Dependabot の uv サポートが発表された🎉

github.blog

もともと Dependabot で uv をサポートするための issue があって,定期的に進捗確認をしていたから「ついに!」という感じ.

github.com

さっそく試す

package-ecosystemuv を指定すれば OK👌

👾 .github/dependabot.yml

version: 2

updates:
  - package-ecosystem: uv
    directory: /
    schedule:
      interval: daily
    open-pull-requests-limit: 1
    target-branch: main

docs.github.com

👾 pyproject.toml

検証用の pyproject.tomlboto3 の依存関係を設定した🐬

[project]
name = "sandbox-dependabot-uv"
version = "0.1.0"
description = "sandbox-dependabot-uv"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
    "boto3==1.37.16",
]

プルリクエスト

すると期待通りにプルリクエストが作られた👏

ちゃんと pyproject.tomluv.lock がアップデートされていてイイ感じ〜 \( 'ω')/

関連記事

kakakakakku.hatenablog.com

kakakakakku.hatenablog.com

JSON Schema の dependentRequired で「a を指定する場合は b もセットで必要」を実現する

JSON Schema で dependentRequired を使うと JSON のプロパティ構造を「条件付きで」バリデーションできる🔐 具体例を挙げると,任意プロパティが ab 2つあるときにa を指定する場合は b もセットで必要」というバリデーションをしたいときに使える❗️

json-schema.org

JSON Schema のドキュメント (Conditional schema validation) に載っている例だと,name プロパティは必須で,もし任意の credit_card プロパティを指定する場合は billing_address プロパティもセットで必要という感じ👌

{
  "type": "object",

  "properties": {
    "name": { "type": "string" },
    "credit_card": { "type": "number" },
    "billing_address": { "type": "string" }
  },

  "required": ["name"],

  "dependentRequired": {
    "credit_card": ["billing_address"]
  }
}

動作確認

pytest でテストコードを実装して dependentRequired の動作確認をしてみた❗️ちなみに JSON Schema のバージョンによって記法が違っていて,Draft 2020-12(正確には Draft 2019-09 以降)では dependentRequired で,Draft-07 だと dependencies となる.詳しくは Draft 2019-09 のリリースノートに載っている📝

json-schema.org

テスト項目としては以下の3種類の JSON を JSON Schema Draft 2020-12 と JSON Schema Draft-07 でバリデーションしてみた👌

  • INPUT_1(name)🙆‍♂️
  • INPUT_2(name and credit_card)🙅
  • INPUT_3(name and credit_card and billing_address)🙆‍♂️
import jsonschema
import pytest
from faker import Faker

fake = Faker()


SCHEMA_DRAFT_2020_12 = {
    '$schema': 'https://json-schema.org/draft/2020-12/schema',
    'type': 'object',
    'properties': {
        'name': {'type': 'string'},
        'credit_card': {'type': 'number'},
        'billing_address': {'type': 'string'},
    },
    'required': ['name'],
    'dependentRequired': {'credit_card': ['billing_address']},
}


SCHEMA_DRAFT_07 = {
    '$schema': 'https://json-schema.org/draft-07/schema',
    'type': 'object',
    'properties': {
        'name': {'type': 'string'},
        'credit_card': {'type': 'number'},
        'billing_address': {'type': 'string'},
    },
    'required': ['name'],
    'dependencies': {'credit_card': ['billing_address']},
}


INPUT_1 = {
    'name': fake.name(),
}

INPUT_2 = {
    'name': fake.name(),
    'credit_card': int(fake.credit_card_number()),
}

INPUT_3 = {
    'name': fake.name(),
    'credit_card': int(fake.credit_card_number()),
    'billing_address': fake.address(),
}


def idfn(param):
    return param['id']


@pytest.mark.parametrize(
    'patterns',
    [
        {
            'id': 'Draft 2020-12|1',
            'schema': SCHEMA_DRAFT_2020_12,
            'validator': jsonschema.Draft202012Validator,
            'input': INPUT_1,
            'valid': True,
        },
        {
            'id': 'Draft 2020-12|2',
            'schema': SCHEMA_DRAFT_2020_12,
            'validator': jsonschema.Draft202012Validator,
            'input': INPUT_2,
            'valid': False,
        },
        {
            'id': 'Draft 2020-12|3',
            'schema': SCHEMA_DRAFT_2020_12,
            'validator': jsonschema.Draft202012Validator,
            'input': INPUT_3,
            'valid': True,
        },
        {
            'id': 'Draft-07|1',
            'schema': SCHEMA_DRAFT_07,
            'validator': jsonschema.Draft7Validator,
            'input': INPUT_1,
            'valid': True,
        },
        {
            'id': 'Draft-07|2',
            'schema': SCHEMA_DRAFT_07,
            'validator': jsonschema.Draft7Validator,
            'input': INPUT_2,
            'valid': False,
        },
        {
            'id': 'Draft-07|3',
            'schema': SCHEMA_DRAFT_07,
            'validator': jsonschema.Draft7Validator,
            'input': INPUT_3,
            'valid': True,
        },
    ],
    ids=idfn,
)
def test_json_schema(patterns):
    if patterns['valid']:
        patterns['validator'](patterns['schema']).validate(patterns['input'])
    else:
        with pytest.raises(jsonschema.ValidationError):
            patterns['validator'](patterns['schema']).validate(patterns['input'])

実行すると期待通りになっていた👌

$ uv run pytest --verbose
(中略)
test_main.py::test_json_schema[Draft 2020-12|1] PASSED
test_main.py::test_json_schema[Draft 2020-12|2] PASSED
test_main.py::test_json_schema[Draft 2020-12|3] PASSED
test_main.py::test_json_schema[Draft-07|1] PASSED
test_main.py::test_json_schema[Draft-07|2] PASSED
test_main.py::test_json_schema[Draft-07|3] PASSED

まとめ

JSON Schema の dependentRequired は便利そうだから覚えておこう〜 \( 'ω')/

Digger で terraform apply 後に自動的にプルリクエストをマージする

Diggerdigger.ymlauto_merge: true を設定すると,terraform apply 後に自動的にプルリクエストがマージできる.プルリクエストのマージを忘れることなく Apply-Before-Merge 戦略を実現できてイイ❗️

docs.digger.dev

👾 digger.yml

検証用プロジェクトを使ってプルリクエストの自動マージを試す👌

まず,digger.ymlauto_merge: true を設定しておく.

projects:
  - name: sandbox
    dir: .
auto_merge: true

次に適当なプルリクエストを作って,terraform plan 結果を確認したら digger apply コマンドを実行する.すると terraform apply 後に digger-cloud bot によって自動的にプルリクエストがマージされた❗️

関連ドキュメント

digger apply 以外のコマンドは以下のドキュメントに載っている📝

docs.digger.dev