なにかの技術メモ置き場

なにかの技術メモ置き場

@インフラエンジニア

AWS CloudFormationを使ってみた - Route 53/EC2によるDR環境構築

f:id:none06:20210710190259p:plain

概要

AWS CloudFormationでインフラ構築を自動化する。
Route 53を使用したDR(Disaster Recovery)環境を構築する。

目的

  • CloudFormationに慣れる
  • 構築の自動化
  • 構築の冪等性の確保
  • 構築内容・手順の可視化(IaC)

今回作成する構成

f:id:none06:20210710190259p:plain

処理概要

スタック1 - プライマリサイトのEC2インスタンスの構築

  • VPCを作成
  • サブネットを作成
  • インタネットゲートウェイを作成
  • ルートテーブルを作成
  • ルートテーブルをサブネットに紐づけ
  • Elastic IPを作成
  • Elastic IPをECインスタンスにアタッチ
  • EC2セキュリティグループを作成
  • EC2インスタンスを作成
  • Elastic IPをアウトプット/エクスポート

スタック2 - セカンダリサイトのEC2インスタンスの構築

  • VPCを作成
  • サブネットを作成
  • インタネットゲートウェイを作成
  • ルートテーブルを作成
  • ルートテーブルをサブネットに紐づけ
  • Elastic IPを作成
  • Elastic IPをECインスタンスにアタッチ
  • EC2セキュリティグループを作成
  • EC2インスタンスを作成
  • Elastic IPをアウトプット/エクスポート

スタック3 - Route53でのフェイルオーバールーティングの設定

  • ホストゾーンの作成
  • プライマリサイト向けDNSレコードの作成
  • セカンダリサイト向けDNSレコードの作成
  • ヘルスチェックの作成

作成したテンプレート

template01.yaml

スタック1、スタック2用のテンプレート。処理が全く同じなので同じテンプレートをを使用する。というか管理する資材(テンプレート)を増やしたくなかったため、マルチリージョン・マルチサイトに対応するよう設計した。

AWSTemplateFormatVersion: "2010-09-09"
Description: TBD

Parameters:
  # EC2 Instance Type
  InstanceType:
    Description: EC2 Instance type
    Type: String
    Default: t2.micro
    AllowedValues:
    - t1.micro
    - t2.nano
    - t2.micro
    - t2.small
    - t2.medium
    - t2.large
    ConstraintDescription: must be a valid EC2 instance type

Mappings:
  RegionAmiMap:
    ap-northeast-1:
      hvm: ami-001f026eaf69770b4
    ap-southeast-1:
      hvm: ami-0e8e39877665a7c92
  RegionAzMap:
    ap-northeast-1:
      AZ: ap-northeast-1a
    ap-southeast-1:
      AZ: ap-southeast-1a
  RegionKeypairMap:
    ap-northeast-1:
      KeyPair: keypair-ap-northeast-1
    ap-southeast-1:
      KeyPair: keypair-ap-southeast-1
  RegionSiteMap:
    ap-northeast-1:
      Site: Primary
    ap-southeast-1:
      Site: Secondary

