【第2回】AWS CDK × Python インフラ自動化入門 — コンストラクトを理解してVPC・EC2・S3を構築する

テックツール

前回は AWS CDK の環境構築(Node.js と CDK CLI、Python の仮想環境まわり)と、cdk initcdk bootstrapcdk deploy でとりあえず動かすところまでを紹介しました。今回はその続きで、「そもそも CDK のコードって何を書いてるんだ?」という部分、つまり App / Stack / Construct の構造と Python での基本構文を整理しつつ、実際に VPC・EC2・S3 を作るところまでやってみます。

自分は最初、cdk init で生成された xxx_stack.py を見て「class があって super().__init__ があって…で、この self って何に渡してるんだ?」で普通に止まりました。そこが分かると一気に読めるようになったので、そのへんを中心に。

この記事でわかること

  • CDK の App / Stack / Construct の親子関係と構造
  • L1 / L2 / L3 コンストラクトの違いと使い分け
  • Python での基本構文(scope、id、プロパティ)
  • L2 を使った VPC・EC2・S3 の実装例
  • IAM 権限を grant メソッドで付与する方法
  • L2 に足りない機能を L1 で補う方法

CDK のコードは「App → Stack → Construct」の入れ子でできている

CDK のアプリは3階層で考えると分かりやすいです。

  • App:アプリ全体のルート。cdk synth の起点
  • Stack:CloudFormation スタック1つに対応。デプロイの単位
  • Construct:VPC や S3 バケットなど、個々の部品

で、この3つは全部「Construct」の仲間です。App も Stack も Construct を継承していて、親子関係のツリーを作っています。だから constructs パッケージから Construct を import するんですね。

# app.py
import aws_cdk as cdk
from cdk_intro.network_stack import NetworkStack

app = cdk.App()

NetworkStack(
    app, "NyanchuNetworkStack",
    env=cdk.Environment(account="123456789012", region="ap-northeast-1"),
)

app.synth()

そして Stack 側。

# cdk_intro/network_stack.py
from aws_cdk import Stack, CfnOutput, RemovalPolicy, Tags
from aws_cdk import aws_ec2 as ec2, aws_s3 as s3
from constructs import Construct


class NetworkStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        Tags.of(self).add("Project", "nyanchu-cdk")

ここが最初にひっかかったポイントなんですが、Construct のコンストラクタは基本的に 第1引数が scope(親)、第2引数が id(識別子)、第3引数以降がプロパティ という決まりになっています。Stack の中でリソースを書くとき self を渡しているのは「このスタックを親にしてね」という意味で、TypeScript の例で見る this と同じです。

第2引数の id は AWS 上のリソース名ではなく、あくまでツリー内での識別子です。これを元に CloudFormation の論理 ID(末尾にハッシュが付くアレ)が決まるので、デプロイ後に id を変えると、差分内容やリソース種類によっては置換(作り直し)が発生することがあります。VPC や RDS でこれをやると悲しいことになるので、id のリネームは慎重に。自分は検証環境で1回やらかしました。

L1 / L2 / L3 コンストラクトの違い

CDK のコンストラクトは抽象度で3段階に分かれています。

L1(Cfn○○)

CfnVpcCfnBucket のように Cfn が頭に付くやつ。CloudFormation のリソース定義とほぼ1対1で、プロパティ名もほぼそのまま。自動生成されているので、新しい AWS サービスにもいち早く対応してくれるのが強みです。

ec2.CfnVPC(
    self, "RawVpc",
    cidr_block="10.0.0.0/16",
    enable_dns_hostnames=True,
    enable_dns_support=True,
)

これだけだとサブネットもルートテーブルも IGW も全部自分で書く羽目になります。CloudFormation を素で書いてるのと変わらないので、正直あまり出番はないです。

L2

人間が書いた高レベル API。デフォルト値が設定されていて、関連リソースもまとめて面倒を見てくれます。CDK を使う旨味の8割はここだと思っています。

L3(パターン)

複数リソースの組み合わせをまるっと提供するもの。aws_ecs_patterns.ApplicationLoadBalancedFargateService とかが有名ですね。便利ですが、中で何が作られているか分からないまま使うと後で困るタイプでもあります。

余談ですが、CDK には CDK Mixins という機能があり、L1 だろうが L2 だろうがカスタムだろうが、暗号化などの共通の振る舞いを .with() で後付け合成できるようになっているらしいです。「新機能を使いたいから L1、でも L2 の便利さも欲しい」という二択がゆるくなる方向みたいで、これは別途ちゃんと触ってみたい。今回は素直に L2 で進めます。

L2 で VPC を作る(ここが一番お金の話に直結する)

VPC の L2 は本当によくできていて、1行でもう動きます。

vpc = ec2.Vpc(self, "Vpc")

これだけでサブネット、ルートテーブル、IGW、NAT Gateway まで一式作ってくれます。……作ってくれるんですが、デフォルトだと AZ ごとに NAT Gateway が立ちます。個人の検証環境でこれをうっかりデプロイして放置すると、月末に「あれ?」となるので注意してください(NAT Gateway は時間課金+データ処理料金)。

個人検証用ならこのくらいで十分かなと。

vpc = ec2.Vpc(
    self, "Vpc",
    ip_addresses=ec2.IpAddresses.cidr("10.0.0.0/16"),
    max_azs=2,
    nat_gateways=0,  # ここ注意。デフォルトのままだと課金される
    subnet_configuration=[
        ec2.SubnetConfiguration(
            name="public", subnet_type=ec2.SubnetType.PUBLIC, cidr_mask=24
        ),
        ec2.SubnetConfiguration(
            name="isolated", subnet_type=ec2.SubnetType.PRIVATE_ISOLATED, cidr_mask=24
        ),
    ],
)

