ひとつ前の記事で「AWS Tools for PowerShell を使ってコマンドラインで VPC を作る」と宣言し、環境設定まで終わったので、今回は実際に構築していきます。CloudFormation を使った VPC 構築は巷にいろいろ情報がありますが、同じことをわざわざ PowerShell でやろうという記事はあまり見かけません。やりがいがありますね!
検証済の動作環境
OS | モジュール |
---|---|
Windows 10 Pro 2004 | Windows PowerShell 5.1 (.NET Framework 4.8) |
動作条件
一口に VPC を構築するといっても、やることはいろいろあります。ざっくり以下のような感じでしょうか。
以下が全体像です。CIDR は今回「10.0.0.0/16」とします。
Set-DefaultAWSRegion -Region "ap-northeast-1" -Scope Script
Import-Module -Name AWS.Tools.EC2
# ------------------------------------
# Name タグ設定用の関数
# ------------------------------------
Function New-EC2NameTag
{
[OutputType([Object])]
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string]$ResourceType,
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string]$TagValue
)
$Tag = @{ Key="Name"; Value=$TagValue }
$Obj = New-Object -TypeName Amazon.EC2.Model.TagSpecification
$Obj.ResourceType = $ResourceType
$Obj.Tags.Add($Tag)
return $Obj
}
# ------------------------------------
# フィルタ作成用の関数
# ------------------------------------
Function New-EC2Filter
{
[OutputType([Object])]
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string]$Name,
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string[]]$Values
)
$Obj = New-Object -TypeName Amazon.EC2.Model.Filter
$Obj.Name = $Name
$Obj.Values = @($Values)
return $Obj
}
# ------------------------------------
# VPC の作成
# ------------------------------------
$CidrBlock = "10.0.0.0/16"
$VpcName = "vpc-01"
$VpcTag = New-EC2NameTag -ResourceType "vpc" -TagValue $VpcName
$VpcTagFilter = New-EC2Filter -Name "tag:Name" -Values $VpcName
If( -not (Get-EC2Vpc -Filter $VpcTagFilter))
{
$TargetVpc = New-EC2Vpc -CidrBlock $CidrBlock -InstanceTenancy default -TagSpecification $VpcTag
$TargetVpcId = $TargetVpc.VpcId
}
Else
{
throw "${VpcName}: すでに存在する名前です。"
}
Edit-EC2VpcAttribute -VpcId $TargetVpcId -EnableDnsSupport $True
Edit-EC2VpcAttribute -VpcId $TargetVpcId -EnableDnsHostname $True
# ------------------------------------
# サブネットの作成
# ------------------------------------
$Subnets = [ordered]@{
"subnet-pub-a" = "10.0.0.0/24";
"subnet-pub-c" = "10.0.1.0/24";
"subnet-pri-a" = "10.0.2.0/24";
"subnet-pri-c" = "10.0.3.0/24";
}
$SubnetTag = New-Object -TypeName Amazon.EC2.Model.TagSpecification
$SubnetTag.ResourceType = "subnet"
ForEach($Subnet In $Subnets.GetEnumerator())
{
$SubnetTagFilter = New-EC2Filter -Name "tag:Name" -Values $Subnet.Key
If( -not (Get-EC2Subnet -Filter $SubnetTagFilter))
{
$Tag = @{ Key="Name"; Value=$Subnet.Key }
$SubnetTag.Tags.Add($Tag)
If($Subnet.Key -match ".*-a$")
{
$AvailabilityZone = "ap-northeast-1a"
}
ElseIf($Subnet.Key -match ".*-c$")
{
$AvailabilityZone = "ap-northeast-1c"
}
Else
{
throw "$($Subnet.Key): サブネット名が条件外です。"
}
$SubnetParams = @{
AvailabilityZone = $AvailabilityZone
CidrBlock = $Subnet.Value
VpcId = $TargetVpcId
TagSpecification = $SubnetTag
}
New-EC2Subnet @SubnetParams
$SubnetTag.Tags.Clear()
}
Else
{
throw "$($Subnet.Key): すでに存在する名前です。"
}
}
# ------------------------------------
# インターネットゲートウェイの作成
# ------------------------------------
$IgwName = "Igw-01"
$IgwTag = New-EC2NameTag -ResourceType "internet-gateway" -TagValue $IgwName
$IgwTagFilter = New-EC2Filter -Name "tag:Name" -Values $IgwName
If( -not (Get-EC2InternetGateway -Filter $IgwTagFilter))
{
$TargetIgw = New-EC2InternetGateway -TagSpecification $IgwTag
$TargetIgwId = $TargetIgw.InternetGatewayId
}
Else
{
throw "${IgwName}: すでに存在する名前です。"
}
Add-EC2InternetGateway -VpcId $TargetVpcId -InternetGatewayId $TargetIgwId
# ------------------------------------
# 仮想プライベートゲートウェイの作成
# ------------------------------------
$VgwName = "vgw-01"
$VgwTag = New-EC2NameTag -ResourceType "vpn-gateway" -TagValue $VgwName
$IgwTagFilter = New-EC2Filter -Name "tag:Name" -Values $VgwName
If( -not (Get-EC2VpnGateway -Filter $VgwTagFilter))
{
$TargetVgw = New-EC2VpnGateway -Type ipsec.1 -TagSpecification $VgwTag
$TargetVgwId = $TargetVgw.VpnGatewayId
}
Else
{
throw "${VgwName}: すでに存在する名前です。"
}
Add-EC2VpnGateway -VpcId $TargetVpcIdId -VpnGatewayId $TargetVgwId
# ------------------------------------
# ルートテーブルの作成と設定
# ------------------------------------
$RtbNames = @("rtb-public-01", "rtb-private-01")
$VpcIdFilter = New-EC2Filter -Name "vpc-id" -Values $TargetVpcId
ForEach($RtbName In $RtbNames)
{
$RtbTag = New-EC2NameTag -ResourceType "route-table" -TagValue $RtbName
$RtbTagFilter = New-EC2Filter -Name "tag:Name" -Values $RtbName
If( -not (Get-EC2RouteTable -Filter $RtbTagFilter))
{
$TargetRtb = New-EC2RouteTable -VpcId $TargetVpcId -TagSpecification $RtbTag
$TargetRtbId = $TargetRtb.RouteTableId
}
Else
{
throw "${RtbName}: すでに存在する名前です。"
}
$PubSwitch = "*pub*"
$PriSwitch = "*pri*"
If($RtbName -like $PubSwitch)
{
New-EC2Route -RouteTableId $TargetRtbId -GatewayId $TargetIgwId -DestinationCidrBlock "0.0.0.0/0"
$SwitchTagFilter = New-EC2Filter -Name "tag:Name" -Values $PubSwitch
$PublicRtbId = $TargetRtbId
}
ElseIf($RtbName -like $PriSwitch)
{
New-EC2Route -RouteTableId $TargetRtbId -GatewayId $TargetVgwId -DestinationCidrBlock "0.0.0.0/0"
$SwitchTagFilter = New-EC2Filter -Name "tag:Name" -Values $PriSwitch
$PrivateRtbId = $TargetRtbId
}
Else
{
throw "${RtbName}: ルートテーブル名が条件外です。"
}
$TargetSubnets = (Get-EC2Subnet -Filter $VpcIdFilter, $SwitchTagFilter).SubnetId
ForEach($TargetSubnet In $TargetSubnets)
{
Register-EC2RouteTable -RouteTableId $TargetRtbId -SubnetId $TargetSubnet
}
}
$OldMainRtbAssoc = (Get-EC2RouteTable -Filter $VpcIdFilter).Associations | Where-Object -FilterScript { $_.Main }
$OldMainRtbId = $OldMainRtbAssoc.RouteTableId
$AssociationId = $OldMainRtbAssoc.RouteTableAssociationId
Set-EC2RouteTableAssociation -AssociationId $AssociationId -RouteTableId $PublicRtbId
Remove-EC2RouteTable -RouteTableId $OldMainRtbId -Force
# ------------------------------------
# S3 への VPC エンドポイントの作成
# ------------------------------------
$EndPointName = "vpce-s3-get-all"
$EndPointTag = New-EC2NameTag -ResourceType "vpc-endpoint" -TagValue $EndPointName
$EndPointFilter = New-EC2Filter -Name "tag:Name" -Values $EndPointName
$ServiceName = "com.amazonaws.ap-northeast-1.s3"
$PolicyDocument = @'
{
"Version": "2008-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "*"
}
]
}
'@
If( -not (Get-EC2VpcEndpoint -Filter $EndPointFilter))
{
$EndPointParams = @{
ServiceName = $ServiceName
VpcId = $TargetVpcId
RouteTableId = $PublicRtbId, $PrivateRtbId
PolicyDocument = $PolicyDocument
TagSpecification = $EndPointTag
}
New-EC2VpcEndpoint @EndPointParams
}
Else
{
throw "${EndPointName}: すでに存在する名前です。"
}
ひとつずつ見ていきます。まず、スクリプト内でのみ有効なデフォルトリージョンを設定します。
Set-DefaultAWSRegion -Region "ap-northeast-1" -Scope Script
今回使うモジュールを読み込んでおきます。(環境によってはいらないかもしれません)
Import-Module -Name AWS.Tools.EC2
VPC を構成する各コンポーネントを作成する際、同時にタグ付けまでしたい場合は-TagSpecification
パラメータにAmazon.EC2.Model.TagSpecification
オブジェクトを渡す必要があります。各コンポーネントを操作する際、ID がわからない場合は Name タグから判定することになるので、非常に重要です。これは何度も出てくるので、関数にしておきましょう。名前はNew-EC2NameTag
とでもしておきます。
Function New-EC2NameTag
{
[OutputType([Object])]
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string]$ResourceType,
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string]$TagValue
)
$Tag = @{ Key="Name"; Value=$TagValue }
$Obj = New-Object -TypeName Amazon.EC2.Model.TagSpecification
$Obj.ResourceType = $ResourceType
$Obj.Tags.Add($Tag)
return $Obj
}
Amazon.EC2.Model.TagSpecification
は以下のように、単一のResourceType
と複数のTags
を持った構造になっています。
PS C:\WINDOWS\system32> New-Object -TypeName Amazon.EC2.Model.TagSpecification
ResourceType Tags
------------ ----
{}
まず Name タグを指定するための連想配列を定義し、
$Tag = @{ Key="Name"; Value=$TagValue }
オブジェクトを初期化し、
$Obj = New-Object -TypeName Amazon.EC2.Model.TagSpecification
単一の値をとるResourceType
に文字列を格納し、
$Obj.ResourceType = $ResourceType
複数の連想配列を格納できるTags
にさきほど作った$Tag
をAdd
します。
$Obj.Tags.Add($Tag)
ResourceType
が取りうる値の一覧についてはこちらを参照してください。余談ですが、[OutputType()]
は付ける癖をつけたほうがいいですね。私はこちらを読むまで付けていませんでした。
もうひとつ関数を作ります。コマンド結果をフィルタするためのAmazon.EC2.Model.Filter
オブジェクトを扱う関数です。これはAmazon.EC2.Model.TagSpecification
の次によく使います。ワンライナーで書けるので必ずしも関数にしなくていいのですが、今回は記述が冗長になるのを避けるために関数にしました。
Function New-EC2Filter
{
[OutputType([Object])]
[CmdletBinding()]
Param
(
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string]$Name,
[Parameter(Mandatory=$True)]
[ValidateNotNullOrEmpty()]
[string[]]$Values
)
$Obj = New-Object -TypeName Amazon.EC2.Model.Filter
$Obj.Name = $Name
$Obj.Values = @($Values)
return $Obj
}
単一のName
に対して、Values
は配列です。例えば以下のような形になります。
PS C:\WINDOWS\system32> New-EC2Filter -Name "tag:Name" -Values "test-01", "test-02", "test-03"
Name Values
---- ------
tag:Name {test-01, test-02, test-03}
ちなみにオブジェクトの構造を確認したい場合は、いっそ json に変換したほうが一目瞭然で把握できていいかもしれません。インデントが微妙ですが。
PS C:\WINDOWS\system32> New-EC2Filter -Name "tag:Name" -Values "test-01", "test-02", "test-03" | ConvertTo-Json
{
"Name": "tag:Name",
"Values": [
"test-01",
"test-02",
"test-03"
]
}
基盤となる VPC を作成します。まずはさきほど作ったNew-EC2NameTag
関数を使い、New-EC2Vpc
コマンドに渡すためのタグ情報を定義します。
$VpcName = "vpc-01"
$VpcTag = New-EC2NameTag -ResourceType "vpc" -TagValue $VpcName
中身はこんな感じになっています。
PS C:\WINDOWS\system32> $VpcTag | ConvertTo-Json
{
"ResourceType": {
"Value": "vpc"
},
"Tags": [
{
"Key": "Name",
"Value": "vpc-01"
}
]
}
続いてNew-EC2Filter
関数でフィルタを作ります。
$VpcName = "vpc-01"
・・・
$VpcTagFilter = New-EC2Filter -Name "tag:Name" -Values $VpcName
ネタがそろったので、VPC を起動します。ユニークな Name タグでのリソース管理を想定しているので、$VpcTagFilter
を使って、すでに同じタグ名が存在していたらエラーをthrow
して終了するようにしました。VPC、サブネット、ゲートウェイ、ルートテーブルと、New-EC2*
コマンドでコンポーネントを作るときは基本的にこの形で処理するようにしています。なお、コンポーネントを作成した後にそのコンポーネントを編集したい場合は採番されたばかりの ID が必要なので、実行結果を変数に格納して後段で利用できるようにしておきます。
$CidrBlock = "10.0.0.0/16"
・・・
If( -not (Get-EC2Vpc -Filter $VpcTagFilter))
{
$TargetVpc = New-EC2Vpc -CidrBlock $CidrBlock -InstanceTenancy default -TagSpecification $VpcTag
$TargetVpcId = $TargetVpc.VpcId
}
Else
{
throw "${VpcName}: すでに存在する名前です。"
}
起動した VPC に対して DNS サポートに関する設定を入れていきます。これは公式ドキュメントに詳しく書かれています。取り扱う属性について噛み砕くと、ざっくりこんな感じかと思います。
属性 | ざっくり説明 |
---|---|
EnableDnsSupport | Amazon が提供する DNS サーバー (Amazon Route 53 Resolver) を使うかどうか |
EnableDnsHostnames | パブリック IP アドレスを持つインスタンスがパブリック DNS ホスト名を取得するかどうか |
ドキュメントの日本語がよくわからないのはさておき、以下引用の通り、AWS Tools for PowerShell で作った場合はEnableDnsSupport
がTrue
かつEnableDnsHostname
がFalse
の状態で作成されます。DNS を適切に設定したい場合はどちらも有効にする必要があります。
デフォルトでは、両方の属性がデフォルト VPC で true に設定されるか、VPC が VPC ウィザードによって作成されます。デフォルトでは、その他の方法で作成された VPC で、enableDnsSupport 属性のみが true に設定されます。
直前に作成した VPC に対して以下2つのコマンドを実行します。各パラメータに$True
を渡すことで有効化します。
# DNS 解決を有効化 ( デフォルト値が True なので、必要に応じて )
Edit-EC2VpcAttribute -VpcId $TargetVpcId -EnableDnsSupport $True
# DNS ホスト名を有効化
Edit-EC2VpcAttribute -VpcId $TargetVpcId -EnableDnsHostname $True
基盤となる VPC に対してサブネットを切っていきます。要件は以下とします。
サブネット名 | CIDR | Public/Private | AvailabilityZone |
---|---|---|---|
subnet-pub-a | 10.0.0.0/24 | Public | ap-northeast-1a |
subnet-pub-c | 10.0.1.0/24 | Public | ap-northeast-1c |
subnet-pri-a | 10.0.2.0/24 | Private | ap-northeast-1a |
subnet-pri-c | 10.0.3.0/24 | Private | ap-northeast-1c |
いらないかもしれませんが、基本事項を書いておきます。ざっくりですが、Public/Private に関しては以下の通りです。
種類 | 説明 |
---|---|
パブリックサブネット | ルートテーブルにインターネットゲートウェイへのルートが含まれているサブネット |
プライベートサブネット | ルートテーブルにインターネットゲートウェイへのルートが含まれていないサブネット |
AvailabilityZone とは、こちらもざっくり言ってしまうと、同じリージョン内で複数ある独立したデータセンターのことです。冗長化の際に重要です。公式のナレッジに記載があるのですが、同じゾーン名でも異なる AWS アカウント間では必ずしも同じゾーンを意味しないということがあるようです。また、こんな記事も見かけました。奥が深いですね。
これら4つのサブネットを作成します。まず サブネット名 =CIDR の形で連想配列を用意します。
$Subnets = [ordered]@{
"subnet-pub-a" = "10.0.0.0/24";
"subnet-pub-c" = "10.0.1.0/24";
"subnet-pri-a" = "10.0.2.0/24";
"subnet-pri-c" = "10.0.3.0/24";
}
次に各サブネットに渡す Name タグのResourceType
だけを定義したオブジェクトを用意します。
$SubnetTag = New-Object -TypeName Amazon.EC2.Model.TagSpecification
$SubnetTag.ResourceType = "subnet"
連想配列をループ処理します。VPC 作成の時と同様に、Name タグの重複確認も組み込みます。
ForEach($Subnet In $Subnets.GetEnumerator())
{
$SubnetTagFilter = New-EC2Filter -Name "tag:Name" -Values $Subnet.Key
If( -not (Get-EC2Subnet -Filter $SubnetTagFilter))
{
# 実際の処理
}
Else
{
throw "$($Subnet.Key): すでに存在する名前です。"
}
}
各ループごとに、Name タグを設定するための連想配列を作って$Tag
に格納し、冒頭で定義した$SubnetTag.Tags
にAdd
します。
$Tag = @{ Key="Name"; Value=$Subnet.Key }
$SubnetTag.Tags.Add($Tag)
正規表現を使ってアベイラリティゾーンを判定します。AZ-A でも AZ-C でもない場合はエラーを投げて終了します。
If($Subnet.Key -match ".*-a$")
{
$AvailabilityZone = "ap-northeast-1a"
}
ElseIf($Subnet.Key -match ".*-c$")
{
$AvailabilityZone = "ap-northeast-1c"
}
Else
{
throw "$($Subnet.Key): サブネット名が条件外です。"
}
ネタがそろったのでNew-EC2Subnet
します。パラメータがたくさんあって横に見切れる場合は可読性を上げるためにスプラッティングするようにしています。また、Name タグを設定するためのオブジェクトはループの外で作ったものを再利用するので、Tags
の中身は次のループに行く前にクリアしておきます。
$SubnetParams = @{
AvailabilityZone = $AvailabilityZone
CidrBlock = $Subnet.Value
VpcId = $TargetVpcId
TagSpecification = $SubnetTag
}
New-EC2Subnet @SubnetParams
$SubnetTag.Tags.Clear()
考え方は一緒です。タグとフィルタを定義し、
$IgwName = "igw-01"
$IgwTag = New-EC2NameTag -ResourceType "internet-gateway" -TagValue $IgwName
$IgwTagFilter = New-EC2Filter -Name "tag:Name" -Values $IgwName
同一の Name タグがない場合だけ処理します。コマンドが違うだけです。コマンド結果を変数に格納しておきます。
If( -not (Get-EC2InternetGateway -Filter $IgwTagFilter))
{
$TargetIgw = New-EC2InternetGateway -TagSpecification $IgwTag
$TargetIgwId = $TargetIgw.InternetGatewayId
}
Else
{
throw "${IgwName}: すでに存在する名前です。"
}
VpcId
もInternetGatewayId
もわかっているので、VPC にインターネットゲートウェイを紐づけます。
Add-EC2InternetGateway -VpcId $TargetVpcId -InternetGatewayId $TargetIgwId
仮想プライベートゲートウェイは、別のネットワークと VPC を VPN 接続するための、VPC 側のコンポーネントです。今回の目的では必ずしも必要ではありませんが、ゆくゆくは自宅 LAN との接続も検討したいため、アタッチしておきます。やることはインターネットゲートウェイとまったく同じなので説明は割愛します。
$VgwName = "vgw-01"
$VgwTag = New-EC2NameTag -ResourceType "vpn-gateway" -TagValue $VgwName
$IgwTagFilter = New-EC2Filter -Name "tag:Name" -Values $VgwName
If( -not (Get-EC2VpnGateway -Filter $VgwTagFilter))
{
$TargetVgw = New-EC2VpnGateway -Type ipsec.1 -TagSpecification $VgwTag
$TargetVgwId = $TargetVgw.VpnGatewayId
}
Else
{
throw "${VgwName}: すでに存在する名前です。"
}
Add-EC2VpnGateway -VpcId $TargetVpcIdId -VpnGatewayId $TargetVgwId
ルートテーブルを作成して必要な設定を入れていきます。流れとしては以下です。
Name タグに入れるためのルートテーブル名を配列で用意します。
$RtbNames = @("rtb-public-01", "rtb-private-01")
ルートテーブルとサブネットの関連付けで、紐づく VPC の判定が必要なので、ID で判定するフィルタを用意します。
$VpcIdFilter = New-EC2Filter -Name "vpc-id" -Values $TargetVpcId
ループ処理ですが、やることはいろいろあります。まず例によってタグとフィルタを作ります。
$RtbTag = New-EC2NameTag -ResourceType "route-table" -TagValue $RtbName
$RtbTagFilter = New-EC2Filter -Name "tag:Name" -Values $RtbName
いままでと同じ制御でNew-EC2RouteTable
します。
If( -not (Get-EC2RouteTable -Filter $RtbTagFilter))
{
$TargetRtb = New-EC2RouteTable -VpcId $TargetVpcId -TagSpecification $RtbTag
$TargetRtbId = $TargetRtb.RouteTableId
}
Else
{
throw "${RtbName}: すでに存在する名前です。"
}
作成したルートテーブルに対してルートを追加します。Public/Private で処理内容が微妙に異なるので、ルートテーブル名で処理を分岐させます。Public なら-GatewayId
パラメータにインターネットゲートウェイを指定してNew-EC2Route
します。Private なら-GatewayId
パラメータに仮想プライベートゲートウェイを指定してNew-EC2Route
します。次の処理にそなえ、フィルタもそれぞれ作っておきます。またルートテーブル ID はあとで使うので、Public/Private でそれぞれ別の変数に格納しておきます。
$PubSwitch = "*pub*"
$PriSwitch = "*pri*"
If($RtbName -like $PubSwitch)
{
New-EC2Route -RouteTableId $TargetRtbId -GatewayId $TargetIgwId -DestinationCidrBlock "0.0.0.0/0"
$SwitchTagFilter = New-EC2Filter -Name "tag:Name" -Values $PubSwitch
$PublicRtbId = $TargetRtbId
}
ElseIf($RtbName -like $PriSwitch)
{
New-EC2Route -RouteTableId $TargetRtbId -GatewayId $TargetVgwId -DestinationCidrBlock "0.0.0.0/0"
$SwitchTagFilter = New-EC2Filter -Name "tag:Name" -Values $PriSwitch
$PrivateRtbId = $TargetRtbId
}
Else
{
throw "${RtbName}: ルートテーブル名が条件外です。"
}
ルートテーブルにサブネットを関連付けます。冒頭で作った$VpcIdFilter
とループ内で作った$SwitchTagFilter
を組み合わせ、関連付けるサブネットを特定します。このフィルタの組み合わせは「今回の処理で作成した VPC に紐づくサブネット、かつ Public 用のサブネット」または「今回の処理で作成した VPC に紐づくサブネット、かつ Private 用のサブネット」という条件になります。
関連付けるサブネットが特定できたので、$TargetSubnets
に配列として格納します。この中身をひとつずつ取り出してRegister-EC2RouteTable
することでルートテーブルにサブネットを関連付けます。
$TargetSubnets = (Get-EC2Subnet -Filter $VpcIdFilter, $SwitchTagFilter).SubnetId
ForEach($TargetSubnet In $TargetSubnets)
{
Register-EC2RouteTable -RouteTableId $TargetRtbId -SubnetId $TargetSubnet
}
ルートテーブル作成の後処理ですが、いちばん苦戦した部分になります。メインルートテーブルの置き換えと、置き換えが終わったあとの不要ルートテーブルの削除を行います。VPC を作成すると自動でメインルートテーブルが作成されますが、今回はすべてスクリプトで作ったあとにメインルートテーブルを置き換える方法を取りました。メインルートテーブルは基本的にパブリック側に設定するようなので、そのようにします。
メインルートテーブルを置き換えるにはSet-EC2RouteTableAssociation
コマンドを使いますが、そのためには必須パラメータ-AssociationId
に渡す値をなんらかの方法で取得する必要があります。試しにルートテーブルのオブジェクト構造を見てみると、Associations
というメンバがいることがわかります。
PS C:\WINDOWS\system32> Get-EC2RouteTable -RouteTableId "rtb-XXXXXXXXXXXXXXXXX" | Get-Member
TypeName: Amazon.EC2.Model.RouteTable
Name MemberType Definition
---- ---------- ----------
・・・
Associations Property System.Collections.Generic.List[Amazon.EC2.Model.RouteTableAssociation] Associations {get;set;}
・・・
さらにAssociations
の中身を見てみましょう。
PS C:\WINDOWS\system32> Get-EC2RouteTable -RouteTableId "rtb-XXXXXXXXXXXXXXXXX" | Select-Object -ExpandProperty Associations | Get-Member
TypeName: Amazon.EC2.Model.RouteTableAssociation
Name MemberType Definition
---- ---------- ----------
・・・
AssociationState Property Amazon.EC2.Model.RouteTableAssociationState AssociationState {get;set;}
GatewayId Property string GatewayId {get;set;}
Main Property bool Main {get;set;}
RouteTableAssociationId Property string RouteTableAssociationId {get;set;}
RouteTableId Property string RouteTableId {get;set;}
SubnetId Property string SubnetId {get;set;}
このAssociations
とは、ルートテーブル、サブネット、インターネットゲートウェイ、または仮想プライベートゲートウェイの間の関連付けを司るオブジェクトで、メインルートテーブルであるかどうかを司るMain
というメンバも含まれています。またSet-EC2RouteTableAssociation
コマンドの必須パラメータである-AssociationId
には、この中にあるRouteTableAssociationId
というメンバを指定する必要があります。
オブジェクト構造がわかったところで、メインルートテーブルを置き換えるためのネタをそろえましょう。まずは現在のメインルートテーブルの関連付け情報を取得します。$VpcIdFilter
でルートテーブルを絞り込み、プロパティAssociations
を取得します。その中でMain
が$True
となっているAssociations
をWhere-Object -FilterScript { $_.Main }
で特定します。特定出来たらあとは必要なプロパティを変数に入れるだけです。
$OldMainRtbAssoc = (Get-EC2RouteTable -Filter $VpcIdFilter).Associations | Where-Object -FilterScript { $_.Main }
$OldMainRtbId = $OldMainRtbAssoc.RouteTableId
$AssociationId = $OldMainRtbAssoc.RouteTableAssociationId
ネタがそろったので処理します。Set-EC2RouteTableAssociation
コマンドですが、-AssociationId
パラメータには現在のメインルートテーブルのAssociationId
を指定し、-RouteTableId
パラメータにはこれからメインにしたいルートテーブルの ID を指定します。
Set-EC2RouteTableAssociation -AssociationId $AssociationId -RouteTableId $PublicRtbId
メインルートテーブルが置き換わったら、不要となったデフォルトのルートテーブルを削除します。これで後始末は完了です。
Remove-EC2RouteTable -RouteTableId $OldMainRtbId -Force
最後の工程になります。基本的にはルートテーブルを作成・設定するところまでで完了なのですが、VPC エンドポイントがあるとなにかと便利なのでサンプルで作ってみます。内容としては、VPC と S3 を直で接続してインターネットを経由せずにコンテンツを GET できるようにしてみます。まずは他と同じでタグとフィルタです。
$EndPointName = "vpce-s3-get-all"
$EndPointTag = New-EC2NameTag -ResourceType "vpc-endpoint" -TagValue $EndPointName
$EndPointFilter = New-EC2Filter -Name "tag:Name" -Values $EndPointName
接続する AWS サービスの名前を指定します。
$ServiceName = "com.amazonaws.ap-northeast-1.s3"
ポリシーを json 形式で渡す必要があります。この場合は、すべての S3 オブジェクトを GET できます。json の作成に関しては AWS Policy Generator が便利です。
$PolicyDocument = @'
{
"Version": "2008-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "*"
}
]
}
'@
ネタがそろったのでエンドポイントを作成します。これでやっと最後のコードです!New-EC2VpcEndpoint
コマンドに接続先 AWS サービス、接続元 VPC、対象ルートテーブル、ポリシー、タグを渡します。もちろん Name タグで制御します。
If( -not (Get-EC2VpcEndpoint -Filter $EndPointFilter))
{
$EndPointParams = @{
ServiceName = $ServiceName
VpcId = $TargetVpcId
RouteTableId = $PublicRtbId, $PrivateRtbId
PolicyDocument = $PolicyDocument
TagSpecification = $EndPointTag
}
New-EC2VpcEndpoint @EndPointParams
}
Else
{
throw "${EndPointName}: すでに存在する名前です。"
}
思い付きで VPC を PowerShell で作ろうとしたところ、このように長い記事となってしまいました。タグやフィルタ等、PowerShell で EC2 を操作する場合の基本的な方法がわかったのは大きな収穫ですが、やはり素直に CloudFormation でやったほうがいいだろうな、というのが感想です。当たり前ですね。今回のつらいところとして、VPC とその各コンポーネントを自動で作る際に、途中でエラーが発生してもロールバックできない、という点が挙げられます。PowerShell にはトランザクションを扱うための仕組みが備わっていて、-UseTransaction
パラメータをつけることができるコマンドに限りトランザクションに参加させることができます。しかし AWS Tools for PowerShell のコマンド群は基本的にトランザクションには参加できないため、エラー処理に気を配る必要がありました。この一連の処理をトランザクション化できるとさらに管理しやすいのにな、と思った次第です。ただ、そうなってくるとなおさら CloudFormation のほうがいいわけで、今後同じことを CloudFormation でもやってみようかな、とも思いました。記事にするかはわかりませんが。VPC の構築が終わったので、次回は EC2 インスタンスを構築します。もちろん PowerShell でやる予定です。