Resources:
  # VPC
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      Tags:
      - Key: Application
        Value: !Ref AWS::StackId
      - Key: Name
        Value: !Sub ${AWS::StackName}-vpc
  # Subnet
  Subnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: VPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: !FindInMap [RegionAzMap, !Ref AWS::Region, AZ]
      Tags:
      - Key: Application
        Value: !Ref AWS::StackId
      - Key: Name
        Value: !Sub ${AWS::StackName}-sbunet
  # Internet Gateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Application
        Value: !Ref AWS::StackId
      - Key: Name
        Value: !Sub ${AWS::StackName}-igw
  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway
  # Route Table
  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Application
        Value: !Ref AWS::StackId
      - Key: Name
        Value: !Sub ${AWS::StackName}-rtb
  # Route
  Route:
    Type: AWS::EC2::Route
    DependsOn: AttachGateway
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  # Subnet Route Table Association
  SubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref Subnet
      RouteTableId: !Ref RouteTable
  # Elastic IP Address
  EIP:
    Type: AWS::EC2::EIP
    DependsOn: AttachGateway
    Properties:
      Domain: vpc
      InstanceId: !Ref EC2Instance
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-eip
  AttachEIP:
    Type: AWS::EC2::EIPAssociation
    Properties:
      AllocationId: !GetAtt EIP.AllocationId
      InstanceId: !Ref EC2Instance
  # Instance Security Group
  InstanceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      VpcId: !Ref VPC
      GroupDescription: Accept SSH, HTTP
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 22
        ToPort: 22
        CidrIp: 0.0.0.0/0
      - IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        CidrIp: 0.0.0.0/0
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-securitygroup
  # EC2 Instance 
  EC2Instance:
    Type: AWS::EC2::Instance
    DependsOn: AttachGateway
    Properties:
      ImageId: !FindInMap [RegionAmiMap, !Ref AWS::Region, hvm]
      AvailabilityZone: !FindInMap [RegionAzMap, !Ref AWS::Region, AZ]
      InstanceType: !Ref InstanceType
      KeyName: !FindInMap [RegionKeypairMap, !Ref AWS::Region, KeyPair]
      NetworkInterfaces:
      - GroupSet:
        - Ref: InstanceSecurityGroup
        AssociatePublicIpAddress: true
        DeviceIndex: 0
        DeleteOnTermination: true
        SubnetId: !Ref Subnet
      UserData:
        Fn::Base64: !Sub
          - |
            #!/bin/bash -xe
            sudo su -
            yum -y install httpd
            cat <<EOF > /var/www/html/index.html
            <html><h1>Here is ${SITE}!</h1></html>
            EOF
            cat /var/www/html/index.html
            systemctl enable httpd.service --now
            systemctl status httpd.service
            curl http://localhost/
            exit
            exit
          - {
              SITE: !FindInMap [RegionSiteMap, !Ref AWS::Region, Site]
            }
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-instance

Outputs:
  EIP:
    Value: !Ref EIP
    Export:
      Name: !Join ["", [!FindInMap [RegionSiteMap, !Ref AWS::Region, Site], EIP] ]

template02.yaml

スタック3用のテンプレート。

AWSTemplateFormatVersion: "2010-09-09"
Description: TBD

Parameters:
  # Hosted Zone
  HostedZoneName:
    Type: String
    Description: DNS Name to create
    Default: example.co.jp
    AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?<!-)
    ConstraintDescription: must be a valid DNS zone name
  # Secondary Elastic IP
  SecondaryEIP:
    Type: String
    Description: Secondary Elastip IP
    Default: _SecondaryEIP_

Resources:
  # Hosted Zone
  HostedZone:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: !Ref HostedZoneName
      HostedZoneTags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-hostedzone
  # Primary DNS Record
  PrimaryDNSRecord:
    Type: AWS::Route53::RecordSet
    DependsOn: HostedZone
    Properties:
      HostedZoneId: !Ref HostedZone
      Comment: DNS Name for Elastic IP Address
      Name: !Join ["", [www, ".", !Ref HostedZoneName, "."] ]
      Type: A
      TTL: 60
      ResourceRecords:
      - !ImportValue PrimaryEIP
      Failover: PRIMARY
      HealthCheckId: !Ref HealthCheck
      SetIdentifier: ec2-primary
  # Secondary DNS Record
  SecondaryDNSRecord:
    Type: AWS::Route53::RecordSet
    DependsOn: HostedZone
    Properties:
      HostedZoneId: !Ref HostedZone
      Comment: DNS Name for Elastic IP Address
      Name: !Join ["", [www, ".", !Ref HostedZoneName, "."] ]
      Type: A
      TTL: 60
      ResourceRecords:
      #- !ImportValue SecondaryEIP
      - !Ref SecondaryEIP
      Failover: SECONDARY
      #HealthCheckId: !Ref HealthCheck
      SetIdentifier: ec2-secondary
  # HealthCheck
  HealthCheck:
    Type: AWS::Route53::HealthCheck
    Properties:
      HealthCheckConfig:
        Type: HTTP
        IPAddress: !ImportValue PrimaryEIP
        Port: 80
        FullyQualifiedDomainName: !Join ["", [www, ".", !Ref HostedZoneName] ]
        ResourcePath: /index.html
        RequestInterval: 30
        FailureThreshold: 3
      HealthCheckTags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-healthcheck

