頭の中は異空間

ものづくり中心

AIコーディングエージェントを使う時のMDファイルのフォーマットをある程度固定してみたい

以下のような構造でMDファイルを用意する。
※中身は基本的にClaudeかチャッピーあたりに書いてもらう。気に入らなければ一部手直し。

フォルダ、ファイル構成


/
.agents/
rules/
architecture.md
coding.md
design.md
testing.md
specs/
acceptance.md
api.md
spec.md
ui.md
AGENTS.md

rules/

architecture.md

どんなアーキテクチャ、技術スタックの組み合わせにするかを記述する

coding.md

コーディング規約を記述する。例えばスネークケースとかケバブケースなどを指定。

design.md

テーブルデザイン、ボタンデザインなどフロントエンドの具体的なデザインを記述する。後はレスポンシブデザインにしようとか、画面サイズをどうしようとか。

testing.md

テスト方法について記述する。これにはpytestなどの単体テストやE2Eテストも含む

specs/

acceptance.md

成果物の受け入れ条件を記述する

api.md

バックエンドのAPI仕様を記述する

spec.md

具体的な仕様やスコープを記述する

ui.md

画面構成を記述する

AGENTS.md

AI自体の振る舞いを記述する。

あれこれ

このファイル群をテンプレートとして使えば色んなアプリ開発に対応できる気がする。場合によってはもっと細分化した方が良いとか、もっと種類を削れるよとかあるかもしれない。そのあたりのノウハウがまとまってきたらまたなんか書きます。

この構成でなんか作ってみた感想

Antigravityで試してみたが、大凡作ってもらいたい通りに作ってくれた。MDファイルを個別にチャッピーに頼んで作らせると、一部記述が冗長になっている部分があったり、かなり細かく書こうとするのでMDファイルの行数が膨らみがち。もっとコンパクトに書いて同じ品質を維持できるようになりたい。

AIを使ったアプリなどの個人開発の方針について自分なりの考えを述べる

大まかな方針

  • AntigravityとClaude Codeを使って開発する

※全部Claude Codeでも良いが金がかかりすぎるので、小規模だったり急ぎじゃないものはAntigravity(無料枠)、大規模でとにかく早く作りたいものはClaude Codeで作る

  • 仕様やガードレールなど必要事項を全てMDファイルに記載することに全力を注ぎ、実際の開発には一切手を出さない(というか人間にとっての開発がMDファイルの実装ということになる→そのMDファイルも大部分はチャッピーとかAIにやらせる)
  • ペライチ程度の簡単なHTML、CSSくらいならMDファイルすら要らない。チャッピーに頼む
  • チャッピー任せのとき以外はVibe Codingはしない
  • やたらめったら多種のAIを使わない。どれで何聞いたか後でワケワカランくなるし実際全て綺麗に使い分けすることを常に考えてたら確実にハゲる。大抵の質問はチャッピーかPerplexityかClaudeで済むでしょ

これのいいところ

  • コスパが良い

ぶっちゃけこれに尽きます。最初から金かけまくってマルチエージェントで複数プロジェクト同時開発が出来れば圧倒的に早いのはわかっているけど、自分のようなキングボンビーには湯水のように使える金はないんですよ

良くないところ

①金ねンだわ
②Antigravity任せ(無料枠のみ)は時間がかかる。あと承認待ち多すぎ問題
トークンを食い潰すのが早いので、マルチエージェントなんか当然使えたもんじゃないし、シングルエージェントだったとしてもそれだけで作りきれる事はありえません。で、使い切った後は1週間待たされます。うーん辛い
まあタダで使えるだけ大いに価値はあるんですが。
③追加機能入ったときとかMDファイル直すの大変
最初だけMDファイル頼みで後から修正分はVibe Codingでやってみたんですが、ドキュメントの整合性が合わなくなる問題がありますし、あとから見たらMDファイルと成果物の整合性が合ってない問題もある。全てキレイに作るなら逐一MDファイルを常に最新に合わせておく必要がある(けどこれってコーディングエージェント全部に言えることです)
この辺雑にやるとMDベースの実装の品質が安定しなくなる&開発に再現性がなくなるので、一番重視すべきポイント。

