CloudFormation でオンプレ環境を模した NW を造ってみる

# はじめに
\_s\_\_o\_ です。
AWS で検証をする際、お客様オンプレ環境が必要になる場合があります (Site-to-Site VPN 経由での接続など)。

とは言えど、おいそれと簡単に用意できるものではないので、代替として「オンプレ環境を模した」 AWS の NW を構築します。手動でポチポチと作成してもいいのですが、あくまで検証用途 (=使い終わったらすぐに消す) の想定のため、環境の改廃がしやすい CloudFormation を使いたいと思います。

# 検証 NW の構築
## 構成イメージ
赤太字の箇所が、CloudFormation で作成する部分です。
![cf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/46216/c8195938-334b-95fc-1c8c-fd2387b9228a.png)

## CloudFormation のコード (YAML)
細かい中身は [前提事項] をご確認ください。

```
AWSTemplateFormatVersion: 2010-09-09
# ------------------------------------------------------------#
#  [前提事項]
#  ・疑似オンプレ環境 (以下、疑似オンプレ) を想定した VPC。
#  ・Site-to-Site VPN を用いて AWS (以下、接続先 AWS) に接続する想定。
#  ・ALB にアクセスさせる (=名前解決必要) ため、AWS の Route53 Resolver を参照させる想定。
#  ・このテンプレートで作成するオブジェクトは下記の通り。無料、且つ、作りきり (変更の可能性が低い) のものを対象としている (故に、すべて NW 系のオブジェクト)
#    - DHCP オプションセット x 1
#    - VCP x 1
#    - サブネット x 2
#    - ルートテーブル x 2
#    - インターネット GW x 1
#    - ENI x 2
#
#  ・このテンプレートで対象外のオブジェクト (=自分で作成する必要がある) は下記の通り。
#    - 疑似オンプレ側
#      -- セキュリティグループ
#      -- VPN ルータ@パブリック NW ※VyOS など
#      -- EC2 サーバ@プライベート NW
#    - 接続先 AWS 側
#      -- CGW 設定
#      -- VPN 設定
#      -- VGW or TGW 設定
#      -- Route53 設定
#      -- Route53 Resolver 設定
#
#  [留意事項]
#  ・VPC の名前解決を Off にすると、SSH の接続に時間がかかる。下記 URL を参考にして、
#    sshd_config の修正を推奨する (「UseDNS=no」、「GSSAPIAuthentication no」)
#    https://yuyarin.hatenadiary.org/entry/20090410/1239298235
#  ・VyOS の場合は「set service ssh disable-host-validation」で DNS 名前解決を無効化できる。
# ------------------------------------------------------------#

# ------------------------------------------------------------#
#  Parameter
# ------------------------------------------------------------#

Parameters:

  # 疑似オンプレの VPC 名。
  VpcNAME:
    Type: String
    Description: VPC name.

  # 疑似オンプレの CIDR (例 : 172.31.0.0/16)
  VpcCIDR:
    Type: String
    Description: VPC CIDR address.

  # 疑似オンプレのパブリック NW (例 : 172.31.1.0/24)
  PublicNwCIDR:
    Type: String
    Description: Public NW CIDR address.

  # 疑似オンプレのプライベート NW (例 : 172.31.101.0/24)
  PrivateNwCIDR:
    Type: String
    Description: Private NW CIDR address.

  # 疑似オンプレの VPN ルータ@パブリック NW の IP アドレス (例 : 172.31.1.254)
  PublicVpnrt1IP:
    Type: String
    Description: Public VPN Router01 IP address (Private IP address).

  # 疑似オンプレの サーバ@プライベート NW の IP アドレス (例 : 172.31.101.51)
  PrivateSv1IP:
    Type: String
    Description: Private Server01 IP address.

  # 接続先 AWS の Route53 Resolver の IP。DHCP オプションセットで使用する。複数の場合はコンマ区切りで記入する (例 : 192.168.100.100, 192.168.101.100)
  Route53ResolverIP:
    Type: String
    Description: Route53Resolver IP address.

  # 接続先 AWS の CIDR。ルートテーブルで使用する。
  RemoteSiteNwCIDR:
    Type: String
    Description: Remote NW CIDR address.

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#

Resources:

# ------------------------------------------------------------#
#  1. AWS::EC2::DHCPOptions
# ------------------------------------------------------------#

# オンプレ想定の VPC のため、AWS の DNS (Route53 Resolver) を
# サーバの DNS として指定する。
  DHCP:
    Type: AWS::EC2::DHCPOptions
    Properties:
      DomainNameServers:
        - !Ref Route53ResolverIP
      Tags:
        - Key: Name
          Value: !Sub "dhcp-${VpcNAME}"

# ------------------------------------------------------------#
#  2. AWS::EC2::VPC
# ------------------------------------------------------------#

# オンプレ想定の VPC のため、EnableDnsSupport (Amazon DNS の利用)、
# EnableDnsHostnames (Amazon DNS 名の付与) は false とする。
  VPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsSupport: "false"
      EnableDnsHostnames: "false"
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub "${VpcNAME}"

# ------------------------------------------------------------#
#  3. AWS::EC2::VPCDHCPOptionsAssociation
# ------------------------------------------------------------#
  VpcDhcpAssociation:
    Type: AWS::EC2::VPCDHCPOptionsAssociation
    DependsOn:
      - VPC
      - DHCP
    Properties:
      VpcId: !Ref VPC
      DhcpOptionsId: !Ref DHCP

# ------------------------------------------------------------#
#  4. AWS::EC2::Subnet
# ------------------------------------------------------------#

  NwPublic1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PublicNwCIDR
      MapPublicIpOnLaunch: 'true'
      AvailabilityZone: "ap-northeast-1a"
      Tags:
        - Key: Name
          Value: !Sub "nw-${VpcNAME}-public1"

  NwPrivate1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PrivateNwCIDR
      MapPublicIpOnLaunch: 'false'
      AvailabilityZone: "ap-northeast-1c"
      Tags:
        - Key: Name
          Value: !Sub "nw-${VpcNAME}-private1"

# ------------------------------------------------------------#
#  5. AWS::EC2::RouteTable
# ------------------------------------------------------------#

  RtPublic:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "rt-${VpcNAME}-public"

  RtPrivate:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "rt-${VpcNAME}-private"

# ------------------------------------------------------------#
#  6. AWS::EC2::InternetGateway
# ------------------------------------------------------------#

  IGW:
    Type: 'AWS::EC2::InternetGateway'
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "igw-${VpcNAME}"

# ------------------------------------------------------------#
#  7. AWS::EC2::VPCGatewayAttachment
# ------------------------------------------------------------#

  IgwAttachGateway:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    DependsOn:
      - VPC
      - IGW
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW

# ------------------------------------------------------------#
#  8. AWS::EC2::SubnetRouteTableAssociation
# ------------------------------------------------------------#

  RouteTableAssociationPublic:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn:
      - NwPublic1
      - RtPublic
    Properties:
      SubnetId: !Ref NwPublic1
      RouteTableId: !Ref RtPublic

  RouteTableAssociationPrivate:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    DependsOn:
      - NwPrivate1
      - RtPrivate
    Properties:
      SubnetId: !Ref NwPrivate1
      RouteTableId: !Ref RtPrivate

# ------------------------------------------------------------#
#  9. AWS::EC2::SecurityGroup
# ------------------------------------------------------------#

# 変更が頻繁に発生して管理が面倒なので、セキュリティグループは作成しない。

  # SgPublicVpnrt:
  #   Type: 'AWS::EC2::SecurityGroup'
  #   DependsOn:
  #     - VPC
  #   Properties:
  #     GroupName: !Sub "sgr-${VpcNAME}-public-vpnrt"
  #     GroupDescription: 'For public VPN router.'
  #     VpcId: !Ref VPC
  #     Tags:
  #       - Key: Name
  #         Value: !Sub "sgr-${VpcNAME}-public-vpnrt"

  # SgPrivateSV:
  #   Type: 'AWS::EC2::SecurityGroup'
  #   DependsOn:
  #     - VPC
  #   Properties:
  #     GroupName: !Sub "sgr-${VpcNAME}-private-sv"
  #     GroupDescription: 'For private EC2 server.'
  #     VpcId: !Ref VPC
  #     Tags:
  #       - Key: Name
  #         Value: !Sub "sgr-${VpcNAME}-private-sv"

# ------------------------------------------------------------#
#  10. AWS::EC2::NetworkInterface
# ------------------------------------------------------------#

  EniPublicVpnrt1:
    Type: AWS::EC2::NetworkInterface
    DependsOn:
      - NwPublic1
    Properties:
      SourceDestCheck: 'false'
      SubnetId: !Ref NwPublic1
      PrivateIpAddress: !Ref PublicVpnrt1IP
      Tags:
        - Key: 'Name'
          Value: !Sub "eni-${VpcNAME}-public-vpnrt1"

  EniPrivateSv1:
    Type: AWS::EC2::NetworkInterface
    DependsOn:
      - NwPrivate1
    Properties:
      SourceDestCheck: 'true'
      SubnetId: !Ref NwPrivate1
      PrivateIpAddress: !Ref PrivateSv1IP
      Tags:
        - Key: 'Name'
          Value: !Sub "eni-${VpcNAME}-private-sv1"

# ------------------------------------------------------------#
#  11. AWS::EC2::Route
# ------------------------------------------------------------#

# Default : 0.0.0.0/0

  RtPublicDefault:
    Type: 'AWS::EC2::Route'
    DependsOn:
      - RtPublic
      - IGW
    Properties:
      RouteTableId: !Ref RtPublic
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref IGW

# Remote Site

  RtPublicRemoteSite:
    Type: 'AWS::EC2::Route'
    DependsOn:
      - RtPublic
      - EniPublicVpnrt1
    Properties:
      RouteTableId: !Ref RtPublic
      DestinationCidrBlock: !Ref RemoteSiteNwCIDR
      NetworkInterfaceId: !Ref EniPublicVpnrt1

  RtPrivateRemoteSite:
    Type: 'AWS::EC2::Route'
    DependsOn:
      - RtPrivate
      - EniPublicVpnrt1
    Properties:
      RouteTableId: !Ref RtPrivate
      DestinationCidrBlock: !Ref RemoteSiteNwCIDR
      NetworkInterfaceId: !Ref EniPublicVpnrt1

# ------------------------------------------------------------#
#  Outputs
# ------------------------------------------------------------#

Outputs:

  VpcID:
    Value: !Ref VPC
    Export:
      Name: !Sub "${VpcNAME}-VPC"

  NwPublic1ID:
    Value: !Ref NwPublic1
    Export:
      Name: !Sub "${VpcNAME}-NW-Public1"

  NwPrivate1ID:
    Value: !Ref NwPrivate1
    Export:
      Name: !Sub "${VpcNAME}-NW-Private1"

  # SgPublicVpnrt:
  #   Value: !Ref SgPublicVpnrt
  #   Export:
  #     Name: !Sub "${VpcNAME}-SG-Public-VPNRT"

  # SgPrivateSv:
  #   Value: !Ref SgPrivateSV
  #   Export:
  #     Name: !Sub "${VpcNAME}-SG-Private-SV"

  EniPublicVpnrt1:
    Value: !Ref EniPublicVpnrt1
    Export:
      Name: !Sub "${VpcNAME}-ENI-Public-VPNRT1"

  EniPrivateSv1:
    Value: !Ref EniPrivateSv1
    Export:
      Name: !Sub "${VpcNAME}-ENI-Private-SV1"

```
## ポイント解説
### DHCP オプションセット
疑似オンプレ環境から、接続先 AWS の Route53 Resolver を参照させるため、DHCP オプションセットを新規作成しています。デフォルトの DHCP オプションセットは Amazon DNS を参照するようになっているので、参照先を Route53 Resolver に変更しています。

