この記事は 株式会社TRAILBLAZER Advent Calendar 2025 の2日目の記事です。
TRAILBLAZERのソリューション事業部の @bignoble です。
主に、JR西日本の内製開発チームでSRE業務を担当しています。
AWSのIaC化にAWS CDKを導入して約1年が経過しました。この1年で得た実運用におけるリアルな学びと直面した課題について、具体的な教訓を共有できればと思います。
💡 はじめに: なぜCDKを選んだのか
CDKを導入するまでのインフラ管理は、CFn(CloudFormation)によるIaC化を少しずつ取り組み始めたところで、一部ではベンダーが使用していたTerraformも混在していました。
この状態は、設定の複雑化と属人化を招いており、冗長なYAML構造の管理負荷がチームのボトルネックになりつつあると感じていました。
CDKが選定された理由は、当時のSREチームにアプリケーション開発のバックグラウンドを持つメンバーが多かったというチーム背景が大きかったと思います。
アプリケーション実装と同じ感覚でインフラを定義できること、そしてコードベースで構成が明確に把握できることで、環境理解の促進とデリバリーのスピードアップを期待しました。
✨ Part 1: アプリケーションエンジニアがCDKに感じた生産性向上の魅力とメリット
CDKは、プログラミング言語(TypeScript, Pythonなど)の特性をインフラ構築に持ち込み、従来のIaCツールにはなかった大きなアドバンテージを持つツールの一つです。
取っ付きやすさ:使い慣れた言語と宣言的な実装
従来のIaCツールが特定のDSL習得を必要とする(例:Terraformの場合、HCL)のに対し、CDKはTypeScriptで実装が可能です。AWSリソースをnew Vpc(...)のようにオブジェクトとして宣言的にインスタンス化できる構造は、学習コストを大幅に軽減してくれました。
これにより、インフラ構築がアプリケーション開発の延長線上にあるタスクとなり、「インフラは専門外」という心理的な壁も低減され、チーム全体でインフラ構成を理解し、関与しやすい環境が整いました。
抽象化の恩恵:冗長性からの解放とカスタムコンストラクト
CFnでは、リソース間の依存関係や繰り返し設定によりYAMLが非常に冗長になり、可読性の低下と管理の煩雑さが課題でした。
CDKでは、L2/L3コンストラクトを活用することで、リソース群を高レベルで抽象化できます。
例えば、VPCやサブネット一式をNetworkConstructに、ECS関連の設定群をEcsConstructにまとめることで、ビジネス要件に近い、論理的な単位でインフラ定義が可能になりました。これにより、コードの可読性はCFnと比較して大きく向上しました。
🚧 Part 2: 1年使って見えた課題:「Terraformだったらなぁ」と感じた領域
CDKは強力なツールですが、運用規模の拡大と複雑化の中で、いくつかの技術的・運用上の課題にも直面しました。
抽象化の「功罪」:リソースの全体像把握の難しさ
CDKコードが美しく抽象化されればされるほど、大規模なスタックでは「このデプロイで実際にAWS上にどのようなリソースが作成されるのか」という、最終的なリソース構成の全体像把握が難しくなる側面があります。
例として、高レベルなL3コンストラクトを使用します。コードは非常に簡潔ですが、このコードを見ただけでは裏側で幾つのリソースが作られるかが分かりません。
// lib/app-stack.ts (抽象化されたCDKコード)
import * as ecs from 'aws-cdk-lib/aws-ecs';
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';
// ... (VPCの定義は省略) ...
new ApplicationLoadBalancedFargateService(this, 'AppService', {
vpc: vpc,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry('my-app:latest'),
containerPort: 80,
},
publicLoadBalancer: true,
// その他の設定はすべてデフォルト
});
// 👆 記述はたったこれだけだが、実際には約10~15個のAWSリソースが作成される
TerraformのHCLは、冗長性がある反面、宣言的でフラットな記述形式のため、特にDev/Stg/Prdといった環境間の設定差異や、特定のスタックが管理するリソースの把握は、Terraformの方が優位だと感じられることがありました。
# main.tf (Terraform HCL)
# ECS Fargateサービス構築の一部抜粋
resource "aws_ecs_cluster" "app" {
name = "app-cluster"
}
resource "aws_ecs_task_definition" "app" {
family = "app-task"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
# ... ログ設定、実行ロールなど多数のプロパティ ...
}
resource "aws_alb" "app_lb" {
name = "app-lb"
# ... LBの種類、セキュリティ設定など多数のプロパティ ...
}
resource "aws_ecs_service" "app" {
name = "app-service"
cluster = aws_ecs_cluster.app.name
task_definition = aws_ecs_task_definition.app.arn
# ... ネットワーク設定、ロードバランサーとの紐付けなど多数のプロパティ ...
}
# 👆 冗長だが、このファイルに書かれたリソース以外はデプロイされないことが保証される
CDKの辛み:特定プロパティの変更を無視する機能の不在
運用上、特定のセキュリティ設定やS3バケットポリシーの一部など、CDKの管理から一時的に外し、手動での変更を許可したいというニーズが生まれることがあります。
Terraformにはignore_changesというStateの変更を無視する便利な機能がありますが、CDKには直接的に同等な組み込み機能は存在しません。
# Terraform HCL
resource "aws_s3_bucket" "log_bucket" {
bucket = "my-corporate-log-bucket-12345"
acl = "private"
lifecycle {
# 運用チームが手動で設定するS3バケットポリシーを管理対象から外す
ignore_changes = [
policy, # <- これが「ignore_changes」機能
]
}
}
# 👆 運用上、特定のポリシーだけは緊急時に手動で変更したい、というニーズに応えられる
CDKの場合、やむを得ずCfnResourceのインスタンスを取得して設定を操作する「エスケープハッチ」を利用することがありましたが、これは少々ハック的でメンテナンス性を低下させる可能性があり、今後の負債になりかねない対策だと感じています。
// lib/s3-bucket-stack.ts (CDK)
import * as s3 from 'aws-cdk-lib/aws-s3';
import { CfnResource } from 'aws-cdk-lib';
// S3バケットを定義
const bucket = new s3.Bucket(this, 'LogBucket', {
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// エスケープハッチを使用して、L1リソースを取得
const cfnBucket = bucket.node.defaultChild as CfnResource;
// L1リソースに対して、CFnのメタデータ操作を適用
// CFnのリソース宣言に「UpdateReplacePolicy: Retain」などを直接書き込むイメージ
// ただし、特定のプロパティのドリフトを無視する直接的な機能は存在しない
cfnBucket.addOverride('Properties.Policy', { /* ... 何らかのポリシー定義 ... */ });
// 👆 CDKの推奨する抽象化を破り、L1レベルで直接設定を操作する必要がある
👻 Part 3: 修羅の道!既存環境のCDK化で直面した技術的な課題と現実的な落とし所
直面した課題とリスクの検証
いくつか直面した課題の中でも、すでに手動で構築・運用されているAWS環境をCDKに移行することは、非常に困難な課題だと思います。
これを実現するためにまず、既存リソースをCDKのStackにインポートする手法を検討しました。
1. 既存環境インポートの基本的な流れ
既存のAWSリソースをCDKの管理下に置くには、主に以下の手順を踏みます。
- 既存リソースの定義
CDKで既存リソースと全く同じコードを書く(new s3.Bucket(...)など) - インポート用テンプレート作成
cdk synthを実行し、CFnテンプレートを生成する - インポート実行
cdk importコマンドで、CFnのインポート機能を用いて、CDK(CFn)のStackにリソースを取り込む
2. 軽微な「ドリフト」による意図しない変更(再作成リスク)
このインポートの際に、CDKコードとAWS上の実際の状態に軽微な差異(ドリフト)があると、次のcdk deploy実行時にCFnがその差異を埋めようとします。
実際に起こり得る例) S3バケットポリシーの差異
| 要素 | 状態 | 詳細 |
|---|---|---|
| AWS上(既存) | ポリシーA | 運用上、手動で追加された特定のIPアドレスからのアクセスを許可するポリシー(例: 監査システム用)が存在する。 |
| CDKコード(新規) | ポリシーB | CDKコードには、最低限必要なサービスアカウントのアクセス許可ポリシーしか定義されていない。 |
| 結果 | ポリシーA ≠ ポリシーB | CDKコードのcdk deployを実行すると、CFnはポリシーBの状態を正とみなし、AWS上のポリシーAに上書きを試みる。 |
この上書きにより、手動で追加されていた監査システム用のアクセス許可が意図せず削除され、システム障害を引き起こすリスクがあり、これは安定稼働が最優先の本番環境では容認できないリスクです。
特にRDSやS3など、永続的なデータを持つステートフルなリソースにおいて、インポート後の最初のデプロイで軽微な設定ドリフトに起因して破壊的な変更やデータ消失に直結する可能性を考慮するとインポートは高リスクな選択肢であるという認識に至りました。
リソースを新規作成し、徐々に既存リソースからトラフィックを移行する方法もありますが、膨大な工数と高レベルなリスク管理が求められます。
現在の現実的な方針
この移行リスクと工数を総合的に判断し、私たちは既存環境のすべてを一度にCDKで管理しようとする試みは見送る判断をしました。
現実的な方針として現在は、「新規リソースの構築時のみCDKを使用し、既存リソースを参照する場合はfromVpcAttributes(),fromLookup(), fromClusterAttributes()などの参照メソッドを活用していく」という方向性です。
このアプローチは、データ損失リスクのある既存のステートフルリソース(RDS, S3など)のインポートを避け、CDKの管理対象外に保ちつつ、新しいサービスに必要なネットワーク情報やセキュリティ情報のみを安全に取り込むことが可能です。
これは、後述する依存関係の最小化(疎結合の維持)にも繋がる、安全性の高い段階的なIaC化戦略ではないかと考えています。まずは、新しく構築する領域からIaC化を徹底しています。
🌟 Part 4: 運用1年で得た「生きた学び」とチームのベストプラクティス
この1年間の実運用を通じて得た学びと、SREチームで定めた設計ルールをいくつかピックアップして紹介します。
依存関係の管理とデッドロックの回避
デッドロック問題の具体例:
クロススタック参照を用いた際、参照先スタックから古いリソースを削除しようとすると、内部のエクスポートパラメータが参照元スタックに依存しているため、削除がブロックされるデッドロック状態に陥りました。
特に、リージョンを跨いだクロススタック参照を用いる場合、CDKはスタック間でリソースオブジェクトを受け渡すだけの簡単な実装で実現こそできますが、内部的にはSSM Parameterやカスタムリソース(Lambda関数)が自動的に作成されます。
同一リージョンのクロススタック参照以上に依存が(しかも身に覚えのないリソースによって)強くなってしまい、この内部メカニズムを把握するまで原因の究明に時間がかかってしまいました。
回避策: 依存関係の最小化
リージョンを跨いでいないクロススタック参照であれば、new cdk.CfnOutput(...)やcdk.Fn.importValue(...)を使い、スタック間で直接的なリソース渡しをしないことで結合度を減らすことが有効です。
ただしリージョンを跨ぐ場合、上記のようなSSM Parameterによる参照渡しができません。そのため、スタック間ではリソースオブジェクト全体を参照として渡すのではなく、特定のARNやIDなど、必要最小限の情報のみを渡すように一部の設計を見直しました。
チーム開発におけるコンストラクトの設計ルール
再利用性を高め、属人化を防ぐために、コンストラクトの粒度と配置に明確なルールを定めてチーム内での共有を図っています。ごくごく一例ですが、以下のようなものがあります。
低レイヤーコンストラクト(共通利用):
/lib/construct/common配下に実装。特定のプロダクトに依存せず、VPCやS3バケットなど汎用性の高いリソース群を定義。
高レイヤーコンストラクト(プロダクト特化):
/lib/construct/product_nameに実装。CommonコンストラクトやCDKライブラリを組み合わせることで、特定のプロダクト向けに高度に抽象化されたインフラ定義を実現。
レビュー: cdk diffによる変更内容の徹底確認
デプロイ前のコードレビューでは、必ずcdk diffの結果を共有します。作成・更新・削除されるリソースが、設計意図と乖離していないかをチーム内でチェックすることで、予期せぬインシデントを未然に防いでいます。
🚀 まとめと今後の展望:アジャイルなデリバリーへの貢献
CDK導入は、単なるツールの変更にとどまらず、内製開発チームのデプロイ・リリース管理における構造的な改善になりました。課題であったベンダー依存を解消しつつあり、自律的な開発体制への大きな一歩を踏み出すことができたと確信しています。
定量的な効果(効率性)
プロダクトAでは、元々ベンダー依存が強かったリリースサイクルが、CDKコードとして管理下に置かれることで、手動作業の介入余地が減少。これにより、リリース判断と実行がより迅速かつアジャイルに実行可能になり、市場へのフィードバックサイクルの短縮に貢献しました。
またプロダクトBにおいては、内製化まもないフェーズであったため自分たちで管理できる開発環境(Dev)の構築すら課題となっていた状況でしたが、SREチームがCDKの抽象化能力を最大限に活かして環境を迅速に構築。開発チーム自身がコードを通じてデプロイできるようになったことで、開発効率が大きく向上し、プロジェクト全体のスピードアップに貢献できました。
今後の挑戦と展望
プロダクトBのStg/Prd環境は、依然としてベンダー依存が強く残るインフラです。これを「修羅の道」と表現しましたが、リスクを最小限に抑えた段階的なCDK管理下への移行を実行していきます。既存リソースの保護と、新規リソースのCDK化を両立させることで、リリースサイクルのさらなる加速を目指します。
また今後は、CDKのプログラミング言語としての特性を活かし、インフラコードに対してもユニットテストや統合テストの導入も検討していきたいです。インフラの変更が安全であることをコードで証明できるようにすることで、リリース時の精神的な負担を軽減し、品質とスピードの向上を達成したいと考えています。
さいごに
TRAILBLAZERでは積極的にエンジニアを募集中です。
もしこの記事をきっかけに興味を持っていただけた方はぜひ採用ページもご覧ください。