改善方法

①年間使い続けるのではなく、実装に必要な期間にのみ課金する
Claude Code契約期間は短く、ただし確実にその期間内に作る→事前準備に全ツッパ
③機能追加などの追加修正ごとにMDファイルの修正もコーディングエージェントにやらせる
その分トークン消費が激しくなるが、整合性がガタガタになるよりはマシ

余談

AIだなんだって言ったって結局金なんだなあ。企業でならすんなり経費で契約してもらえるかもしれないが、個人だとなかなか手が出せない懐事情だったりする人が割と多い気がしています。今後はこういうのを躊躇いなく契約できる人とそうでない人で格差が大きく開いていくんじゃないかな。流石にVibe Codingだけなんてかなり無理がある。開発効率が劇的に向上するわけでもないし。
というか、AI周りはちょっと目を話した隙に開発手法が一気に変わり杉内。ベストプラクティスを追い続けるのは大変だし、最新のモデルを使うとなるとそれが最善とも限らないというわけで、そういうのを常に試したい人に人柱になってもらって、自分たちは評判の良いものを少し後追いで使う、が現実的な落としどころなんじゃないかな。

【シケタイ】【AWS】SAA試験に向けての覚書

ポイントはWell-Architected Framworkを理解しているかどうか。
オンプレのシステムをよりコスパよく、よりセキュアで、より高可用でスケーラブルなシステムにするためのフレームワークだと言っています。

使った教材

  1. Ping-t
  2. 【AWS資格】無料WEB問題集&徹底解説 | ソリューションアーキテクト(SAA)

ただで使えて他に推奨している人がいるならやらない手はないよねって話。
※Ping-tは一部無料で、試験部分は大半有料

SAA(というかWell-Architected Framwork)の考え方

まずは公式資料(フレームワークの柱 - AWS Well-Architected フレームワーク)を見るのが良い

AWSにはとにかくアホみたいに大量のサービスが出てくるが、多すぎて全部覚えるのはキビシイ
でも良く出てくるキーワードは理解必須

  • フルマネージドサービス
  • リージョン
  • AZ(Availability Zone)
  • サーバレス
フルマネージドサービス

Amazon側がサービスが動作しているサーバ周りをすべて管理しているサービスという意味。要するにユーザはOSアップデートやらディスク故障時の入れ替え作業やらの面倒なメンテナンス作業を一切やらなくていい(なんかあっても責任の所在がAmazon側にある)ことが特徴
AWS(に限らずクラウド全般そうだけど)のコンセプトでは、このサービスを使って欲しいということ
→ところが料金やコンプラなど自社ルール的にフルマネージドを使えない場合に、それ用のサービスもあったりなかったりする

リージョン

DCのある場所。「レイテンシ」の問題を解決する。更にDR要件も満たせる
自社サービスをグローバル展開するなら、どの国どの地域からアクセスされても高速なレスポンスを返したい。そんな要望に応えられる
※物理的な距離が遠いサーバとの通信にはより時間がかかることを知っているとわかりやすい

AZ(Availability Zone)

リージョンを切り分けたもの。シングル、マルチの2種類あるが、余程の理由がない限りは可用性向上のためにマルチAZにするべし
「DR(Disaster Recovery)」に有効

サーバレス

サーバ管理というインフラの作業の一切を省いてスクリプトやアプリ開発に集中できる仕組み
※実際にサーバがないという意味ではない。当然ながらLambdaやAPI GateWayなどを構成するコードが動作しているサーバ自体は物理的に存在するが、Amazon側の責任でそれを管理するため、ユーザがそれを一切意識する必要がない。よって自分で管理するサーバがない、という意味で使われる
※実際にはどのホストからAWSのどのサービスを呼び出していて、頻度や用途がどうなのか、ドキュメントとしてまとめる手間は出てくるが、サーバ運用の手間と比べれば微々たるもの
手間がかかるということはその分人件費が発生するので、サーバレスじゃないと結果的に割高になるよ、とも言える