### VPC
オンプレ想定のため、Amazon DNS を参照しないように「DNS による名前解決」と「ホスト名の使用」を false にしています。

### Security Group
最初はセキュリティグループも CloudFormation で定義していましたが、ちょこちょこ内容に変更が入って、そのたびにコードを更新するのが面倒だったので、CloudFormation の対象から外しました。Security Group のように中身の変更頻度が多いオブジェクトは、CloudFormation の管理対象にしない方がいいかもしれないです。

### Outputs
今後、クロススタックの利用も想定し、作成したオブジェクトを Export しています。

### その他の注意事項
コードの [前提事項] に記載していますが、VPC で「DNS による名前解決」を Off にすると、Linux などに SSH ログインする際に時間がかかります。そのため、サーバを構築する際は、sshd_config の修正を推奨します (「UseDNS=no」、「GSSAPIAuthentication no」)。[参考](https://yuyarin.hatenadiary.org/entry/20090410/1239298235)

# おまけ
今回、VPN ルータとして VyOS を使用しました。どうやら、VyOS でルーティングが「静的」の場合、ルータ投入用の config がダウンロードできないみたいです (というか、そもそもダウンロードの項目に存在しない)。

config の修正に関しては、[こちら](https://dev.classmethod.jp/articles/vyos-aws-vpn-static-routing/) のサイトが参考になるので、あわせてご参照ください。

# まとめ
以上、オンプレ環境を模した検証 NW の構築でした。

いつも必要ではないですが、時々ふと必要になったりするので、こういったものは CloudFormation でテンプレート化しておくと色々捗ります。