CDKでNetwork FirewallエンドポイントリソースのAZを判別する際につまずいた話
はじめに
こんにちはyuuchanです。
前回執筆時と同じPJで、AWSを用いて複数のアカウントが絡んだりオンプレ環境との接続も絡んだりするような、比較的複雑なネットワークを構築するタスクを担当しています。
AWSリソースの構築にはCDK(Typescript)を使用しているのですが、一般的なアプリケーションコードと同様の感覚で書いていると、思いがけない箇所でつまずく場合があります。
今回は、マルチAZに配置したNetwork FirewallエンドポイントのAZを判別する処理を作成する際につまずいた話、およびその解決方法を紹介したいと思います。
作成したいAWSリソース
まず、どのようなAWSリソースを作成しようとしていたかを記します。
複数VPCから構成されるネットワークにおいて、インターネットとの通信を1つのVPCに集約するために以下図のような設計を行いました。
「インターネット接続用VPC」にのみInternet Gatewayを配置し、インターネットとの通信は必ずこのVPCのNetwork Firewallを経由するようなつくりになっています。
以降の節で、上図のようなAWSリソースをCDKで作成するにあたりどのような点でつまずいたかを説明していきます。
Network Firewallの作成
Transit Gateway ENIが配置されているサブネット(以下、TGWサブネット)からインターネットに出ようとしているトラフィックや、NAT Gatewayが配置されているサブネット(以下、NATGWサブネット)から「インターネットにアクセスする必要がある他VPC(以下、他VPC)」に入ろうとしているトラフィックをは、どちらもNetwork Firewall Endpoint(以下、NFWe)が配置されているサブネットにルーティングする必要があります。
このとき、ap-northeast-1aにあるTGWサブネットやNATGWサブネットからはap-northeast-1aにあるNFWeにルーティングするように、ap-northwast-1cにあるTGWサブネットやNATGWサブネットからはap-northwast-1cにあるNFWeにルーティングするようにルートテーブルを作成しなければなりません。
Network Firewallリソースを作成する際、以下のようなCDKのコード(抜粋)を書きます。
const networkFirewall = new nfw.CfnFirewall(
this,
"NetworkFirewall",
{
firewallName: "network-firewall",
description: "Network firewall for practice.",
firewallPolicyArn: firewallPolicy.ref, // 事前に作成したポリシーを指定する
subnetMappings: [
{
subnetId: firewallSubnet1.ref, // 事前に作成したap-northeast-1aのNFWe用サブネットを指定する
},
{
subnetId: firewallSubnet2.ref, // 事前に作成したap-northwast-1cのNFWe用サブネットを指定する
},
],
vpcId: internetAccessVpc.vpcId, // 事前に作成した「インターネット接続用VPC」を指定する
}
);
後続処理でTGWサブネットやNATGWサブネットに関連づくルートテーブル作成する際、ルートのターゲットとしてNFWeを指定する必要がありますが、その情報はnetworkFirewall.attrEndpointIds: string[]
(以下、attrEndpointIds
)に格納されています。
Network Firewallを作成する際、Construct PropsのsubnetMappings
で複数のサブネットを指定した場合はattrEndpointIds
に複数のエンドポイント情報が格納されることになります。
では、attrEndpointIds
に格納されている複数のエンドポイント情報の内、どれがap-northeast-1aのサブネットに配置されたもので、どれがap-northwast-1cのサブネットに配置されたものかをどのように判別できるのでしょうか。
ここについては簡単で、attrEndpointIds
には["ap-northeast-1a:vpce-123456789012", "ap-northeast-1c:vpce-987654321098"]
のように、リージョンやAZ情報がエンドポイント情報と「:」で結合された状態で格納されてるため、「:」より前の文字列を見ればそのエンドポイントがどのAZに配置されているかを判別することができます。
それでは、上記情報をもとにTGWサブネットやNATGWサブネットに関連づくルートテーブルを作成していきたいと思います。
ルートテーブルの作成、つまずいた点
例として、ap-northeast-1aにあるTGWサブネットに関連付けるルートテーブルを作成していきます。CfnRouteTable
やCfnSubnetRouteTableAssociation
、CfnRoute
を用いて、ルートテーブルの作成やサブネットとの関連付けを行います。CfnRoute
のConstruct PropsであるvpcEndpointId
にNFWeを設定する必要がありますが、ここで、作ろうとしているルートテーブルと関連付けるサブネットと同じAZにあるNFWeを指定する必要があります。
今回は、filterEndpointIdBySubnet(nfwEpIds, subnet)
という関数を作成し、適切なNFWeを選択できるようにします。
// subnetと同じAZにあるNFWeをnfwEpIdsから取得する パターン1
const filterEndpointIdBySubnet = (
nfwEpIds: string[],
subnet: ec2.CfnSubnet
): string => {
const nfwEpId = nfwEpIds.find((ep) => {
const epArray = ep.split(":");
const epAz = epArray[0];
epAz === subnet.attrAvailabilityZone;
});
if (nfwEpId === undefined) {
return "";
}
return nfwEpId.split(":")[1];
};
// subnetと同じAZにあるNFWeをnfwEpIdsから取得する パターン2
const filterEndpointIdBySubnet = (
nfwEpIds: string[],
subnet: ec2.CfnSubnet
): string => {
let nfwEpId = "";
nfwEpIds.forEach((ep) => {
const epArray = ep.split(":");
const epAz = epArray[0];
const epId = epArray[1];
if (epAz === subnet.attrAvailabilityZone) {
nfwEpId = epId;
}
});
return nfwEpId;
};
// ルートテーブルの作成
const routeTable = new ec2.CfnRouteTable(
this,
"RouteTable",
{
vpcId: internetAccessVpc.vpcId, // 事前に作成した「インターネット接続用VPC」を指定する
}
);
// ルートテーブルとサブネットの関連付け
new ec2.CfnSubnetRouteTableAssociation(
this,
"SubnetRouteTableAssociation",
{
routeTableId: routeTable.ref,
subnetId: tgwSubnet1.ref, // 事前に作成したap-northeast-1aのTGWサブネットを指定する
}
);
new ec2.CfnRoute(
this,
"Route for Internet",
{
routeTableId: routeTable.ref,
destinationCidrBlock: "0.0.0.0/0",
vpcEndpointId: filterEndpointIdBySubnet(
networkFirewall.attrEndpointIds,
tgwSubnet1 // ルートテーブルと関連付けたサブネットと同じサブネットを指定する
),
}
);
上記CDKのコードでリソースをデプロイしようとしたところ、パターン1と2どちらのfilterEndpointIdBySubnet()
を使用した場合でも以下のようなエラーが発生しました。
Resource handler returned message: "The request must contain exactly one of gatewayId, natGatewayId, networkInterfaceId, vpcPeeringConnectionId, egressOnlyInternetGatewayId, transitGatewayId, localGatewayId, car
rierGatewayId, vpcEndpointId, coreNetworkArn or instanceId
どちらのパターンのfilterEndpointIdBySubnet()
でも期待通りの値が返却されていないようです。
試しに、AZの判定を行わずに決め打ちで配列1つめの要素を返却するようにして再度デプロイを実行してみます。
const filterEndpointIdBySubnet = (
nfwEpIds: string[],
subnet: ec2.CfnSubnet
): string => {
return nfwEpIds[0].split(":")[1];
};
・・・これでも前述と同様のエラーが出てしまいました。
どうやらそもそもsplit()
などが期待通りに動いていないような気がします。
問題点および修正後のコード
調べてみたところ、デプロイ後に値が決まるようなCDKクラス(CfnFirewall
やCfnSubnet
など)のプロパティに対して操作を行う際は、CDKの組み込み関数を使わなければいけないそうです。
// nfwEpIds[0] ではなく
cdk.Fn.select(0, nfwEpIds)
// ep.split(":") ではなく
cdk.Fn.split(":", ep)
// epAz === subnet.attrAvailabilityZone ではなく
cdk.Fn.conditionEquals(epAz, subnet.attrAvailabilityZone)
上記問題点を踏まえ、filterEndpointIdBySubnet()
のコードを修正します。
// subnetと同じAZにあるNFWeをnfwEpIdsから取得する
const filterEndpointIdBySubnet = (
nfwEpIds: string[],
subnet: ec2.CfnSubnet
): string => {
for (let i = 0; i < nfwEpIds.length; i++) {
let nfwEpId = cdk.Fn.select(i, nfwEpIds);
let nfwEpAz = cdk.Fn.select(0, cdk.Fn.split(":", nfwEpId));
let nfwEp = cdk.Fn.select(1, cdk.Fn.split(":", nfwEpId));
if (cdk.Fn.conditionEquals(nfwEpAz, subnet.attrAvailabilityZone)) {
return nfwEp;
}
}
throw new Error("Valid AZ not found.");
};
foreach()
やfind()
を使わずfor
ループでNFWeを1つずつ参照するようにし、cdk.Fn.select()
やcdk.Fn.split
を用いて配列要素の参照や文字列操作を行うよう修正しました。
結果、今回記事内で提示していないルートテーブルも含め、無事すべて期待通りのルートテーブルをデプロイすることができました。
おわりに
いかがでしたでしょうか。
CDKが提供しているクラスのプロパティにアクセスする際は、Typescriptで普段使用しているような書き方ができない場合があり、その際は組み込み関数を使用しなければいけません。
CDKの組み込み関数は他にも20以上用意されており、一通り押さえておいたほうが今後CDKを用いてAWSリソースを構築していく際に困らずに済みそうですね。
今後もしばらくCDKを使用していくことになりそうですので、またつまずいた点やテクニック等をご紹介できればと思います。