Toyokumo Tech Blog

トヨクモ株式会社の開発本部のブログです。

堅牢な VPC を CloudFormation でサクッと構築する方法

こんにちは。トヨクモ開発本部インフラエンジニアの井上です。

AWS には CloudFormation というリソースの構築を自動化できるサービスがあります。 今回は CloudFormation を使って、堅牢な RDS ネットワークを構築してみたいと思います。

背景

日々データストアを運用していると、以下のようなことをやりたい場面が出てくるのではないでしょうか。

  • 本番の RDS に対して、マイグレートやスクリプトの実行時間を検証したい
  • 稼働中のEC2インスタンスからアクセスさせたくない
  • 恒久的な構成ではなく一時的に作成、削除したい

やり方は色々あると思いますが弊社では次のような方法で解決しています。

方法

アクセス元が限定された検証用の堅牢なネットワークを CloudFormation で作成し、RDS のインスタンスをその VPC ネットワーク内に構築する。

RDS は本番のリードレプリカからマスターに昇格させたものを使います。これで本番のデータが壊れることはなくなります。 さらにアクセス元を弊社IPからのみに限定することで、セキュリティーを保ちデータを外部に流出させないようにします。

構成図は以下の通り。

fig

ネットワークリソースの要件

  • 検証用 VPC
  • ルートテーブル
  • インターネットゲートウェイ
  • AvailabilityZone の異なるサブネット2つ
  • RDS サブネットグループ
  • 特定 IP 接続のみ許可したセキュリティグループ

RDS インスタンスの設置

  • RDS インスタンスのリードレプリカを作成する
  • 作成したリードレプリカをマスターに昇格させる
  • 検証用 VPC のネットワークに移動させる

コード

少し長いですが CloudFormation コードの全体像です。 PostgreSQL と MySQL に対応しています。

---
AWSTemplateFormatVersion: '2010-09-09'
Description: Robust RDS Network for Testing

# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
      - Label:
          default: "Network Configuration"
        Parameters:
          - VPCCIDR
          - AvailabilityZone1
          - AvailabilityZone2
          - RDSPublicSubnet1CIDR
          - RDSPublicSubnet2CIDR
      - Label:
          default: "Connection Source IP"
        Parameters:
          - ConnectionSourceIP
    ParameterLabels:
      VPCCIDR:
        default: "VPC CIDR"
      AvailabilityZone1:
        default: "AvailabilityZone 1"
      AvailabilityZone2:
        default: "AvailabilityZone 2"
      RDSPublicSubnet1CIDR:
        default: "RDS PublicSubnet 1 CIDR"
      RDSPublicSubnet2CIDR:
        default: "RDS PublicSubnet 2 CIDR"
      ConnectionSourceIP:
        default: "Source IP"

# ------------------------------------------------------------#
#  Input Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix:
    Type: String
  VPCCIDR:
    Type: String
    Default: "14.100.0.0/16"
  AvailabilityZone1:
    Type: String
    Default: "ap-northeast-1b"
  AvailabilityZone2:
    Type: String
    Default: "ap-northeast-1c"
  RDSPublicSubnet1CIDR:
    Type: String
    Default: "14.100.21.0/24"
  RDSPublicSubnet2CIDR:
    Type: String
    Default: "14.100.22.0/24"
  ConnectionSourceIP:
    Type: String
    Default: "Set Source IP xxx.xxx.xxx.xxx/32"

Resources:
  # ------------------------------------------------------------#
  #  VPC
  # ------------------------------------------------------------#
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      InstanceTenancy: default
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-vpc"

  # ------------------------------------------------------------#
  #  Internet Gateway
  # ------------------------------------------------------------#
  IGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-igw"
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW

  # ------------------------------------------------------------#
  #  RDS PublicSubnet
  # ------------------------------------------------------------#
  RDSPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref RDSPublicSubnet1CIDR
      AvailabilityZone: !Ref AvailabilityZone1
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-rds-public-subnet-1"
  RDSPublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref RDSPublicSubnet2CIDR
      AvailabilityZone: !Ref AvailabilityZone2
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-rds-public-subnet-2"

  # ------------------------------------------------------------#
  #  RDS Subnet Group and Security Group
  # ------------------------------------------------------------#
  RDSSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Robust Subnet Group
      SubnetIds:
      - Ref: RDSPublicSubnet1
      - Ref: RDSPublicSubnet2
  RDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId:
        Ref: VPC
      GroupDescription: Robust DB SecurityGroup
      SecurityGroupIngress:
      # MySQL
      - IpProtocol: tcp
        FromPort: '3306'
        ToPort: '3306'
        CidrIp: !Ref ConnectionSourceIP
      # PostgreSQL
      - IpProtocol: tcp
        FromPort: '5432'
        ToPort: '5432'
        CidrIp: !Ref ConnectionSourceIP

  # ------------------------------------------------------------#
  #  Public RouteTable
  # ------------------------------------------------------------#
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-public-rt"

  # ------------------------------------------------------------#
  #  Public Routing
  # ------------------------------------------------------------#
  PublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref IGW

  # ------------------------------------------------------------#
  #  Public RouteTable Association
  # ------------------------------------------------------------#
  RDSPublicSubnetAttach1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref RDSPublicSubnet1
  RDSPublicSubnetAttach2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref RDSPublicSubnet2