コツとか?

EBSのスペック選ぶ問題

IOPSとか用途で分かれる。正直細かい数値を覚えてられんので、コスト縛りがないなら最上位のプロビジョンド、とでも覚えておく

IAMとリソースベースポリシーの分け方

IAMはAWSアカウントやIAMユーザに対しての認証・認可、リソースベースはS3とかKMSへのアクセス管理が対象
→シークレットキーとかアクセスキーを作るやり方は漏洩リスクがあるから推奨されない。

AWSアカウントとIAMユーザの分け方

AWSアカウントはAWSを始めるための所謂rootユーザ、IAMユーザは作業用ユーザ
1つのAWSアカウントが複数のIAMユーザを管理するイメージ

IAMグループとOrganizationsの分け方
  • IAMグループはIAMユーザを纏めるためのもの
  • OrganizationsはAWSアカウントを纏めるためのもの
問題を深読みしない

→問題文に書いてない詳細な構成まで考え出すとドツボにハマって時間を浪費するだけ。
当たり前だが、書いてあることから回答できる選択肢になっている。

本番までにとにかく練習問題をたくさんこなして、クソみてえな引っ掛け問題に散々引っかかっておくと、本番の勝率が上がる
練習問題の時点で殆どパーフェクトに得点できるなら日和ってないでさっさと受けちまえ

Ping-tの主要なサービスは詳細な機能とか数値まで覚えておいて、それ以外については大まかな役割だけ覚えておけばだいたいなんとかなる

  • このサービス(機能)はなんのためにあるのか?
  • 求められているものは何か?

を意識して回答する

特定のキーワードが出たときに選択肢にあれば正解の可能性が高いもの
  • マイクロサービス、疎結合、非同期処理=SQS、SNS
  • イベント駆動=EventBridge
  • リアルタイムな大量データ処理=Kinesis Data Streams, Kinesis Data Firehose, Apache Flinkあたり
  • 大量データ送信したい、処理したいが帯域が限られている、ほとんど使えない=Snowball Edge
  • インターネット経由せずにVPC外のサービスへアクセスしたい=VPCエンドポイント
  • DDoS対策したい=Shield Standard / Shield Advance
  • CloudWatch Logsを手間を掛けずに分析したい=Athena
  • IAMユーザとかの設定変更を監視したい=Config

...etc
例えばSQSならパフォーマンスの問題で一部のリクエストが処理されていないケースの改善案として選択肢に出されることがある

EC2は高価なうえ運用管理の手間あり、Lambdaは関数作成の手間がかかる&コールドスタート&最大実行時間15分という明確なデメリットが有る

最適な構成=コスパが良い、手間がかからない構成

無駄にカネがかかったり複雑怪奇になる回答をさせる頭おかしい問題はまず出ない
課金プラン的に一番金のかからない構成を具体的な金額計算させるような鬼畜問題は出ない。余計にたくさん構築の手間がかかるような回答はアウト、とかしておけば一発で正解がわからなくても明らかアウトな選択肢を省いて2択までは絞り込めたりする
例えば設問でインターネット経由させたくないって言ってるのにパブリックサブネットを使おうとしてみたり、オンプレから移行して運用負荷減らしたいって言ってるのにEC2インスタンスを立てさせたりしてるのは誤り

ていうかEC2って存在自体が一番Well-Architectedの考えに違反してると思うんですけど(名推理)

個人的な考え方

これに限らず、本番前模擬試験で合格点より余裕を持って得点できてればOK。途中段階の練習問題なんか0点でもいいです。最終的に理解できてれば合格できる。
要は本番で合格点叩き出せればいいわけで、そのための準備期間。後そのための模擬試験

その他

  • 受験前にAWS Skill Builderのアカウント作成が必須。このとき、アルファベットの入力ミスで違う名前になってると受験できない

