yamamototis1105’s tech blog

ネットワークを中心とした技術ブログです

AWS TGW Connectアタッチメント接続のCFnカスタムリソース作成

はじめに

 ここ最近、TGW Connectアタッチメント接続するための検証環境を構築することが多々ありますが、この度CFn提供待ちに痺れを切らして、カスタムリソースを作ったので本記事を書きました。
 カスタムリソースは可読性や保守性を考えると使いどころが難しいですが、もし作成される場合には本記事を一例としてご参考にしていただけますと幸いです。

TGW Connectアタッチメント接続構成

 TGW~仮想アプライアンスをBGP over GRE接続する場合、Connectアタッチメントを利用します。Connectアタッチメントは、以下の流れで作成します。

  1. TGW~VPC(仮想アプライアンスあり)をVPCアタッチメントで接続する
  2. トランスポートアタッチメント(GREトンネルアドレスをルーティングするため)としてVPCアタッチメントを指定し、Connectアタッチメントを作成する
  3. さらにConnectアタッチメント内にConnectピアを作成し、GREおよびVPNを定義する

 以下の構成を作成する場合、TGW、TGWルートテーブル、VPCアタッチメント、ConnectアタッチメントまでCFnカバーされていますが、ConnectピアのみCFnカバーされていない状況です。
 ちなみに、VPCアタッチメントはGREトンネルアドレスをルーティングするためにAttachする必要がありますが、Association & Propagationする必要はありません。



 CFnロードマップ上、2022.06にカバレッジラベル付与、2023.03にResearching遷移、2023.08にComming Soon遷移、と間も無くカバーされる気もしますが、コレばかりは何とも言えないですね...

github.com github.com

カスタムリソースの概要

 それでは、どのようにCFnカバーされていないリソースを構築自動化するのでしょうか。
 CFnはLambdaやSNSを呼び出すカスタムリソースという機能があり、呼び出されたLambdaやSNS自由度の高いロジックを実装することが可能となっています。
 例えば、CFnカバーされていないConnectピアを作成するLambdaを作成し、カスタムリソースからLambdaを呼び出すといった使い方も考えられます。


 一見すると便利そうですが、IaCの優位点である可読性や保守性が下げてしまう可能性もあります。個人的には、カスタムリソース検討時に以下のチェックポイントで確認するように心掛けてます。

  • 更新頻度はほぼなく繰り返し利用するか
  • サービス提供するため利用者の作業負担を抑えたいか
  • 部分的手動やshell+aws cliで代替できないか

 その他のカスタムリソースに関する詳細な情報は、CloudFormationのユーザガイドをご参照下さい。個人的には、MarketplaceでAMIと一緒に提供されているテンプレートを読む等も勉強になりました。

docs.aws.amazon.com

カスタムリソースの作成

 それでは、以下のミニマム構成(VPC、TGW、EC2、カスタムリソース)を作成していきます。
カスタムリソースは作成時のみ実装し、更新時・削除時が実装できていないためご留意下さい。

VPC

 VPC、Subnet、RouteTableなどを作成します。Route宛先のVPCアタッチメント作成は時間がかかるため、DependsOnにVPCアタッチメントを設定し待機させます。

{
  "Resources" : {
    "Vpc" : {
      "Type" : "AWS::EC2::VPC",
      "Properties" : {
        "CidrBlock" : "10.0.0.0/16",
        "Tags" : [ { "Key" : "Name", "Value" : "vpc" } ]
      }
    },
    "VpcSubnet" : {
      "Type" : "AWS::EC2::Subnet",
      "Properties" : {
        "VpcId" : { "Ref" : "Vpc" },
        "CidrBlock" : "10.0.1.0/24",
        "AvailabilityZone" : { "Fn::Select": [ 0, { "Fn::GetAZs": "" } ] },
        "Tags" : [ { "Key" : "Name", "Value" : "vpc-subnet" } ]
      }
    },
    "VpcRtb" : {
      "Type" : "AWS::EC2::RouteTable",
      "Properties" : {
        "VpcId" : { "Ref" : "Vpc" },
        "Tags" : [ { "Key" : "Name", "Value" : "vpc-rtb" } ]
      }
    },
    "VpcSubnetRouteTableAssociation" : {
      "Type" : "AWS::EC2::SubnetRouteTableAssociation",
      "Properties" : {
        "SubnetId" : { "Ref" : "VpcSubnet" },
        "RouteTableId" : { "Ref" : "VpcRtb" }
      }
    },
    "VpcRtbRoute" : {
      "Type": "AWS::EC2::Route",
      "Properties": {
        "RouteTableId" : { "Ref" : "VpcRtb" },
        "DestinationCidrBlock" : "192.168.1.0/24",
        "TransitGatewayId" : { "Ref" : "Tgw" }
      },
      "DependsOn" : "TgwVpcAttach"
    }
  }
}

TGW

 TGW、RouteTable、VPCアタッチメント、Connectアタッチメントなどを作成します。前述の通り、VPCアタッチメントはAssociation & Propagationしないようにします。

