AWSのインフラをCloudFormationで管理していると、
「テンプレートが肥大化して保守しづらい」「環境ごとに微妙に設定が違う」
――そんな悩みを感じたことはないでしょうか。
特にVPCは必ずと言っていいほど構築するので、
再利用可能なテンプレートがあるだけで運用効率が段違いです。
前回はネステッドスタックを使ったテンプレートの考え方を紹介しました。
本記事では、実務でそのまま使えるよう調整したテンプレートを公開します。
1. CloudFormationとは?
CloudFormationは「AWSリソースをテンプレートで定義して、まとめて管理できるサービス」です。
概念やテンプレート構造の基礎は、以下の記事で“たとえ”も交えて解説しています。
2. ネステッドスタックとは?
VPC、EC2、セキュリティグループ、IAMなどを責務ごとに別テンプレートへ分割し、
それらを親スタックが呼び出してまとめる仕組みが「ネステッドスタック(Nested Stack)」です。
詳細は以下を参照ください。
3. VPC構築テンプレート
以下に本記事で公開する VPC 構築テンプレートの概要を示します。
構成イメージ
Root Stack (network-root.yml) → Parameters と Outputs を集約
└── Child Stack (network-core.yml) → 実リソース作成
├── VPC
├── Public Subnet(s)
├── Private Subnet(s)
├── Internet Gateway
├── NAT Gateway (auto / force / never)
├── RouteTable + Route
└── S3 Gateway Endpoint
network-root.yml(親スタック)
AWSTemplateFormatVersion: 2010-09-09
Description: |
CloudFormation template with VPC, Subnets, IGW, NAT Gateway, S3 Endpoint, and CIDR validation
Parameters:
SystemCode: { Type: String, Description: "Unique identifier for the system" }
Environment: { Type: String, Description: "Deployment environment (e.g., dev, stg, prod)" }
ChildTemplateS3Url:
Type: String
Description: S3 URL of the child stack (beex-vpc-standard-network-core.yml)
AllowedPattern: '^https://.+\.amazonaws\.com/.+\.(yml|yaml|json)$'
ConstraintDescription: "Must be an HTTPS S3 URL to a .yml/.yaml/.json template."
VPCCidr:
Type: String
Description: CIDR block for the VPC
Default: 10.0.0.0/16
AllowedPattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'
ConstraintDescription: "CIDR block must be in the format 'x.x.x.x/x', where x is a number."
AzCount:
Type: Number
Default: 2
AllowedValues: [1, 2, 3]
Description: "Number of AZs to use (maximum 3)"
PublicSubnetCount:
Type: Number
Default: 2
AllowedValues: [0, 1, 2, 3]
Description: "Number of public subnets (recommended to be equal to or less than the number of availability zones)"
PrivateSubnetCount:
Type: Number
Default: 2
AllowedValues: [0, 1, 2, 3]
Description: "Number of private subnets (recommended to be equal to or less than the number of availability zones)"
CreateInternetGateway: { Type: String, Default: "true", AllowedValues: ["true","false"] }
CreateS3GatewayEndpoint: { Type: String, Default: "true", AllowedValues: ["true","false"] }
NatCreationPolicy:
Type: String
Default: auto
AllowedValues: [auto, force, never] # NAT生成方針(auto=同AZPrivate必須/force=強制/never=作成しない)
Description: |
auto = Create NAT only when the Private is in the same AZ |
force = Create NAT even without Private |
never = Do not create NAT
Conditions:
HasPublic: !Not [!Equals [!Ref PublicSubnetCount, 0]] # public-subnet を作らない場合、子の Outputs.PublicSubnetIds が存在しないため、!GetAtt が失敗する。Condition で回避するためのフラグ。
HasPrivate: !Not [!Equals [!Ref PrivateSubnetCount, 0]] # private-subnet を作らない場合、子の Outputs.PrivateSubnetIds が存在しないため、!GetAtt でエラーになる。Condition で回避するためのフラグ。
Metadata:
AWS::CloudFormation::Interface:
# パラメータの並び順
ParameterGroups:
- Label:
default: "Project Configuration"
Parameters:
- SystemCode
- Environment
- ChildTemplateS3Url
- VPCCidr
- AzCount
- PublicSubnetCount
- PrivateSubnetCount
- CreateInternetGateway
- CreateS3GatewayEndpoint
- NatCreationPolicy
Resources:
NetworkStack:
Type: AWS::CloudFormation::Stack
Properties:
TemplateURL: !Ref ChildTemplateS3Url
Parameters:
SystemCode: !Ref SystemCode
Environment: !Ref Environment
VPCCidr: !Ref VPCCidr
AzCount: !Ref AzCount
PublicSubnetCount: !Ref PublicSubnetCount
PrivateSubnetCount: !Ref PrivateSubnetCount
CreateInternetGateway: !Ref CreateInternetGateway
CreateS3GatewayEndpoint: !Ref CreateS3GatewayEndpoint
NatCreationPolicy: !Ref NatCreationPolicy
Outputs:
VpcId:
Description: "VPC ID"
Value: !GetAtt NetworkStack.Outputs.VpcId
Export:
Name: !Sub "${AWS::StackName}-vpc-id"
PublicSubnetIds:
Description: "Public Subnet IDs (CSV)"
Condition: HasPublic
Value: !GetAtt NetworkStack.Outputs.PublicSubnetIds
Export:
Name: !Sub "${AWS::StackName}-public-subnet-ids"
PrivateSubnetIds:
Description: "Private Subnet IDs (CSV)"
Condition: HasPrivate
Value: !GetAtt NetworkStack.Outputs.PrivateSubnetIds
Export:
Name: !Sub "${AWS::StackName}-private-subnet-ids"
親スタックの主なポイント
| セクション | 内容 |
|---|---|
| Parameters | 環境名・システムコード・子テンプレURL・VPC設定など |
| Conditions | Public/Private サブネットの有無を判定 |
| Resources | 子スタックをAWS::CloudFormation::Stackとして呼び出す |
| Outputs | 子スタックのOutputsをExport(空値回避条件付き) |
Outputs(親スタックでExport)
| Output名 | 内容 | Condition | Export名 |
|---|---|---|---|
VpcId |
作成されたVPCのID | 常に出力 | ${AWS::StackName}-vpc-id |
PublicSubnetIds |
PublicサブネットID(CSV) | Publicが1つ以上 | ${AWS::StackName}-public-subnet-ids |
PrivateSubnetIds |
PrivateサブネットID(CSV) | Privateが1つ以上 | ${AWS::StackName}-private-subnet-ids |
network-core.yml(子スタック)
AWSTemplateFormatVersion: 2010-09-09
Description: Network nested stack (VPC/Subnets/IGW/NAT/S3 GW Endpoint)
# Parent=parameter hub / Child=resource creation.
Parameters:
SystemCode: { Type: String }
Environment: { Type: String }
VPCCidr: { Type: String }
AzCount: { Type: Number, AllowedValues: [1,2,3] }
PublicSubnetCount: { Type: Number, AllowedValues: [0,1,2,3] }
PrivateSubnetCount: { Type: Number, AllowedValues: [0,1,2,3] }
CreateInternetGateway: { Type: String, AllowedValues: ["true","false"] }
NatCreationPolicy: { Type: String, AllowedValues: ["auto", "force", "never"] } # NAT生成方針(auto=同AZPrivate必須/force=強制/never=作成しない)
CreateS3GatewayEndpoint: { Type: String, AllowedValues: ["true","false"] }
Conditions:
# --- ベース条件 ---
UseIGW: !Equals [!Ref CreateInternetGateway, "true"]
UseS3GW: !Equals [!Ref CreateS3GatewayEndpoint, "true"]
NatPolicyNever: !Equals [!Ref NatCreationPolicy, "never"]
NatPolicyForce: !Equals [!Ref NatCreationPolicy, "force"]
NatPolicyAuto: !Equals [!Ref NatCreationPolicy, "auto"]
# --- AZ数条件 ---
AzAtLeast2: !Or [!Equals [!Ref AzCount, 2], !Equals [!Ref AzCount, 3]]
AzAtLeast3: !Equals [!Ref AzCount, 3]
# --- Public/Privateサブネット作成条件 ---
PubAtLeast1: !Not [!Equals [!Ref PublicSubnetCount, 0]]
PubAtLeast2: !Or [!Equals [!Ref PublicSubnetCount, 2], !Equals [!Ref PublicSubnetCount, 3]]
PubAtLeast3: !Equals [!Ref PublicSubnetCount, 3]
PriAtLeast1: !Not [!Equals [!Ref PrivateSubnetCount, 0]]
PriAtLeast2: !Or [!Equals [!Ref PrivateSubnetCount, 2], !Equals [!Ref PrivateSubnetCount, 3]]
PriAtLeast3: !Equals [!Ref PrivateSubnetCount, 3]
# --- サブネット別作成条件 ---
CreatePub1: !Condition PubAtLeast1
CreatePub2: !And [!Condition PubAtLeast2, !Condition AzAtLeast2]
CreatePub3: !And [!Condition PubAtLeast3, !Condition AzAtLeast3]
CreatePri1: !Condition PriAtLeast1
CreatePri2: !And [!Condition PriAtLeast2, !Condition AzAtLeast2]
CreatePri3: !And [!Condition PriAtLeast3, !Condition AzAtLeast3]
# --- サブネット作成条件 ---
UsePublic: !Or [!Condition CreatePub1, !Condition CreatePub2, !Condition CreatePub3]
UsePrivate: !Or [!Condition CreatePri1, !Condition CreatePri2, !Condition CreatePri3]
# --- NATをAZごとに有効化する条件 ---
# auto = 同AZにPrivate-Subnetが存在する場合のみNAT作成
# force = Private-Subnetの有無に関係なく作成
UseNat1: !And [!Not [!Condition NatPolicyNever], !Condition UseIGW, !Condition CreatePub1,
!Or [!Condition NatPolicyForce, !And [!Condition NatPolicyAuto, !Condition CreatePri1]]]
UseNat2: !And [!Not [!Condition NatPolicyNever], !Condition UseIGW, !Condition CreatePub2,
!Or [!Condition NatPolicyForce, !And [!Condition NatPolicyAuto, !Condition CreatePri2]]]
UseNat3: !And [!Not [!Condition NatPolicyNever], !Condition UseIGW, !Condition CreatePub3,
!Or [!Condition NatPolicyForce, !And [!Condition NatPolicyAuto, !Condition CreatePri3]]]
# --- ルートテーブル作成条件 ---
RoutePub: !And [!Condition UseIGW, !Condition UsePublic] # PublicRT存在時のみIGW経路追加
RoutePriWithS3GW: !And [!Condition UseS3GW, !Condition UsePrivate] # PrivateRTが存在する場合のみS3エンドポイント経路を追加
RoutePri1: !And [!Condition CreatePri1, !Condition UseNat1]
RoutePri2: !And [!Condition CreatePri2, !Condition UseNat2]
RoutePri3: !And [!Condition CreatePri3, !Condition UseNat3]
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCidr
EnableDnsSupport: true
EnableDnsHostnames: true
InternetGateway:
Condition: UseIGW
Type: AWS::EC2::InternetGateway
VPCGatewayAttachment:
Condition: UseIGW
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# ---------- Public ----------
PublicSubnet1:
Condition: CreatePub1
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !GetAZs ""]
CidrBlock: !Select [0, !Cidr [!Ref VPCCidr, 6, 8]]
MapPublicIpOnLaunch: true
PublicSubnet2:
Condition: CreatePub2
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [1, !GetAZs ""]
CidrBlock: !Select [1, !Cidr [!Ref VPCCidr, 6, 8]]
MapPublicIpOnLaunch: true
PublicSubnet3:
Condition: CreatePub3
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [2, !GetAZs ""]
CidrBlock: !Select [2, !Cidr [!Ref VPCCidr, 6, 8]]
MapPublicIpOnLaunch: true
PublicRouteTable:
Condition: UsePublic
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PublicDefaultRoute:
Condition: RoutePub # IGW接続かつPublicRTが存在する場合のみ作成
Type: AWS::EC2::Route
DependsOn: VPCGatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
PublicSubnet1RouteTableAssociation:
Condition: CreatePub1
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable
PublicSubnet2RouteTableAssociation:
Condition: CreatePub2
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable
PublicSubnet3RouteTableAssociation:
Condition: CreatePub3
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet3
RouteTableId: !Ref PublicRouteTable
# ---------- Private ----------
PrivateSubnet1:
Condition: CreatePri1
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !GetAZs ""]
CidrBlock: !Select [3, !Cidr [!Ref VPCCidr, 6, 8]]
MapPublicIpOnLaunch: false
PrivateSubnet2:
Condition: CreatePri2
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [1, !GetAZs ""]
CidrBlock: !Select [4, !Cidr [!Ref VPCCidr, 6, 8]]
MapPublicIpOnLaunch: false
PrivateSubnet3:
Condition: CreatePri3
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [2, !GetAZs ""]
CidrBlock: !Select [5, !Cidr [!Ref VPCCidr, 6, 8]]
MapPublicIpOnLaunch: false
PrivateRT1:
Condition: CreatePri1
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PrivateRT2:
Condition: CreatePri2
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
PrivateRT3:
Condition: CreatePri3
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
AssocPri1:
Condition: CreatePri1
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRT1
AssocPri2:
Condition: CreatePri2
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRT2
AssocPri3:
Condition: CreatePri3
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet3
RouteTableId: !Ref PrivateRT3
# ---------- NAT(AZごとに設置) ----------
# AZ1
NatEip1:
Condition: UseNat1
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NatGateway1:
Condition: UseNat1
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEip1.AllocationId
SubnetId: !Ref PublicSubnet1
# AZ2
NatEip2:
Condition: UseNat2
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NatGateway2:
Condition: UseNat2
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEip2.AllocationId
SubnetId: !Ref PublicSubnet2
# AZ3
NatEip3:
Condition: UseNat3
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NatGateway3:
Condition: UseNat3
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEip3.AllocationId
SubnetId: !Ref PublicSubnet3
# ---------- Private RT のデフォルトルート(同一AZの NAT を参照) ----------
Pri1DefaultToNat:
Condition: RoutePri1
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRT1
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway1
Pri2DefaultToNat:
Condition: RoutePri2
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRT2
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway2
Pri3DefaultToNat:
Condition: RoutePri3
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRT3
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway3
# ---------- S3 Gateway Endpoint ----------
# PrivateRTが1つ以上存在する場合のみ作成
S3GatewayEndpoint:
Condition: RoutePriWithS3GW
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3"
VpcEndpointType: Gateway
RouteTableIds:
- !If [CreatePri1, !Ref PrivateRT1, !Ref "AWS::NoValue"]
- !If [CreatePri2, !Ref PrivateRT2, !Ref "AWS::NoValue"]
- !If [CreatePri3, !Ref PrivateRT3, !Ref "AWS::NoValue"]
Outputs:
VpcId: { Value: !Ref VPC }
PublicSubnetIds:
Condition: UsePublic # Publicサブネット未作成時に空Exportを防ぐ
Value: !Join
- ","
- [
!If [CreatePub1, !Ref PublicSubnet1, !Ref "AWS::NoValue"],
!If [CreatePub2, !Ref PublicSubnet2, !Ref "AWS::NoValue"],
!If [CreatePub3, !Ref PublicSubnet3, !Ref "AWS::NoValue"]
]
PrivateSubnetIds:
Condition: UsePrivate # Privateサブネット未作成時に空Exportを防ぐ
Value: !Join
- ","
- [
!If [CreatePri1, !Ref PrivateSubnet1, !Ref "AWS::NoValue"],
!If [CreatePri2, !Ref PrivateSubnet2, !Ref "AWS::NoValue"],
!If [CreatePri3, !Ref PrivateSubnet3, !Ref "AWS::NoValue"]
]
子スタックの主なポイント
| リソース種別 | 条件式 | 説明 |
|---|---|---|
| VPC | 常に作成 | ベースVPC |
| InternetGateway / VPCGatewayAttachment |
CreateInternetGatewayがtrue |
インターネット接続用 |
| Public Subnet (0〜3) | PublicSubnetCount |
AZごとに可変 |
| Private Subnet (0〜3) | PrivateSubnetCount |
AZごとに可変 |
| NAT Gateway |
NatCreationPolicyと AZ の有無で制御 |
コスト注意 |
| RouteTable / Route | Public/Privateそれぞれに紐づけ | |
| S3 Gateway Endpoint |
CreateS3GatewayEndpointがtrue |
Private ルートテーブルに関連付け |
NatCreationPolicyの動的制御について
以下の3パターンでNAT作成の制御を実施しています。
| 値 | 意味 | 作成条件 | 方針 |
|---|---|---|---|
auto |
同一AZにPrivateがある場合のみ作成 | Public + Private + IGW | Public + Private + NAT + IGWの構成が欲しいとき |
force |
Privateの有無に関係なく作成 | Public + IGW | Privateを後から足す |
never |
常に作成しない | - | NATが不要なとき |
コスト注意:NAT Gateway は時間単価+データ処理課金が発生します。検証時は auto/never の使い分け推奨。
パラメータの入力値によって作成される構成イメージ
パターン1(pub2 pri2 IGW Endpoint NAT)
| PublicSubnetCount | PrivateSubnetCount | IGW | S3Gateway | NatCreationPolicy |
|---|---|---|---|---|
| 2 | 2 | true | true | auto |
パターン2(pub2 pri0 IGW Endpoint NAT)
| PublicSubnetCount | PrivateSubnetCount | IGW | S3Gateway | NatCreationPolicy |
|---|---|---|---|---|
| 2 | 0 | true | true | force |
パターン3(pub0 pri2 Endpoint)
| PublicSubnetCount | PrivateSubnetCount | IGW | S3Gateway | NatCreationPolicy |
|---|---|---|---|---|
| 0 | 2 | false | true | never |
4. デプロイ手順(手動)
では、実際に作成したCloudFormationをマネジメントコンソールからデプロイしてみましょう。
事前準備として、前回作成したS3バケットを作成するCloudFormationテンプレートを活用して、親子スタックをアップロードするS3バケットを作成します。
詳細については前回の記事を参照してください。
マネジメントコンソールにアクセスし、S3を選択し対象のバケットに先ほど作成した以下のファイルをアップロードします。
- network-root.yaml
- network-core.yaml
親子テンプレートそれぞれのオブジェクトURLをコピーしておきます。
マネジメントコンソールにアクセスし、CloudFormationを選択し「スタックの作成」を押下します。