前提条件

  • プライマリサイトとセカンダリサイトのリージョンは異なる場所にする。DRなので。

サイト リージョン 補足
プライマリ ap-norheast-1 東京
セカンダリ ap-southeast-1 シンガポール

キーペア名 リージョン
keypair-ap-norheast-1 ap-norheast-1
keypair-ap-southeast-1 ap-southeast-1

  • ドメイン名はexample.co.jpとする。
  • スタック1とスタック3は同一リージョンのCloudFormationで作成する。
    • Elastic IPの値をスタック1からExportし、スタック3にImportするため。Export/Importは同一リージョン内でのみ可能。
    • スタック2はリージョンが異なるためExport/Importができない。そのため、手動で値をコピーする方式とした。自動化の検討ポイントである。

スタックの作成

今までは1つのスタックだけだったため気にする必要がなかったが、今回は3つのスタックを順序性を意識して実行する。

  • 1と2は順不同。並列実行も可能。
  • 3の開始条件は「1の完了」かつ「2の完了」。順序性を意識するのはここだけ。

1.プライマリサイトの構築

  • プライマリサイトのリージョン(ap-northeast-1)のCloudFormationでスタック1を作成する。
  • テンプレートはtemplate01.yml。
  • スタック名は任意。

2.セカンダリサイトの構築

  • セカンダリサイトのリージョン(ap-southeast-1)のCloudFormationでスタック2を作成する。
  • テンプレートはtemplate01.yml。
  • スタック名は任意。

3.フェイルオーバールーティングの設定

  • スタック2の出力より「SecondaryEIP」の値をコピーする。
  • プライマリサイトのリージョン(ap-northeast-1)のCloudFormationでスタック3を作成する。
  • テンプレートはtemplate02.yml。
  • スタック名は任意。
  • パラメータ「SecondaryEIP」にはコピーしておいた「SecondaryEIP」の値を入力する。

参考

これらをAWS CLILinuxコマンドで表したものを掲載しておく。

# 1.プライマリサイトの構築
aws cloudformation create-stack --stack-name stack01 --region ap-northeast-1 --template-body file://template01.yml
aws cloudformation describe-stacks --stack-name stack01 --region ap-northeast-1

# 2.セカンダリサイトの構築
aws cloudformation create-stack --stack-name stack02 --region ap-southeast-1 --template-body file://template01.yml
aws cloudformation describe-stacks --stack-name stack02 --region ap-southeast-1

# 3.フェイルオーバールーティングの設定
_SecondaryEIP=`aws cloudformation describe-stacks --stack-name stack02 --region ap-southeast-1 --output text | grep OUTPUTS | grep SecondaryEIP | awk '{print $4}'`
sed "s/_SecondaryEIP_/${_SecondaryEIP}/g" template02.yml > template02_tmp.yml
diff template02.yml template02_tmp.yml
aws cloudformation create-stack --stack-name stack03 --region ap-northeast-1 --template-body file://template02_tmp.yml
aws cloudformation describe-stacks --stack-name stack03 --region ap-northeast-1

スタック作成後の作業

1.ドメインの取得

任意のサービスでドメイン名「example.co.jp」を取得する。有料だがAWSでも取得できる。
なお、スタック作成前でも可。

2.ドメインサービス側の設定

Route53のホストゾーンのNSレコードのDNSサーバ名を、ドメインサービス側のDNSサーバに登録する。

3.動作確認

http://www.example.co.jpにアクセスし、以下を確認する。

  • プライマリサイトの正常稼働時は「Here is Primary!」と表示されること。
  • プライマリサイトを停止すると「Here is Secondary!」に表示が変わること。
  • プライマリサイトを復旧すると「Here is Primary!」に表示が変わること。

あとがき

まだまだ自動化の余地がある。スタックが完了したら次のスタックを起動するなどの処理はAWSのサービスで実現可能なはず。