# ------------------------------------------------------------#
#  Output Parameters
# ------------------------------------------------------------#
Outputs:
  VPCID:
    Value: !Ref VPC
    Export:
      Name: !Sub "${PJPrefix}-vpc-id"
  VPCCIDR:
    Value: !Ref VPCCIDR
    Export:
      Name: !Sub "${PJPrefix}-vpc-cidr"
  #
  # RDS
  #
  RDSPublicSubnet1ID:
    Value: !Ref RDSPublicSubnet1
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-1-id"
  RDSPublicSubnet1CIDR:
    Value: !Ref RDSPublicSubnet1CIDR
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-1-cidr"
  RDSPublicSubnet2ID:
    Value: !Ref RDSPublicSubnet2
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-2-id"
  RDSPublicSubnet2CIDR:
    Value: !Ref RDSPublicSubnet2CIDR
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-2-cidr"
  #
  # Route
  #
  PublicRouteTableID:
    Value: !Ref PublicRouteTable
    Export:
      Name: !Sub "${PJPrefix}-public-route-id"

使い方

コードの解説をする前に、上記 yml ファイルの使い方を説明していきます。

スタックの作成

cf-1

最初に AWS コンソールから CloudFormation リソースにアクセスし、スタックの作成を選択します。

テンプレートのアップロード

cf-2

先ほどのコードをテンプレートとしてアップロードします。ファイル名はなんでもいいですが、拡張子は .yml にします。

パラメータの入力

cf-3

パラメータに適当な値を入れていきます。

AvailabilityZone は各リージョン毎に適したものをいれます。

東京なら ap-northeast-1b などです。ここでは例としてロンドンリージョンに作成する場合を書きました。

Source IP はアクセス元となる IP アドレスを CIDR 表記(ex. 12.34.56.78/32)で書きます。

スタックオプションの設定

cf-4

スタックオプションの設定は特に何も指定せずに 次へ を選択します。

ステータス

cf-5

数分待ってから CREATE_COMPLETE という表示が出れば完了です。

これでアクセスが限定された堅牢 VPC の出来上がりです。あとは作成された環境に RDS インスタンスのリードレプリカを作ってマスターに昇格させてから、適宜ツールなどを用いてエンドポイントに接続します。

リソースの一括削除

cf-6

最後に検証が終わったら、RDS のリソースを削除した上で、CloudFormation の削除を選択すると、作成されたリソースを一括で削除することができます。RDS が残った状態だとエラーになって削除できません。

説明

ポイントとなる部分だけかいつまんでコードの解説をしていきます。

メタデータとパラメータ

# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Project Name Prefix"
        Parameters:
          - PJPrefix
      - Label:
          default: "Network Configuration"
        Parameters:
          - VPCCIDR
          - AvailabilityZone1
          - AvailabilityZone2
          - RDSPublicSubnet1CIDR
          - RDSPublicSubnet2CIDR
      - Label:
          default: "Connection Source IP"
        Parameters:
          - ConnectionSourceIP
    ParameterLabels:
      VPCCIDR:
        default: "VPC CIDR"
      AvailabilityZone1:
        default: "AvailabilityZone 1"
      AvailabilityZone2:
        default: "AvailabilityZone 2"
      RDSPublicSubnet1CIDR:
        default: "RDS PublicSubnet 1 CIDR"
      RDSPublicSubnet2CIDR:
        default: "RDS PublicSubnet 2 CIDR"
      ConnectionSourceIP:
        default: "Source IP"

# ------------------------------------------------------------#
#  Input Parameters
# ------------------------------------------------------------#
Parameters:
  PJPrefix:
    Type: String
  VPCCIDR:
    Type: String
    Default: "14.100.0.0/16"
  AvailabilityZone1:
    Type: String
    Default: "ap-northeast-1b"
  AvailabilityZone2:
    Type: String
    Default: "ap-northeast-1c"
  RDSPublicSubnet1CIDR:
    Type: String
    Default: "14.100.21.0/24"
  RDSPublicSubnet2CIDR:
    Type: String
    Default: "14.100.22.0/24"
  ConnectionSourceIP:
    Type: String
    Default: "Set Source IP xxx.xxx.xxx.xxx/32"