テンプレートソースは「Amazon S3 URL」を選択し、先ほどコピーした親スタックのオブジェクトURLを入力します。
スタック名とパラメータ(先ほどコピーした子スタックのオブジェクトURL)を入力して、「次へ」を押下します。
画像内の設定値は本記事内のパターン1に該当します。
設定はそのままで、ページ下部の機能欄に表示されているチェックボックスにチェックを入れて、「次へ」を押下します。
設定内容を確認し、「送信」を押下することで、CloudFormationの作成が始まります。
※最近タイムラインビューが表示されるようになり、リソースの作成状況がわかりやすくなりました。
ステータスが「CREATE_COMPLETE」になり、マネジメントコンソールから、VPCに行くと
指定したVPCやサブネット、NATGateway等が作成されていました。
5. リソースの変更(おすすめの使い方)
このテンプレートは、視覚的に試行錯誤しながら検証できるよう、あえてコンソールデプロイを前提にしています。
そのため、パラメータ変更だけでリソースの追加/削除が柔軟に切り替え可能です。
例:NAT Gateway不要なケース(パターン3)
- PrivateSubnetCount : 2 -> 0
- CreateInternetGateway : true -> false
- NatCreationPolicy : auto -> never
先ほど作成したスタックを選択し、「スタックを更新」-> 「直接更新を実行」を選択します。

「既存のテンプレートを使用」を選択し、「次へ」を押下します。
パラメータを変更し、「次へ」を押下します。
※先ほどと同様にチェックボックスと確認画面が表示されるのでそのまま「送信」を押下します。
更新後、VPCコンソールで Public Subnet / NAT Gateway / Internet Gateway が削除されていることを確認できます。
6. まとめ
- ネステッドスタックでテンプレートを分割・再利用できる
- Parameters/Conditions/Outputs を設計すると、環境差分に強いテンプレートになる
- コンソール前提でもパラメータ更新だけで構成変更が可能(検証に最適)
この記事のテンプレートは、S3にアップロードすればすぐ使えます。
必要に応じて GitHub にも配置して、更新履歴を管理するのがおすすめです。