→万一間違って登録したことが後から発覚した場合、修正依頼をサポートに投げて2~3日程待つ。試験前日に気づくようなことがあったら高確率で死亡(1敗)

  • テストセンター受験の場合、場所にも寄るが受験者数が多いのか埋まりがち。かなり早めに予定を押さえてしまったほうが後々楽。

【シケタイ】Python3 エンジニア認定実践試験用 勉強方法とか

主に自分がやったこと

勉強方法について

Python実践レシピの購入

まず書籍を購入します
これは単なる教科書でなく試験範囲を完璧にカバーしている攻略本でもあるため、本当に試験に受かりたいなら買わない選択肢はないです
試験範囲は公式サイトに明記されている
500P近くあって分厚いけど、全部が範囲じゃないので、シケタイのつもりで買うなら気楽に考えておk

手元のPCにPython環境構築

実際にコーディングして動作確認するためです。本を何度も読み返すよりも頭に残るので物凄く効果的。シケタイだけじゃなくて今後仕事で使っていくことも考えるならより一層やるべき
試験が3系なので当然3系をインストールすること

模擬試験実施

資格試験ならやらない理由はないので細かいことは言わない
以下2つをやること
(特に後者の模擬試験は出題形式が似ているので、直前の対策として使える)


ここまでをやったうえで、試験日までに本をよく読み返すと良いと思います

Q&A

Q. 攻略本とかワロス。実務でPython使ってるからわざわざ読まんでいいだろ

A. 余程腕に自信がありPythonを知り尽くしているのなら兎も角、独学で身につけたとか職場で初めて触ったとか、その程度の知識で挑んだならほぼ間違いなく爆死しますよ
なので買ったほうがいいです

Q. 試験の難易度高いの?

A. それなり。合格率は5割ちょいなので特段厳しいわけでもないが、本番試験ではかなり細かいことを聞かれるし、人によっては使わないメソッドなんかも試験範囲だから、甘く見てると爆死します

Q. 基礎試験をすっ飛ばして受けるのはあり?

A. 大いにあり。
勿論基礎から受けてもいいし、学割が効いてる間なら基礎からやったほうが間違いはないと思います。まだプログラミング自体が初めてって人は無理せず基礎の方からやるのがいい
※基礎受かってからでないと実践受けられないってことはないです

Q. 出題数が多いところを集中して学習して、それ以外はなあなあで済ませるのはアリ?

A. おすすめはしないがアリと言えばアリ
だからといって全く触らないのはNG。対策に使える時間があんまりないよって場合は仕方ないかもだが、基本全ての範囲を念入りに手元で動作確認するくらいはしたほうがいい

自分の点数


初回ですが900いかんかった
Python環境構築0%は草

【シケタイ】Python3 エンジニア認定実践試験用 実行結果例

主に分かりづらいものとか、実際に試験に出たら混乱しそうなものを掲載

文字列

>>> tes = "AA BB CC"
>>> tes.split()
['AA', 'BB', 'CC']
>>> tes.split(' ')
['AA', 'BB', 'CC']
>>> tes.split(',')
['AA BB CC']
>>> tes.split('')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: empty separator

>>> arrstr = ['a', 'b', 'c']
>>> ''.join(arrstr)
'abc'
>>> ', '.join(arrstr[1:2])
'b'
>>> teststr = 'abcde'
>>> teststr.startswith('ab', 0)
True



delはlistだろうとtupleだろうと削除可(定義ごと消える)

list