古い記事だと cidr="10.0.0.0/16" と書いてあることが多いですが、今は ip_addresses が推奨です。IpAddresses.cidr() のほかに IPAM から払い出す指定もできます。

nat_gateways=0 にすると PRIVATE_WITH_EGRESS なサブネットは作れないので、代わりに PRIVATE_ISOLATED を使っています。DB を置くだけならこれで足ります。外に出たくなったら NAT Gateway を1個だけ立てるか、VPC エンドポイントを使うか、という判断になりますね。

EC2 と S3 を足して、grant で権限をつなぐ

instance = ec2.Instance(
    self, "WebServer",
    vpc=vpc,
    instance_type=ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
    machine_image=ec2.MachineImage.latest_amazon_linux2023(),
    vpc_subnets=ec2.SubnetSelection(subnet_type=ec2.SubnetType.PUBLIC),
)

instance.connections.allow_from_any_ipv4(ec2.Port.tcp(80), "allow http")

bucket = s3.Bucket(
    self, "AssetBucket",
    encryption=s3.BucketEncryption.S3_MANAGED,
    versioned=True,
    removal_policy=RemovalPolicy.DESTROY,
    auto_delete_objects=True,
)

bucket.grant_read(instance.role)

CfnOutput(self, "BucketName", value=bucket.bucket_name)
CfnOutput(self, "PublicIp", value=instance.instance_public_ip)

CDK を触っていて一番「おおっ」となったのがこの grant_read() です。IAM ポリシーの JSON を一切書かずに、「このバケットをこのロールに読ませる」と書くだけで、必要なアクションとリソース ARN を含んだポリシーが生成されます。grant_read_write()grant_put() あたりも同様。KMS キーを使っている場合はキーへの権限も一緒に付けてくれるので、地味にありがたい。

セキュリティグループも同じ発想で、instance.connections 経由で書けます。db.connections.allow_default_port_from(instance) みたいに書くと、SG 同士の参照ルールを勝手に作ってくれます。この「オブジェクト同士をつなぐと権限が生える」感じが、素の CloudFormation との一番大きな差だと個人的には思っています。

なお s3.Bucketremoval_policy はデフォルトが RETAIN です。cdk destroy してもバケットが残るので、検証用なら上のように DESTROY + auto_delete_objects=True にしておくと後片付けが楽です。本番でこれを書くと事故るので、そこは環境で分けるべきかなと。

L2 で足りないときの逃げ道(エスケープハッチ)

L2 はデフォルトが優秀な反面、「CloudFormation にはあるのに L2 のプロパティに無い」ということがたまにあります。そのときは L1 に降りられます。

cfn_bucket = bucket.node.default_child
cfn_bucket.add_override("Properties.AccelerateConfiguration.AccelerationStatus", "Enabled")

node.default_child で内側の L1 コンストラクトを取り出して、add_override()add_property_override() で直接いじる形です。型が IConstruct なので Cursor の補完が効かなくてちょっとイラッとしますが、キャストすれば OK。ここは正直、使う頻度が低くて毎回ググっています。

synth して差分を見る

cdk synth
cdk diff
cdk deploy

cdk synthcdk.out/ に CloudFormation テンプレートが出るので、L2 が実際に何を作っているのか一度は覗いてみるのがおすすめです。上の VPC 定義(20行くらい)が YAML だと 200 行以上になっていて、「これを手で書かなくていいのか」という気持ちになれます。

あと cdk diff は毎回打つ癖を付けたほうがよさそうです。CDK は書き方によって置換(Replacement)が発生することがあり、diff だと [~] ... replace のように出してくれます。CLI 側も進化していて、最近は cdk deploy--revert-drift でコンソールから手動変更されたリソース(特に hotswap が絡んだドリフト)を戻すオプションなんかも入っているようです。まだ試せていないですが、手作業で直しちゃった系のドリフト解消が1コマンドで済むならかなり嬉しい。

バージョンまわりのメモ

2026年8月時点だと、aws-cdk-lib と CDK CLI(aws-cdk

あと地味に大事なのが Python のバージョンで、aws-cdk-lib は Python 3.10 以上が必要になっています。3.9 を使っている環境だと入らないので、そこだけ先に確認しておくといいかもしれません。

まとめ

コンストラクトが scope と id を持つツリーで、L1 が生の CloudFormation、L2 がデフォルト付きの便利版、というのが飲み込めると、公式ドキュメントの API リファレンスがだいぶ読めるようになります。自分も最初は TypeScript のサンプルばかりでつらかったんですが、プロパティ名を snake_case に読み替えるだけで大体そのまま動くと気づいてからは、むしろ TS の記事のほうが情報量が多くて助かっています。

ところで、みなさん CDK のスタックってどのくらいの粒度で分けてるんでしょうか。ネットワーク/アプリ/データストアで3分割が定番っぽいですが、個人開発だと1スタックのままでもいい気がしていて、まだ答えが出ていません。

📚 シリーズ「AWS CDK × Python インフラ自動化入門」(第2回 / 全4回)

← 前回の記事: 前回の記事はこちら

→ 次回の記事: 【第3回】AWS CDK × Python インフラ自動化入門 — スタックの分割・環境ごとのデプロイ管理を実践する

参考になったらクリックしてもらえると嬉しいです!

Blogmura NetDev Ranking
タイトルとURLをコピーしました