{
  "Resources" : {
    "Tgw" : {
      "Type": "AWS::EC2::TransitGateway",
      "Properties": {
        "AmazonSideAsn" : 64512,
        "DefaultRouteTableAssociation" : "disable",
        "DefaultRouteTablePropagation" : "disable",
        "TransitGatewayCidrBlocks" : [ "192.168.1.0/24" ],
        "Tags" : [ { "Key" : "Name", "Value" : "tgw" } ]
      }
    },
    "TgwRtb" : {
      "Type" : "AWS::EC2::TransitGatewayRouteTable",
      "Properties" : {
        "TransitGatewayId" : { "Ref" : "Tgw" },
        "Tags" : [ { "Key" : "Name", "Value" : "tgw-rtb" } ]
      }
    },
    "TgwVpcAttach" : {
      "Type": "AWS::EC2::TransitGatewayAttachment",
      "Properties": {
        "SubnetIds": [ { "Ref" : "VpcSubnet" } ],
        "TransitGatewayId": { "Ref" : "Tgw" },
        "VpcId": { "Ref" : "Vpc" },
        "Tags" : [ { "Key" : "Name", "Value" : "tgw-vpc-attach" } ]
      }
    },
    "TgwConnectAttach" : {
      "Type": "AWS::EC2::TransitGatewayConnect",
      "Properties": {
        "Options" : { "Protocol" : "gre" },
        "TransportTransitGatewayAttachmentId": { "Ref" : "TgwVpcAttach" },
        "Tags" : [ { "Key" : "Name", "Value" : "tgw-connect-attach" } ]
      }
    },
    "TransitGatewayRouteTableAssociation" : {
      "Type": "AWS::EC2::TransitGatewayRouteTableAssociation",
      "Properties": {
        "TransitGatewayAttachmentId" : { "Ref" : "TgwConnectAttach" }, 
        "TransitGatewayRouteTableId" : { "Ref" : "TgwRtb" }
      }
    },
    "TransitGatewayRouteTablePropagation" : {
      "Type": "AWS::EC2::TransitGatewayRouteTablePropagation",
      "Properties": {
        "TransitGatewayAttachmentId" : { "Ref" : "TgwConnectAttach" }, 
        "TransitGatewayRouteTableId" : { "Ref" : "TgwRtb" }
      }
    }
  }
}

EC2

 EC2、ENI、SecurityGatewayなどを作成します。EC2はMarketplaceからCatalyst 8000Vを起動し、ユーザーデータでGREおよびBGP設定を行います。

{
  "Resources" : {
    "RouterSg" : {
      "Type" : "AWS::EC2::SecurityGroup",
      "Properties" : {
        "GroupName" : "router-sg",
        "GroupDescription" : "router-sg",
        "VpcId" : { "Ref" : "Vpc" },
        "SecurityGroupIngress" : [ { "IpProtocol" : "-1", "FromPort" : "-1",  "ToPort" : "-1",  "CidrIp" : "10.0.0.0/8" } ],
        "SecurityGroupEgress" : [ { "IpProtocol" : "-1", "FromPort" : "0", "ToPort" : "65535", "CidrIp" : "0.0.0.0/0" } ],
        "Tags" : [ { "Key" : "Name", "Value" : "router-sg" } ]
      }
    },
    "RouterEni" : {
      "Type" : "AWS::EC2::NetworkInterface",
      "Properties" : {
        "GroupSet" : [ { "Ref" : "RouterSg" } ],
        "SubnetId": { "Ref" : "VpcSubnet" },
        "PrivateIpAddress" : "10.0.1.254",
        "SourceDestCheck" : "false",
        "Tags" : [ { "Key" : "Name", "Value" : "router-eni" } ]
      }
    },
    "Router" : {
      "Type" : "AWS::EC2::Instance",
      "Properties" : {
        "InstanceType" : "t3.medium",
        "ImageId" : { "Fn::FindInMap" : [ "RouterAmi", { "Ref" : "AWS::Region" }, "AmiId" ] },
        "NetworkInterfaces": [ { "DeviceIndex": "0", "NetworkInterfaceId": { "Ref" : "RouterEni" } } ],
        "UserData" : { "Fn::Base64" : { "Fn::Join" : ["", [
          "ios-config-1=\"interface GigabitEthernet1\"\n",
          "ios-config-2=\" ip address 10.0.1.254 255.255.255.0\"\n",
          "ios-config-3=\"interface Tunnel1\"\n",
          "ios-config-4=\" ip address 169.254.6.1 255.255.255.248\"\n",
          "ios-config-5=\" tunnel source GigabitEthernet1\"\n",
          "ios-config-6=\" tunnel destination 192.168.1.1\"\n",
          "ios-config-7=\"ip route 192.168.1.0 255.255.255.0 10.0.1.1\"\n",
          "ios-config-8=\"router bgp 65000\"\n",
          "ios-config-9=\" timer bgp 10 30\"\n",
          "ios-config-10=\" neighbor 169.254.6.2 remote-as 64512\"\n",
          "ios-config-11=\" neighbor 169.254.6.2 ebgp-multihop\"\n",
          "ios-config-12=\" neighbor 169.254.6.3 remote-as 64512\"\n",
          "ios-config-13=\" neighbor 169.254.6.3 ebgp-multihop\"\n"
        ] ] } },
        "Tags" : [ { "Key" : "Name", "Value" : "router" } ]
      }
    }
  }
}