>>> arr = [0,1,2,3,4,5]
>>> arr[:]
[0, 1, 2, 3, 4, 5]
>>> arr[0:2]
[0, 1]
>>> arr[10:12]
[]
>>> arr[10:]
[]
>>> arr[:-9]
[]
>>> arr[:12]
[0, 1, 2, 3, 4, 5]
>>> arr[-1:2]
[]
>>> arr[-1:]
[5]
>>> arr[:-2]
[0, 1, 2, 3]
>>> arr[-3:-2]
[3]
>>> arr[0:5:2]
[0, 2, 4]
>>> arr[0:5:0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: slice step cannot be zero
>>> del arr[0]
>>> arr
[1, 2, 3, 4, 5]
>>> del arr[:]
>>> arr
[]
>>> del arr
>>> arr
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'arr' is not defined


tuple

>>> t = ('a', 'b', 3)
>>> t[1:2]
('b',)
>>> t[1:]
('b', 3)
>>> del t[:]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item deletion
>>> del t[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object doesn't support item deletion
>>> del t
>>> t
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 't' is not defined


dict

>>> d = {'a': 100, 'b': 200}
>>> del d['b']
>>> d
{'a': 100}
>>> d.clear()
>>> d
{}
>>> del d
>>> d
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'd' is not defined


set

>>> del s
>>> s
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 's' is not defined


itertools

>>> import itertools
>>> l = list(range(5))
>>> itertools.islice(l, -1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: Stop argument for islice() must be None or an integer: 0 <= x <= sys.maxsize.
>>> itertools.islice(l, 0)
<itertools.islice object at 0x7f045f76ec70>
>>> list(l)
[0, 1, 2, 3, 4]

>>> i = [0,1,2]
>>> i2 = 'abc'
>>> i3 = (100, 'v', 20)
>>> z = zip(i, i2, i3)
>>> z
<zip object at 0x7f045f679e40>
>>> for v in z:
...     print(v)
...
(0, 'a', 100)
(1, 'b', 'v')
(2, 'c', 20)



一旦こんな感じで、追記することがあればまた書きます

【Zabbix】クローズされた障害に該当するヒストリを連携したい

Zabbixでは最新データに登録されるヒストリのうち、どの値で障害がクローズされたのかを特定できないっぽい
({ITEM.VALUE}や{ITEM.LASTVALUE}では単純に現在の最新のヒストリの値を参照してしまうため)
※ここでは現状の最新版であるZabbix7.2を前提とする

Zabbixから障害発生をトリガーとして他の何かに通知するとき、通知メッセージにヒストリの値を連携したい場合はITEM.VALUEなどを使うことになるものの、例えば同タイミングで複数のヒストリが登録された場合、通知元のヒストリがどの値かはわからない
→Zabbixマクロのみではどうにも解決しないため、障害がクローズされたときにトリガーアクションでスクリプトを実行するようにし、スクリプトから通知先にメッセージを送る方法で解決する。

直接
Zabbix→通知先
ではなく、
Zabbix→スクリプト実行→通知先
という順番で処理が流れる

通知>メディアタイプから、通知先ごとの設定をする

MS Teamsなど通知したい先の設定をする
詳細は割愛

zabbix_serverの動作しているホスト上のどこかに、以下のようなスクリプトを配置する
#!/usr/bin/env python3

import requests
import json
from datetime import datetime

ZABBIX_URL = '<zabbix_url>/api_jsonrpc.php'
USERNAME = 'Admin'
PASSWORD = 'zabbix'


def zabbix_request_headers_without_token():
    return {'Content-Type': 'application/json-rpc'}


def zabbix_request_headers(token):
    return {'Content-Type': 'application/json-rpc', 'Authorization': f'Bearer {token}'}


def get_zabbix_auth_token():
    payload = {
        'jsonrpc': '2.0',
        'method': 'user.login',
        'params': {
            'username': USERNAME,
            'password': PASSWORD
        },
        'id': 1
    }
    response = requests.post(ZABBIX_URL, headers=zabbix_request_headers_without_token(), data=json.dumps(payload))
    return response.json()


def zabbix_api(method, params, auth):
    payload = {
        'jsonrpc': '2.0',
        'method': method,
        'params': params,
        'id': 2
    }
    response = requests.post(ZABBIX_URL, headers=zabbix_request_headers(auth), data=json.dumps(payload))
    return response.json()


auth_token = get_zabbix_auth_token()['result']

# クローズ済みの最新eventを取得
events = zabbix_api('event.get', {
    'output': 'extend',
    'objectids': [<trigger_id>],
    'object': 0,
    'value': 1,
    'sortfield': 'clock',
    'sortorder': 'DESC',
    'limit': 1
}, auth_token)


last_event = events['result'][0]

# クローズ済み最新eventのタイムスタンプがtime_till、time_fromと一致するヒストリのみ取得
history = zabbix_api('history.get', {
    'output': 'extend',
    'history': 4,
    'itemids': [<item_id>],
    'time_till': last_event['clock'],
    'time_from': last_event['clock'],
    'sortfield': 'clock',
    'sortorder': 'DESC',
    'limit': 10 # ここは必要に応じて調整
}, auth_token)


# 取得したヒストリのうち、ナノ秒単位で一致するものを特定→これがクローズされた本来のヒストリ
history_values = history['result']
history_value = [history['value'] for history in history_values if history['ns'] == last_event['ns']][0]

<ここに通知先へ通知する処理を入れる>
通知>アクション>トリガーアクションからアクションを以下の通り作成


※ここでの「test」はテスト用に自作のメディアタイプを用意したもの


ここまで設定したうえで障害が上がれば、障害が同時に複数上がっても、それぞれの障害の原因のヒストリを結びつけることができる

【Bootstrap】【HTML/CSS/JS】アコーディオンを用いたテーブルっぽい見せ方

tableタグでアコーディオン表示なんてできないので、flexboxを使います
以下がBootstrap5を用いたアコーディオン

コード

HTML
<div class="accordion">
  <div class="accordion-item">
    <!-- thead相当 -->
    <div class="d-flex flex-row justify-content-center align-items-center mb-3 accordion-childless">
      <div class="p-2 flex-fill w-20 text-center"><b>項目名1</b></div>
      <div class="p-2 flex-fill w-20 text-center"><b>項目名2</b></div>
      <div class="p-2 flex-fill w-20 text-center"><b>項目名3</b></div>
      <div class="p-2 flex-fill w-20 text-center"><b>項目名4</b></div>
      <div class="p-2 flex-fill w-20 text-center"><b>項目名5</b></div>
    </div>

    <!-- tbody相当 -->
    <div class="d-flex flex-row justify-content-center align-items-center mb-3 accordion-header" data-header-id="header_id1"> <!-- 
親要素 -->
      <div class="p-2 flex-fill w-20 text-center">項目1</div>
      <div class="p-2 flex-fill w-20 text-center">項目2</div>
      <div class="p-2 flex-fill w-20 text-center">項目3</div>
      <div class="p-2 flex-fill w-20 text-center">項目4</div>
      <div class="p-2 flex-fill w-20 text-center">項目5
        <span class="icon invisible"></span>
      </div>
    </div>
    <div class="d-flex flex-row justify-content-center align-items-center mb-3 accordion-header accordion-content" data-header-id="header_content_id1" data-content-id="header_id1"> <!-- 
子要素 -->
      <div class="p-2 flex-fill w-20 text-center">項目1</div>
      <div class="p-2 flex-fill w-20 text-center">項目2</div>
      <div class="p-2 flex-fill w-20 text-center">項目3</div>
      <div class="p-2 flex-fill w-20 text-center">項目4</div>
      <div class="p-2 flex-fill w-20 text-center">項目5
        <span class="icon invisible"></span>
      </div>
    </div>
    <div class="d-flex flex-row justify-content-center align-items-center mb-3 accordion-content accordion-child" data-content-id="header_content_id1" data-child-id="header_content_id1"> <!-- 
孫要素 -->
      <div class="p-2 flex-fill w-20 text-center">項目1</div>
      <div class="p-2 flex-fill w-20 text-center">項目2</div>
      <div class="p-2 flex-fill w-20 text-center">項目3</div>
      <div class="p-2 flex-fill w-20 text-center">項目4</div>
      <div class="p-2 flex-fill w-20 text-center">項目5</div>
    </div>
  </div>
</div>
CSS
body {
  margin: 5rem 0;
}

.w-20 {
  width: 20%;
}

.invisible {
  display: none;
}

/* ============================================================= */

/* アイコン(^)のスタイル */
.accordion-header .icon {
  margin: -.8rem 3.5rem;
  position: absolute;
  width: 0.6rem;
  height: 0.6rem;
  border-right: 2px solid #000;
  border-top: 2px solid #000;
  border-color: #777777;
  font-size: 2rem;
  transform: rotate(135deg);
  transition: .1s;
}

/* アイコン(^)をクリック時に `v` に変更 */
.accordion-header-opened  .icon {
  transform: rotate(-135deg) scaleY(-1);
  transition: .1s;
  margin: -.5rem 3.5rem;
}


/* アコーディオンの基本スタイル */
.accordion {
  margin: 20px auto;
  border: 1px solid #ccc;
  border-radius: 5px;
  overflow: hidden;
}

/* ボタンのスタイル */
.accordion-header {
  width: 100%;
  padding: 10px;
  background-color: #AAAAff;
  border: none;
  text-align: left;
  font-size: 16px;
  cursor: pointer;
  outline: none;
  transition: background 0.3s;
}

.accordion-header:hover {
  opacity: .7;
}

/* コンテンツのスタイル */
.accordion-content {
  padding: 0 15px;
  max-height: 0;
  overflow: hidden;
  background-color: #DDDDff;
  transition: max-height 0.1s ease-out, padding 0.1s ease-out;
}

.accordion-content p {
  padding: 15px 0;
}

.accordion-childless {
  padding: 10px;
}

.accordion-child {
  background-color: #EEEEff;
}
JS
document.addEventListener("DOMContentLoaded", function() {
  const headers = document.querySelectorAll(".accordion-header");
  headers.forEach(header => {
    header.addEventListener("click", function() {
      const headerId = this.getAttribute("data-header-id");
      const contents = document.querySelectorAll(`.accordion-item .accordion-content[data-content-id="${headerId}"]`);

      // 現在の要素の開閉を切り替え
      contents.forEach(content => {
        icon = content.querySelector('.icon');
        if (content.style.maxHeight) {
          header.classList.remove('accordion-header-opened');
          content.style.maxHeight = null;
          content.style.padding = "0 15px";
          content.classList.remove('mb-3');

          if (icon) {
              icon.classList.add('invisible');
          }
          content.classList.remove('accordion-header-opened');

          // 親要素が閉じたら子要素も閉じる
          const childHeaderId = content.getAttribute("data-header-id");
          const childContents = document.querySelectorAll(`.accordion-item .accordion-content[data-child-id="${childHeaderId}"]`);
          childContents.forEach(content => {
            content.style.maxHeight = null;
            content.style.padding = "0 15px";
            content.classList.remove('mb-3');
          });
        } else {
          header.classList.add('accordion-header-opened');
          content.style.maxHeight = content.scrollHeight + "px";
          content.style.padding = "20px 15px";
          content.classList.add('mb-3');

          if (icon) {
              icon.classList.remove('invisible');
          }
        }
      });
    });
  });
});

解説

  • accordion > accordion-item以下にflexboxのdivを設置し、その1つ下の階層に項目を記述
  • thead相当のヘッダ部分は子要素なしという意味でaccordion-childlessをclassに指定します。独自のclass名なので、CSSで定義しています。
  • w-20はBootstrap5のclassに存在しないので、自分で定義しています
  • アコーディオンのボタンは、
<span class="icon"></span>

で指定します。invisibleは非表示にしたいときに使います(これも自分で定義)。初期表示で閉じていて欲しい行に当たる部分にだけinvisibleをつけています

  • アコーディオンボタンをクリック時に、親要素と子要素(親クリック時に表示される)を結びつけているのはdata-header-id及びdata-content-idです。ここに同じ値を指定すれば、複数の行を同時に表示/非表示切り替え可能です(この例では2段階のアコーディオンにしているので、data-header-idとdata-content-idを同時に持つ要素がある)
  • data-content-idは、親要素が閉じたときに開いていた孫要素も同時に閉じるために設定しています。