まずはメタデータとパラメータの部分です。ここで書いた内容はスタック作成の際にパラメータとして入力できるようになります。

CloudFormation を書く中でマジックナンバー的な要素を書かなくてはならないときはパラメータとして追い出してやるといいです。

ここでは VPC の範囲 複数リージョンの対応のための AZ アクセス元 IP をパラメータとして分離しました。

RDS サブネットとサブネットグループ

  # ------------------------------------------------------------#
  #  RDS PublicSubnet
  # ------------------------------------------------------------#
  RDSPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref RDSPublicSubnet1CIDR
      AvailabilityZone: !Ref AvailabilityZone1
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-rds-public-subnet-1"
  RDSPublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref RDSPublicSubnet2CIDR
      AvailabilityZone: !Ref AvailabilityZone2
      Tags:
      - Key: Name
        Value: !Sub "${PJPrefix}-rds-public-subnet-2"

  # ------------------------------------------------------------#
  #  RDS Subnet Group and Security Group
  # ------------------------------------------------------------#
  RDSSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Robust Subnet Group
      SubnetIds:
      - Ref: RDSPublicSubnet1
      - Ref: RDSPublicSubnet2
  RDSSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId:
        Ref: VPC
      GroupDescription: Robust DB SecurityGroup
      SecurityGroupIngress:
      # MySQL
      - IpProtocol: tcp
        FromPort: '3306'
        ToPort: '3306'
        CidrIp: !Ref ConnectionSourceIP
      # PostgreSQL
      - IpProtocol: tcp
        FromPort: '5432'
        ToPort: '5432'
        CidrIp: !Ref ConnectionSourceIP

サブネットグループ ( Type: AWS::RDS::DBSubnetGroup )

次に RDS のサブネットについて見ていきます。 RDS インスタンスの配置するネットワークは最初は少々戸惑います。

RDS は RDS 専用のサブネットグループという場所に設置するのですが、これは RDS リソースから作成します。

サブネットグループとは通常の複数のサブネットを1つにまとめたものです。

Multi AZ にするには2つ以上のサブネットを選択して、サブネットグループを作成する必要があります。

セキュリティグループ ( Type: AWS::EC2::SecurityGroup )

そしてセキュリティグループの作成です。

RDS リソースに対するセキュリティグループなのに AWS::EC2::SecurityGroup でいいのかという感じがしますが、これで大丈夫です。

DB セキュリティグループと勘違いして作成しがちですが、DB セキュリティグループは VPC 内にない EC2-Classic DB インスタンスへのアクセスを制御するためのものなので今回は関係ありません。

セキュリティグループはインバウンドで MySQL と PostgreSQL のポートをアクセス元を限定して解放しています。アクセス元は CidrIp: !Ref ConnectionSourceIP で関連づけます。

参考

docs.aws.amazon.com

出力パラメータ

# ------------------------------------------------------------#
#  Output Parameters
# ------------------------------------------------------------#
Outputs:
  VPCID:
    Value: !Ref VPC
    Export:
      Name: !Sub "${PJPrefix}-vpc-id"
  VPCCIDR:
    Value: !Ref VPCCIDR
    Export:
      Name: !Sub "${PJPrefix}-vpc-cidr"
  #
  # RDS
  #
  RDSPublicSubnet1ID:
    Value: !Ref RDSPublicSubnet1
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-1-id"
  RDSPublicSubnet1CIDR:
    Value: !Ref RDSPublicSubnet1CIDR
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-1-cidr"
  RDSPublicSubnet2ID:
    Value: !Ref RDSPublicSubnet2
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-2-id"
  RDSPublicSubnet2CIDR:
    Value: !Ref RDSPublicSubnet2CIDR
    Export:
      Name: !Sub "${PJPrefix}-rds-public-subnet-2-cidr"
  #
  # Route
  #
  PublicRouteTableID:
    Value: !Ref PublicRouteTable
    Export:
      Name: !Sub "${PJPrefix}-public-route-id"

最後にアウトプットパラメータについてです。

これは今回の作業においては必要ない部分ですが、設定しておくと別のテンプレートからリソースを呼び出せたり、AWS コンソールて手軽に id などが確認できるので便利です。

まとめ

CloudFormation の利用例は Web サービスインフラの構築の例とかが多いと思いますが、拡張性がある形で設計するのは結構たいへんです。今回のように限定的な使われ方をするけど何回か使い回すような利用シーンだと手軽で便利に使えるのではないかと思います。


トヨクモでは一緒に働いてくれるエンジニアを募集しております。

採用に関する情報を公開しております。 気になった方はこちらからご応募ください。