カスタムリソース

 Lambda、Role、カスタムリソースなどを作成します。LambdaはBoto3でConnectピアを作成し、Roleは最小権限の原則よりCloudWatchログ出力、Connectピア作成のみ認可します。

{
  "Resources" : {
    "TgwConnectPeerCreatorRole" : {
      "Type" : "AWS::IAM::Role",
      "Properties" : {
        "AssumeRolePolicyDocument" : {
          "Version": "2012-10-17",
          "Statement": [ {
            "Effect": "Allow",
            "Principal": { "Service" : [ "lambda.amazonaws.com" ] },
            "Action": [ "sts:AssumeRole" ]
          } ]
        },
        "Path" : "/",
        "ManagedPolicyArns" : [ "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ],
        "Policies" : [ {
          "PolicyName": "TgwConnectPeerCreatorPolicy",
          "PolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [ {
              "Effect": "Allow",
              "Action": [ "ec2:CreateTransitGatewayConnectPeer" , "ec2:CreateTags" ],
              "Resource": "*"
            } ]
          }
        } ]
      }
    },
    "FunctionCreateTgwConnectPeer" : {
      "Type" : "AWS::Lambda::Function",
      "Properties" : {
        "Code" : {
          "ZipFile" : { "Fn::Join": [ "", [
            "import json\n",
            "import boto3\n",
            "import cfnresponse\n",
            "def lambda_handler(event, context):\n",
            "  try:\n",
            "    client = boto3.client('ec2')\n",
            "    response = client.create_transit_gateway_connect_peer(\n",
            "      TransitGatewayAttachmentId='" , { "Ref" : "TgwConnectAttach" } , "',\n",
            "      TransitGatewayAddress='192.168.1.1',\n",
            "      PeerAddress='10.0.1.254',\n",
            "      BgpOptions={'PeerAsn':65000},\n",
            "      InsideCidrBlocks=['169.254.6.0/29'],\n",
            "      TagSpecifications=[{\n",
            "        'ResourceType':'transit-gateway-connect-peer',\n",
            "        'Tags':[{'Key':'Name','Value':'tgw-connect-attach-peer'}],\n",
            "      }],\n",
            "      DryRun=False\n",
            "    )\n",
            "    cfnresponse.send(event, context, cfnresponse.SUCCESS, {})\n",
            "  except Exception as e:\n",
            "    cfnresponse.send(event, context, cfnresponse.FAILED, {})"
          ] ] }
        },
        "Handler": "index.lambda_handler",
        "Role": { "Fn::GetAtt" : [ "TgwConnectPeerCreatorRole", "Arn" ] },
        "Runtime": "python3.9",
        "Timeout": 60
      }
    },
    "CustomCreateTgwConnectPeer" : {
      "Type" : "Custom::FunctionCreateTgwConnectPeer",
      "Properties" : {
        "ServiceToken" : { "Fn::GetAtt" : [ "FunctionCreateTgwConnectPeer", "Arn" ] }
      }
    }
  }
}

つまづきポイントと解決方法

 テンプレートは直ぐに作成できたわけでなく、スタック作成・更新・削除を繰り返し試行することで作成できました。ここでは自らのつまづきポイントと解決方法をご紹介します。

CloudFormation/Lambdaのコーディング

 今日びググったら多くのサンプルを手に入れることは容易いですが、マイナーな設定と遭遇した場合は王道な調査方法が分かってないと立往生します。
 苦労したポイントは、CFnのリソースの構造、IAMポリシーのアクション、Boto3のメソッドの引数あたりですが、下記サイトで解決することができました。

CloudFormation/Lambdaのトラブルシュート

 大抵CloudFormationはCloudFormationイベント、LambdaはCloudWatchログを確認することにより問題判明できてましたが、それでも理解不能なエラーメッセージは出てきます。
 その時に解決の糸口になったのがCloudTrailイベント履歴でした。一つ一つのAPIログが確認でき、CloudWatchログより有益な情報が得られる場合がありました。

まとめ

 Connectピアのカスタムリソースを作成し、Connectアタッチメント接続がフル自動化できました。 カスタムリソースは可読性や運用性が失われる可能性もあるため十分検討したうえでご利用下さい。

 今回ご紹介したコードは下記ページをご参照下さいませ。

github.com

おわりに

 AWS TGW Connectアタッチメント接続に限らずカスタムリソースを利用される方にとって、本記事がご参考になりますと幸いです。