こんにちは。トヨクモ開発本部インフラエンジニアの井上です。
AWS には CloudFormation というリソースの構築を自動化できるサービスがあります。 今回は CloudFormation を使って、堅牢な RDS ネットワークを構築してみたいと思います。
背景
日々データストアを運用していると、以下のようなことをやりたい場面が出てくるのではないでしょうか。
- 本番の RDS に対して、マイグレートやスクリプトの実行時間を検証したい
- 稼働中のEC2インスタンスからアクセスさせたくない
- 恒久的な構成ではなく一時的に作成、削除したい
やり方は色々あると思いますが弊社では次のような方法で解決しています。
方法
アクセス元が限定された検証用の堅牢なネットワークを CloudFormation で作成し、RDS のインスタンスをその VPC ネットワーク内に構築する。
RDS は本番のリードレプリカからマスターに昇格させたものを使います。これで本番のデータが壊れることはなくなります。 さらにアクセス元を弊社IPからのみに限定することで、セキュリティーを保ちデータを外部に流出させないようにします。
構成図は以下の通り。
ネットワークリソースの要件
- 検証用 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 ファイルの使い方を説明していきます。
スタックの作成
最初に AWS コンソールから CloudFormation リソースにアクセスし、スタックの作成を選択します。
テンプレートのアップロード
先ほどのコードをテンプレートとしてアップロードします。ファイル名はなんでもいいですが、拡張子は .yml
にします。
パラメータの入力
パラメータに適当な値を入れていきます。
AvailabilityZone は各リージョン毎に適したものをいれます。
東京なら ap-northeast-1b
などです。ここでは例としてロンドンリージョンに作成する場合を書きました。
Source IP
はアクセス元となる IP アドレスを CIDR 表記(ex. 12.34.56.78/32
)で書きます。
スタックオプションの設定
スタックオプションの設定は特に何も指定せずに 次へ
を選択します。
ステータス
数分待ってから CREATE_COMPLETE
という表示が出れば完了です。
これでアクセスが限定された堅牢 VPC の出来上がりです。あとは作成された環境に RDS インスタンスのリードレプリカを作ってマスターに昇格させてから、適宜ツールなどを用いてエンドポイントに接続します。
リソースの一括削除
最後に検証が終わったら、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
で関連づけます。
参考
出力パラメータ
# ------------------------------------------------------------# # 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 サービスインフラの構築の例とかが多いと思いますが、拡張性がある形で設計するのは結構たいへんです。今回のように限定的な使われ方をするけど何回か使い回すような利用シーンだと手軽で便利に使えるのではないかと思います。
トヨクモでは一緒に働いてくれるエンジニアを募集しております。