NestJS - TECH PLAY - TECH PLAY

TECH PLAY

NestJS

イベント

該当するコンテンツが見つかりませんでした

マガジン

該当するコンテンツが見つかりませんでした

技術ブログ

本記事は 2026 年 7 月 20 日 に公開された「 Connection pooling strategies in Amazon Aurora DSQL 」を翻訳したものです。 本記事では、Aurora DSQL の接続負荷を減らし、1 秒あたり 100 接続のレート制限を超えず、再接続が一斉に集中する thundering herd を避けるための、具体的な 4 つの戦略を解説します。読み終える頃には、大規模でも安定した性能を発揮する接続プールを構成するための、本番運用に使えるチェックリストが手に入ります。 接続プーリングの戦略次第で、 Amazon Aurora DSQL アプリケーションが安定してスケールするか、負荷で破綻するかが決まります。Amazon Aurora DSQL 独自のアーキテクチャは、接続プーリングの効果をさらに大きくします。トランザクション単位の多重化と AWS Identity and Access Management (IAM) による認証により、Aurora DSQL では適切なプーリング戦略が、従来の PostgreSQL の接続管理では実現できない形で性能を高めます。 注: 本記事は、PostgreSQL の接続管理の概念と基本的な IAM 認証を理解していることを前提としています。Amazon Aurora DSQL を初めて使う場合は、これらの戦略を適用する前に、まず Getting Started ガイド から始めてください。 サーバーレス環境での接続プーリング Amazon Aurora DSQL は運用の複雑さを自動的に処理しますが、それでも接続プーリングは欠かせません。新しい接続のたびに、コストの高い TLS ハンドシェイクと認証情報の交換が発生します。プーリングは確立済みの接続を再利用し、この負荷を抑えてレイテンシーを削減します。また、トラフィックが急増すると、1 秒あたり 100 接続 (バースト時 1,000) のレート制限に達することがあり、既存の接続を再利用すればサービスの制限内に安全に収まります。さらに、Amazon Aurora DSQL には接続の最大有効期間があり、クライアントは 1 時間で切断されます。適切に構成したプールはこの制限に達する前に接続をリサイクルし、セッションを透過的に維持します。 トランザクション単位のプーリング: アーキテクチャに組み込み済み Amazon Aurora DSQL はトランザクションプーリングモデルを採用しています。接続を Query Processor に割り当てるのは、セッション全体ではなくトランザクションの実行中だけです。標準的な PostgreSQL では、接続はバックエンドのサーバープロセスと 1:1 のセッション関係を持続的に維持します。一方 Amazon Aurora DSQL はこの対応関係を切り離すため、少数の Query Processor ではるかに多くの接続を処理できます。 この設計は、接続の使用率が低いこと、つまりほとんどの接続が大半の時間アイドル状態であることを前提としています。使用率が低いほど、接続プールに必要な Query Processor は少なくて済みます。そのため、PgBouncer や pgpool-II のようなデータベース側のプロキシは使うべきではありません。トランザクション単位の接続多重化がデフォルトで組み込まれているため、これらのツールはサービスの接続アーキテクチャと重複します。 さらに、一部の PostgreSQL 機能はサポートされていません。SQL レベルの PREPARE/DEALLOCATE ステートメント (ただし拡張クエリプロトコル経由のプリペアドステートメントは正常に動作します)、WITH HOLD カーソル、セッションレベルのアドバイザリーロックです。 Amazon Aurora DSQL の 4 つの接続プーリング戦略 以下の 4 つの戦略は、それぞれ Amazon Aurora DSQL の接続管理の特定の側面に対応します。組み合わせて適用すれば、アプリケーションがサービスの制限内に収まり、負荷がかかっても安定した性能を保てます。新しいアプリケーションを構築する場合は戦略 1 から始め、既存の PostgreSQL アプリケーションを移行する場合はチェックリストとして活用してください。 戦略 1: 公式の AWS コネクタを使う 公式の Amazon Aurora DSQL コネクタ の利用をお勧めします。トークンの生成、有効期間の 80% でのキャッシュ、透過的な更新など、IAM トークンのライフサイクル全体を自動的に処理します。 表 1: プログラミング言語別の推奨接続プールライブラリと主要な設定パラメータ 言語 プーリングライブラリ 主要な設定 Java HikariCP maximumPoolSize Python psycopg ConnectionPool min_size Node.js node-postgres (pg.Pool) max Go pgxpool MaxConns Java (HikariCP) — Spring Boot、Quarkus、Micronaut のアプリケーションでは HikariCP を使います。 HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:postgresql://<cluster-endpoint>:5432/<db>?ssl=true&sslmode=verify-full&sslrootcert=<path>"); config.setMaximumPoolSize(20); config.setMaxLifetime(55 * 60 * 1000); // 55 minutes config.setIdleTimeout(10 * 60 * 1000); // 10 minutes config.setConnectionTimeout(30 * 1000); // 30 seconds config.setKeepaliveTime(5 * 60 * 1000); // 5 minutes HikariDataSource ds = new HikariDataSource(config); Python (psycopg ConnectionPool) — Django、Flask、FastAPI のアプリケーションでは psycopg ConnectionPool を使います。 from psycopg_pool import ConnectionPool pool = ConnectionPool( conninfo="host=<endpoint> dbname=<db> sslmode=verify-full", min_size=2, max_size=20, max_lifetime=55 * 60, # 55 minutes in seconds max_idle=10 * 60, # 10 minutes idle timeout reconnect_timeout=30, # 30 seconds to reconnect ) Node.js (node-postgres) — Express.js、NestJS、サーバーレスの Node.js 関数では node-postgres の Pool を使います。 const { Pool } = require("pg"); const pool = new Pool({ host: "<cluster-endpoint>", database: "<db>", ssl: { rejectUnauthorized: true }, max: 20, idleTimeoutMillis: 10 * 60 * 1000, // 10 minutes connectionTimeoutMillis: 30 * 1000, // 30 seconds }); Go (pgxpool) — Gin、Echo、AWS Lambda Go ランタイムで構築する Go アプリケーションでは pgxpool を使います。 connStr := "host=<cluster-endpoint> dbname=<db> sslmode=verify-full" config, _ := pgxpool.ParseConfig(connStr) config.MaxConns = 20 config.MinConns = 2 config.MaxConnLifetime = 55 * time.Minute // 55 minutes config.MaxConnIdleTime = 10 * time.Minute // 10 minutes idle pool, _ := pgxpool.NewWithConfig(ctx, config) AWS Lambda: プールはハンドラーの外でインスタンス化する AWS Lambda は Aurora DSQL のワークロードでよく使われるコンピューティングサービスです。Lambda の実行モデルには特有のプーリングパターンが必要です。接続プールをモジュールスコープ (ハンドラー関数の外) で作成し、同じ実行環境のウォーム呼び出し間で保持されるようにします。 Lambda が実行環境を再利用すると、モジュールスコープで作成したプールはアクティブなまま残ります。以降の呼び出しでは TLS ハンドシェイクと IAM トークンの交換を完全にスキップできます。コールドスタート時にはプールが一度だけ初期化され、その環境が存続する間は再利用されます。 同時に実行される各 Lambda 呼び出しはそれぞれ独立した実行環境で動くため、プールも個別に持ちます。多数の同時実行にわたって接続を過剰に確保しないよう、プールサイズは小さく (Lambda インスタンスあたり 1〜3 接続) 保ってください。 Lambda の主なガイドライン: プールはハンドラーの外でインスタンス化する。これが最も重要なルールです。 最大プールサイズを 1〜3 に設定する。各 Lambda インスタンスは (バッファ呼び出しを使わない限り) 一度に 1 リクエストしか処理しないため、大きなプールは無駄になります。 アイドルタイムアウトを設定しない。接続は実行環境が存続する間ずっと維持させます。 Lambda 以外のワークロードと同様に、1 時間のハード制限に達しないよう、最大有効期間を 55 分にしてジッターを加える。 コールドスタートのレイテンシーを考慮する。最初の呼び出しでは TLS と IAM 認証の負荷が発生します。以降のウォーム呼び出しではプールされた接続を再利用します。 Python (psycopg ConnectionPool) — Lambda # pool is created ONCE at module scope, reused across warm invocations from psycopg_pool import ConnectionPool import boto3 pool = ConnectionPool( conninfo="host=<cluster-endpoint> dbname=<db> sslmode=verify-full", min_size=1, max_size=2, max_lifetime=55 * 60, # 55 minutes reconnect_timeout=30, ) def handler(event, context): """Lambda handler — pool is already initialized.""" with pool.connection() as conn: result = conn.execute("SELECT * FROM orders WHERE id = %s", [event["order_id"]]) return result.fetchone() Node.js (node-postgres) — Lambda // Pool is created ONCE at module scope, reused across warm invocations const { Pool } = require("pg"); const pool = new Pool({ host: "<cluster-endpoint>", database: "<db>", ssl: { rejectUnauthorized: true }, max: 2, // small pool per Lambda instance idleTimeoutMillis: 0, // don't close idle connections connectionTimeoutMillis: 30 * 1000, // 30 seconds }); exports.handler = async (event) => { // Pool is already warm on subsequent invocations const client = await pool.connect(); try { const result = await client.query("SELECT * FROM orders WHERE id = $1", [event.orderId]); return result.rows[0]; } finally { client.release(); } }; Go (pgxpool) — Lambda package main import ( "context" "time" "github.com/aws/aws-lambda-go/lambda" "github.com/jackc/pgx/v5/pgxpool" ) // Pool created at package scope — initialized once per execution environment var pool *pgxpool.Pool func init() { connStr := "host=<cluster-endpoint> dbname=<db> sslmode=verify-full" config, _ := pgxpool.ParseConfig(connStr) config.MaxConns = 2 config.MinConns = 1 config.MaxConnLifetime = 55 * time.Minute config.MaxConnLifetimeJitter = 5 * time.Minute pool, _ = pgxpool.NewWithConfig(context.Background(), config) } func handler(ctx context.Context, event map[string]string) (string, error) { row := pool.QueryRow(ctx, "SELECT name FROM orders WHERE id = $1", event["order_id"]) var name string err := row.Scan(&name) return name, err } func main() { lambda.Start(handler) } ヒント: Lambda 関数の同時実行数が多い場合 (数百の同時実行)、各実行環境がそれぞれプールを作成します。最大プールサイズ 2 で 500 の Lambda が同時実行されると、Aurora DSQL への接続は最大 1,000 になり得ます。接続の総数を監視し、クラスターあたり 10,000 同時接続のクォータ内に収めてください。 重要: クライアント接続には sslmode=verify-full を設定し、証明書を完全に検証して経路上の攻撃 (on-path attack) を防ぎます。Aurora DSQL は verify-ca や require といった弱いモードでも接続を受け付けますが、これらはサーバーの正当性を検証しません。 安定した接続プールを維持するには、IAM トークンのライフサイクルの理解が重要です。 新しい接続ごとに、新しい IAM 認証トークンを生成します。トークンの生成はローカルでの署名操作で、負荷はごくわずかです。公式の AWS コネクタはこれを自動的に処理します。独自のプールを使う場合は、トークンを手動でキャッシュするのではなく、接続ファクトリの beforeConnect フックで generate-db-connect-auth-token コマンドを呼び出してください。 トークンは、基となる IAM 認証情報より長くは有効になりません。1 時間のセッションでロールを引き受けた場合、 --expires-in の値に関係なく、トークンは最大でも 1 時間で失効します。 データベースロールは最小権限に絞る。 IAM 認証は誰が接続できるかを制御しますが、接続後にそのセッションが何をできるかは制限しません。各アプリケーションのデータベースロールには、必要な最小限の権限だけを付与します。たとえば、読み取り専用のサービスには SELECT を付与し、書き込みアクセスは特定のスキーマやテーブルに限定します。アプリケーションの接続プールで管理者ロールを使うのは避けてください。 手順の詳細は、 Authorizing database roles to use SQL in your database を参照してください。 戦略 2: 最大接続有効期間にジッターを設定して接続の失効を防ぐ Amazon Aurora DSQL には、1 時間という接続の最大有効期間のハード制限があります。接続がこの期間に達すると、アイドル中でもトランザクションの途中でも、サービスはその接続を閉じます。プールはそうなる前に接続をリサイクルする必要があります。 ジッターが重要な理由: プール内のほとんどの接続がほぼ同じタイミング (たとえばアプリケーションの起動時) に作成された場合、それらはほぼ同時に失効します。すると新しい接続リクエストが一斉に集中する「thundering herd」が発生し、1 秒あたり 100 接続のレート制限を超えることがあります。最大有効期間にランダムなジッターを加えると、接続のリサイクルが時間的に分散されます。 // Go (pgxpool) — Use native per-connection jitter config.MaxConnLifetime = 55 * time.Minute config.MaxConnLifetimeJitter = 5 * time.Minute // Each connection independently gets a lifetime between 55-60 minutes // Java (HikariCP): maxLifetime already applies per-connection jitter automatically // Python: use max_lifetime with a randomized offset in your pool factory 期待される結果: 接続のリサイクルが個々の接続にわたって時間的に均等に分散し、新しい接続リクエストが 1 秒あたり 100 のレートクォータを十分に下回った状態を保てます。 戦略 3: プールサイズを最適化する Amazon Aurora DSQL は多数の同時接続でも高い性能を発揮しますが、スループット、レイテンシー、リソース消費のバランスをとれるようにプールサイズを設定する必要があります。 表 3: 接続プールのサイズ設定パラメータの推奨初期値 パラメータ 推奨初期値 理由 最小プールサイズ 2〜5 接続 低負荷時にリソースを無駄にせず、プールをウォームに保つ 最大プールサイズ アプリケーションインスタンスあたり 10〜20 控えめに始める。アプリケーションインスタンスを追加してスケールアウトする 接続タイムアウト 30 秒 トラフィックが多いイベント中に、DSQL の接続バーストキュー (100/秒) を消化できるようにする アイドルタイムアウト 無効 (または MaxConnLifetime に合わせる) アイドル接続にコストはかからない。開いたままにしておけば、再接続時の不要な TLS/認証の負荷を避けられる Amazon CloudWatch で DPU (Distributed Processing Unit) の消費を監視し、容量の追加が必要なタイミングを把握します。トラフィックの急増が予想される場合 (スケジュールされたバッチジョブやマーケティングキャンペーンなど) は、事前にプールをウォームアップしておきます。インスタンスあたりの接続数を増やすのではなく、小さめのプールを持つアプリケーションインスタンスを増やして水平方向にスケールアウトします。Amazon Aurora DSQL は、短期的な負荷増加には垂直スケーリングで、長期的な変動にはフリート全体の水平スケーリングで対応します。クラスターあたり 10,000 同時接続のクォータを活用し、必要であれば AWS Support に連絡して上限を引き上げてください。 期待される結果: ワークロードに合わせてプールが適切にサイズ設定され、接続の負荷とプール枯渇エラーの両方を最小限に抑えられます。 戦略 4: マルチリージョンでのプーリングの考慮事項 Amazon Aurora DSQL のマルチリージョン active-active デプロイを使うグローバル分散アプリケーションでは、接続プーリングにもう一段の検討が必要です。 このサービスのマルチリージョンアーキテクチャでは、SQL の実行、読み取り、書き込みのスプーリングをクライアントのリージョン内でローカルに処理します。リージョン間の通信は、コミット時に Adjudicator と Journal のレプリケーションプロトコルを通じてのみ発生します。マルチリージョンモードでも読み取りはローカルなので、読み取り専用トランザクションはリージョン間のレイテンシーなしで完了します。読み書きトランザクションでリージョン間レイテンシーが発生するのは COMMIT 時のみで、実行した SQL ステートメントの数に関係なく、リージョン間のラウンドトリップ約 1〜1.5 回分です。各ステートメントごとにラウンドトリップが発生する転送ベースの設計と比べ、レイテンシーを削減できます。リージョンの選択も重要です。接続性の良いリージョンの組み合わせほどコミットレイテンシーが低く、地理的に離れた組み合わせほどそれに比例して大きくなります。 マルチリージョンでのプーリングでは、リージョンごとに個別の接続プールを作成し、それぞれローカルの Amazon Aurora DSQL エンドポイントを指すようにします。トークン更新のロジックがリージョンごとの IAM エンドポイントを考慮していることを確認してください。アプリケーションは active-active アクセスを前提に設計します。このサービスにはプライマリリージョンという概念がなく、各リージョンは対称的に動作します。 期待される結果: リージョン間レイテンシーはコミット時にのみ発生し、読み取りはローカルリージョンの速度で完了します。 オブザーバビリティ: データベースだけでなく接続プールも監視する Aurora DSQL の CloudWatch メトリクス (TotalTransactions や CommitLatency など) はデータベースレベルの挙動を示しますが、接続プールがボトルネックかどうかまではわかりません。プールの健全性を把握するには、アプリケーション側に計測を組み込む必要があります。次に挙げるクライアント側のメトリクスが、接続プーリングの問題を捉える重要なシグナルです。 アプリケーションに組み込むべきメトリクス メトリクス 示す内容 アラートのしきい値 発生時のアクション プール取得レイテンシー (.get() の所要時間) プールから接続を取得するまでアプリケーションが待つ時間 P95 > 500 ms プールが飽和している。最大プールサイズを増やすか、アプリケーションインスタンスをスケールアウトする アクティブ接続数 現在トランザクションを実行中の接続数 最大プールサイズに張り付いている すべての接続が使用中。プールサイズを増やすか、トランザクションの実行時間を短くする アイドル接続数 プール内で使われていない接続の数 0 のまま張り付いている トラフィックのバーストに対する余裕がない。最小プールサイズを増やすか、急増が予想される前にウォームアップする リクエストの同時実行数 対 プールサイズ 処理中の同時リクエスト数と最大プールサイズの比率 同時実行数が最大プールサイズの 80% を超える プール枯渇に近づいている。スケールアウトするか、プールサイズを増やす プール枯渇イベント 利用可能な接続を待って接続リクエストがタイムアウトした回数 > 0 直ちに対応が必要。プールサイズを増やす、トランザクションの実行時間を短くする、またはアプリケーションインスタンスを追加する 接続作成レート プールが 1 秒あたりに開く新規接続の数 100/秒 に近づく Aurora DSQL のレート制限に達するリスクがある。ジッターを加える、インスタンスの起動をずらす、または最小プールサイズを増やして接続の入れ替わりを減らす ほとんどの接続プールライブラリは、これらの統計をネイティブに公開しています。 Java (HikariCP): HikariPoolMXBean を使って getActiveConnections() 、 getIdleConnections() 、 getThreadsAwaitingConnection() 、 getTotalConnections() にアクセスします。Micrometer 経由で CloudWatch やアプリケーションパフォーマンスモニタリング (APM) ツールにエクスポートします。 Python (psycopg ConnectionPool): pool.get_stats() を使います。 pool_min 、 pool_max 、 pool_size 、 pool_available 、 requests_waiting 、 requests_num を返します。 Node.js (node-postgres): pool.totalCount 、 pool.idleCount 、 pool.waitingCount に直接アクセスします。一定間隔で、またはリクエストごとに出力します。 Go (pgxpool): pool.Stat() を使います。 AcquireCount() 、 AcquiredConns() 、 IdleConns() 、 TotalConns() 、 AcquireDuration() を提供します。 これらはカスタム CloudWatch メトリクスとして発行するか ( PutMetricData API またはログ内の CloudWatch Embedded Metric Format を使用)、既存の APM ツール (AWS X-Ray、Datadog、Prometheus/Grafana など) に送信します。 補足: Aurora DSQL の CloudWatch メトリクス 主要なシグナルはクライアント側のメトリクスであるべきですが、プールサイズを決めるうえで役立つ Aurora DSQL のメトリクスが 1 つあります。 メトリクス プーリングに役立つ理由 TotalTransactions プール取得レイテンシーと関連付けて見ます。トランザクションが増えているのに取得レイテンシーが横ばいなら、プールサイズは適切です。両方が増えているなら、容量の追加が必要です。 よくある接続プールの問題のトラブルシューティング アラートが発生したら、この表を使って最もよくある障害シナリオを診断し、対処してください。 表 5: よくある Amazon Aurora DSQL 接続プール問題のトラブルシューティングガイド 症状 考えられる原因 診断ステップ 対処 プール枯渇 (接続タイムアウトエラー) トラフィックに対して maxPoolSize が低すぎる アクティブ接続とアイドル接続を比較して監視する 最大プールサイズを増やすか、アプリケーションインスタンスを追加する 約 55 分後の認証失敗 トークンの失効が処理されていない トークン更新のログを確認する 公式コネクタを使っているか確認する。MaxConnLifetime の設定を確認する 新規接続レートのエラー 起動時の thundering herd 接続作成のタイムスタンプを確認する MaxConnLifetime にジッターを加える。アプリケーションインスタンスの起動をずらす クイックリファレンス: 主要な制限と設定 接続の有効期間、トランザクションタイムアウト、接続レート、同時実行数などの最新の制限については、 Cluster quotas and database limits を参照してください。 まとめ 本記事では、Amazon Aurora DSQL のトランザクション単位のプーリングモデルが従来の PostgreSQL とどう違うかを解説しました。また、コネクタの選択からマルチリージョンでのプーリングまで、アプリケーションの性能と回復力を保つための具体的な 4 つの戦略も紹介しました。PgBouncer や pgpool-II のようなデータベース側のプロキシは使わないでください。このサービスはトランザクション単位の多重化をネイティブに処理します。マルチリージョンのデプロイでは、リージョン間レイテンシーがコミット時にのみ発生することを覚えておいてください。トランザクションはローカルの読み書きを活かせるように設計しましょう。 これらの戦略に従えば、アプリケーションは Amazon Aurora DSQL の自動スケーリング、強整合性、高可用性を最大限に活用できます。接続の負荷を最小限に抑え、大規模でもレイテンシーを予測可能に保てます。 著者について Tejas Dubey Tejas は、 Tejas は AWS のテクニカルアカウントマネージャーで、エンタープライズのお客様が回復力とセキュリティに優れ、AI 対応のクラウドアーキテクチャを構築できるよう支援しています。新しいテクノロジーを実際のビジネス価値に変えることに情熱を注いでいます。仕事を離れているときは、息子を追いかけたり、ピックルボールをしたり、最新のテクノロジーをいじったりしています。 Dhvani Shah Dhvani は、 Dhvani はテクニカルアカウントマネージャーで、クラウドの回復力、セキュリティ、生成 AI の導入に関する戦略的なガイダンスでエンタープライズのお客様を支援しています。デジタルトランスフォーメーションを加速する、スケーラブルで安全なソリューションの設計を支援しています。仕事以外では、旅行やバドミントン、家族と過ごす時間を楽しんでいます。 この記事は Kiro が翻訳を担当し、Solutions Architect の Arisa Izuno がレビューしました。
プログラミング言語を選ぶとき、開発効率や学習コスト、エコシステムの充実度など、考慮すべき要素は多岐にわたります。OSS パッケージを標的にしたサプライチェーン攻撃の増加や脆弱性に対するゼロデイ攻撃の発生といった状況を踏まえると、「アプリケーションが依存するライブラリをどう管理するか」も技術選定の重要な軸となっています。 私の所属する Control Plane 部の認証認可グループでは、2025年から、 Go の利用を本格化しました。技術選定時に挙げられていたメリットの一つが、まさに依存管理にあります。 本記事では、私たちが Go を選んだ背景と、1年間実際に開発してみて分かったことを振り返ります。 なぜ Go を選んだか 対象システムの特性 キャディの主流言語: TypeScript Go の特徴 1年間やってみてどうだったか 良かった点 メンテナンスコストの低さ AIコーディングとの相性 苦労した点 ライブラリに頼らないことのトレードオフ Go の慣習に慣れるまでのコスト 期待と現実のギャップ 採用した技術スタック まとめ なぜ Go を選んだか 対象システムの特性 キャディでは、製造業AIデータプラットフォームCADDi の基盤として「Control Plane」と呼ばれるシステム群を開発しています。Control Plane は、テナント管理や認証認可など、アプリケーションの機能から独立した管理層を担うシステムです。詳しくは こちらの記事 で紹介しています。 開発言語を選ぶにあたって重要だったのは、Control Plane が持つ特性です。Control Plane は、1つの大きなサービスではなく、認証ゲートウェイ、トークン発行、テナント管理、認可といった、それぞれが明確な責務を持つ小さなサービスの集合体です。各サービスはシンプルな機能を提供する一方で、プラットフォーム基盤としての信頼性が求められます。 つまり、「小さなサービスを多数、手堅く作れること」が言語選定において重要な要件でした。 キャディの主流言語: TypeScript キャディのバックエンドでは TypeScript が主流です。TypeScript の開発エコシステムの充実度は群を抜いています。 また、フロントエンドとバックエンドを同じ言語で統一できるのも大きなメリットです。 一方で、依存管理の観点では、ライブラリの数が増えがちで、依存管理のコストが増大します。 Go の特徴 Go は Web API の構築に向いた言語です。標準ライブラリもサードパーティのライブラリもシンプルなものが多く、必要なパーツを組み合わせて開発するようなスタイルを取りやすいです。 こうした特性が、小さなサービスを数多く開発・運用する Control Plane とよくマッチすると判断しました。 1年間やってみてどうだったか 良かった点 メンテナンスコストの低さ Go を採用して最も良かったと感じているのは、メンテナンスコストの低さです。 Control Plane のアプリケーションは、一度作った後は数ヶ月単位で改修が入らないことも珍しくありません。そのような場合でも、セキュリティアップデートは定期的に適用する必要があります。このような作業は1つ1つは小さくても、積み重なると大きな負担になります。 Control Plane には、Go採用以前から開発・運用されている TypeScript(NestJS)アプリケーションがありますが、これと比較して Go アプリケーションはセキュリティアップデートが必要になる頻度が低いと感じています(およそ1/5程度)。 AIコーディングとの相性 Claude Code や Devin を用いたAIコーディングはキャディ社内でも主流となっていますが、構文がシンプルで書き手による差異が生じづらい Go は、AIが読み書きしやすいという点でもメリットがあります。また、コードフォーマッター( go fmt )やテストランナー( go test )が標準のツールチェインに組み込まれていて、かつ、高速に動作するという点も Go の強みです。 苦労した点 ライブラリに頼らないことのトレードオフ Go では依存ライブラリを少なく保って開発することができます。これは裏を返せば、基本的な処理を自前で実装する必要があるということです。例えば、以下のコードではサーバのグレースフルシャットダウンを行なっています。 ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer stop() go func () { // e は Echo インスタンス if err := e.Start( ":" + c.Port); err != nil && !errors.Is(err, http.ErrServerClosed) { slog.Error(fmt.Sprintf( "error occurred when starting the server: %v" , err)) } }() ... <-ctx.Done() ctx, cancel := context.WithTimeout(ctx, 10 *time.Second) defer cancel() if err := e.Shutdown(ctx); err != nil { slog.Error(fmt.Sprintf( "error occurred when shutting down the server: %v" , err)) } このような、他の言語であればフレームワークが担ってくれるような処理も自分たちで書く必要がありました *1 。 依存の少なさと自前実装の手間は、トレードオフの関係にあります。私たちはこのトレードオフを意図的に受け入れました。長期的な運用コストの低さのほうが、初期の実装コストより重要だと判断したからです。 Go の慣習に慣れるまでのコスト チームメンバーの多くは Go での開発経験がそれほど多くありませんでした。Go 特有の慣習やイディオム ── たとえばエラーハンドリングの作法や、パッケージ構成の考え方 ── に慣れるまでには時間がかかりました。言語仕様の学習コストは低くても、「Go らしいコード」がわかるようになるまでには別のハードルがあります。 期待と現実のギャップ Go の採用にあたっては、goroutine による並行処理のしやすさにも期待していました。しかし、現時点では並行処理を積極的に活用する場面はまだありません。現在の Control Plane のサービス群は、典型的なリクエスト/レスポンス型の API が中心であり、goroutine の恩恵を実感する局面がまだ訪れていないのが実情です。 今後、バッチ処理や非同期ワーカーのようなワークロードが増えてきた際に、この特性が活きてくると考えています。 採用した技術スタック Goコミュニティにおいて実績のある技術スタックの中で、薄めのものを選定しました。 通信: Connect フレームワーク: Echo ※アプリケーションによってはフレームワーク無しの場合も ORM: Bun JWT: lestrrat-go/jwx いずれも優れたライブラリですが、特に Connect は素の gRPC に比べてデバッグしやすく、重宝しています。 まとめ Go は万能な言語ではありません。例えば、Go の特徴の一つである明示的なエラー処理は、コードの冗長さと表裏一体です。しかし、小さなサービスを数多く開発・運用する Control Plane においては、Go のシンプルさが強みになりました。 技術選定は常に文脈次第であり、私たちの経験がそのまま他の組織に当てはまるとは限りません。それでも、「Control Planeを Go で作る」という選択は、私たちにとって正しかったと考えています。 今後も Go を活用しながら Control Plane を拡充し、CADDi のマルチテナントプラットフォームをより堅牢なものにしていきます。 Go を使った Control Plane の開発に興味がある方は、ぜひ以下のページもご覧ください。 caddi.tech tech.caddi.com *1 : このサンプルコードは Echo v4 です。Echo v5 ではフレームワークに グレースフルシャットダウン機能 が搭載されています。
はじめに # 本記事では、GitHub Copilotのエージェント(Agents)およびインストラクション(Instructions)の設定方法について説明します。 Agents(エージェント)とは 特定のタスクや分野に特化した専門家としてCopilotをカスタマイズする機能です。 たとえば、バックエンド開発用、フロントエンド開発用など、異なる専門性を持つ複数のエージェントを定義し、状況に応じて使い分けることができます。 Issueにアサインしたり、VS Code上で選択して利用します。 Instructions(インストラクション)とは Copilotに対する共通のルールや制約を定義する機能です。 コーディング規約、命名規則、プロジェクト固有のベストプラクティスなどを記載し、すべての開発者が一貫したコード生成の支援を受けられるようにします。 エージェントと併用され、VS CodeでのコーディングやPull Requestのレビューなど、あらゆる場面で適用されます。 適用順序 # 以下の順序でルールが適用されます。競合するルールがある場合、数字の小さいルールが優先されます。 選択したエージェント(例: agents/backend.agent.md ) マッチするインストラクション(例: instructions/typescript.instructions.md ← applyToパターンに一致) 全体のインストラクション( copilot-instructions.md ) ファイルの配置 # GitHub Copilotが認識できるよう、下記のように配置します。 .github/ agents/ xxx.agent.md : 特定の分野(ロールなど)に合わせて定義するエージェントファイル(e.g. backend, frontend, test) copilot-instructions.md : 全体に適用されるルールや制約を定義するファイル instructions/ xxx.instructions.md : 特定の分野(テクノロジーなど)に合わせて定義するファイル。(e.g. typescript, python, react)※サブフォルダーで分類したくなりますが、フォルダー分けすると読み込まれません。 --> Caution AGENTS.mdについて .github/AGENTS.md (ディレクトリ直下)は GitHub CLI用 のファイルです。 VS Codeでは読み込まれませんので注意してください。 VS Codeでは agents/ ディレクトリ内の *.agent.md ファイルのみが有効です。 --> Information ワークスペースでの配置 VS Codeでマルチリポジトリ(複数のリポジトリを同時に開いて作業)する場合、 設定ファイルは ワークスペースのルートディレクトリ の .github/ に配置する必要があります。 各リポジトリに個別の設定を使いたい場合は、リポジトリごとに別のVS Codeウィンドウで開いてください。 .github/agents/sample.agent.md : ワークスペースルートに配置すると選択できます repo-A/.github/agents/sample.agent.md : 各リポジトリ配下のエージェントは読み込まれません repo-B/.github/agents/sample.agent.md ヘッダー部の用途 # 定義ファイルのヘッダー部に設定できるプロパティの一部を紹介します。 エージェント --- name: Backend Agents(TypeScript) description: This custom agent implements backend features using TypeScript. model: GPT-5.2 --- ※エージェント選択画面(VS Code) プロパティ 設定時 未設定時 name エージェント名として使用 拡張子を除いたファイル名をエージェント名として使用 description エージェントの説明として使用 空欄 model 使用するAIモデルを指定 デフォルトモデル インストラクション --- applyTo: "src/**/*.ts" # e.g. src配下のtsファイルを対象 --- プロパティ 設定時 未設定時 applyTo 指示を適用するファイルのパターンを指定(globパターン) すべてのファイルに適用 --> Information applyToの指定例 **/*.ts - すべてのTypeScriptファイル src/** - srcディレクトリ配下のすべてのファイル **/*.{js,ts} - JavaScriptとTypeScriptファイル 定義例 # エージェントおよびインストラクションの定義例を以下に示します。 これらを組み合わせることで、プロジェクトやタスクに最適化されたCopilotの動作を実現できます。 エージェント:バックエンド開発者 # .github/agents/backend-specialist.agent.md --- name: Backend Developer Agent description: NestJSを使用したバックエンド開発の専門家 --- # 役割 あなたはNestJSとTypeScriptを使用したバックエンド開発の専門家です。 # 技術スタック - **フレームワーク**: NestJS 11.x - **言語**: TypeScript 5.x - **データベース**: PostgreSQL - **ORM**: TypeORM - **テスト**: Jest # コーディング規約 - ヘキサゴナルアーキテクチャを遵守してください - DTOには必ずバリデーションデコレータを付与してください - 例外処理は適切なHTTPステータスコードを返すカスタム例外を使用してください # テスト方針 - 単体テストはすべてのServiceクラスに対して作成してください - テストカバレッジは80%以上を目標としてください 全体インストラクション:プロジェクト共通規約 # .github/copilot-instructions.md # コーディング規約 ## 共通ルール - **言語**: 日本語でコメントとドキュメントを記載してください - **命名規則**: - クラス名: PascalCase - 関数名・変数名: camelCase - 定数: UPPER_SNAKE_CASE - **インデント**: スペース2文字 - **文字列**: シングルクォートを使用 ## 禁止事項 - `any`型の使用は原則禁止(型定義を適切に行うこと) - `console.log`のコミットは禁止(ロガーを使用すること) - 機密情報のハードコードは厳禁 ## セキュリティ - 外部入力は必ずバリデーションを行うこと - SQLインジェクション対策を実施すること - 認証・認可が必要なエンドポイントにはガードを設定すること 分野別インストラクション:TypeScript専用ルール # .github/instructions/typescript.instructions.md --- applyTo: "**/*.ts" --- # TypeScript固有のルール ## 命名規則 - ファイル名: kebab-case ## 型定義 - 明示的な型注釈を優先してください - Utility Typesを活用してください(`Partial`, `Pick`, `Omit`など) - 複雑な型は`type`エイリアスで定義してください ```typescript // Good type UserProfile = { id: string; name: string; email: string; }; type UserProfileUpdate = Partial<Pick<UserProfile, 'name' | 'email'>>; // Bad const updateUser = (data: any) => { ... }; ``` ## 非同期処理 - `async/await`を使用してください(Promiseチェーンは避ける) - エラーハンドリングは`try-catch`で行ってください ## インポート順序 1. 外部ライブラリ 2. 内部モジュール(絶対パス) 3. 相対パス ```typescript // 外部ライブラリ import { Injectable } from '@nestjs/common'; import { Repository } from 'typeorm'; // 内部モジュール import { UserEntity } from '@/entities/user.entity'; import { CreateUserDto } from '@/dto/create-user.dto'; // 相対パス import { UserService } from './user.service'; ``` 運用上の注意事項 # VS Codeでのキャッシュ管理 # エージェントやインストラクションのファイルを変更した場合、初回ロードした内容がキャッシュされます。 変更を反映するには、以下のいずれかの操作が必要です。 チャットで変更したファイルを明記して再読み込みを促す(例: sample.agent.mdを変更したので再読み込みしてください ) 新しいチャットを開始する VS Codeを再起動する GitHubでのファイルサイズ制限 # エージェントファイルの文字数が30,000文字(バイト数ではなく、ヘッダー部は含まない)を超えると選択できなくなります。 適切な粒度でファイルを分割してください。 GitHub IssueでCopilotをアサイン後に表示されるダイアログ

動画

該当するコンテンツが見つかりませんでした

書籍

該当するコンテンツが見つかりませんでした