SQL Server FCIをFSx for Windowsを使って構築する(AWSリソース編)

RDS for SQL Serverは利用できないが、SQL Server on EC2をマルチAZで動かしたい場合の選択肢となる、 SQL Server Always OnのFCIを構築する機会があったので構築手順を紹介します。
2021.03.09

AWSでSQL Serverを利用する場合、マネージドなRDS for SQL Serverを利用することをオススメします。

しかし、さまざまな理由によってRDS for SQL Serverを採用できず、EC2上でSQL Serverを利用することもあります。

今回はそんなケースで、RDS for SQL Serverは利用できないが、SQL Server on EC2をマルチAZで動かしたい場合の選択肢となる、 SQL Server Always OnのFCI(フェールオーバークラスタインスタンス)を構築する機会があったので構築手順を紹介します。

2021/03/15追記

本ブログではAWSリソースのみ構築していきます。 Windows内部の構築についてはこちらのブログに書きましたのであわせて御覧ください。

参考サイト

このAWSブログを参考にSQL Server FCI環境を作ります。 本ブログではCloudFormationを使って、具体的なAWSリソースを構築していきます。

構成図

AWSの構成は次のような感じです。

SQL ServerのNodeとして使うEC2には、ENIを追加して3つのプライベートIPを付与します。 EC2はすべてこのブログを参考にCloudFormationでドメイン参加させます。

AWS環境を構築してみる

CloudFormationを使って前述の構成図の環境を構築します。 CloudFormationのテンプレートは長いので、ブログの最後に記します。

CFnテンプレートをアップロードして次へ進みます。

スタックの名前は適当につけます。今回は SQLServerFCISample という名前にしています。 各パラメーターは次のとおりに設定していきます。

KeyName
EC2のキーペアを選択します。
MicrosoftAdDomainName
Microsoft ADに設定するドメイン名を入力します。
MicsoftAdPassword
Microsoft ADに設定するパスワードを入力します。
MyCidr
Bastion用のEC2に接続可能なCIDRを指定します。設定したCIDRのみセキリュティグループでBastionサーバーへの接続を許可します。
SsmParameterValueWindowsServer2016JapaneseFullBaseParameter
Windows ServerのAMIが指定されているSSMパラメーターを指定します。特にデフォルトから変更する必要はありません。
VpcCidrPrefix
VPCのCIDRの第1,2オクテットを指定します。

スタックオプションの設定は特に必要ないので次へ進みます。

最後に設定を確認し、IAMの確認にチェックを入れてスタックの作成をします。

これでAWS環境の構築は完了です。

動作確認

まずはBationサーバーにリモートデスクトップで接続してみます。

BastionサーバーはElastic IPを付与しているので、パブリックIPをメモしておきます。

リモートデスクトップアプリを立ち上げて、先ほどメモしたパブリックIP宛に接続します。

EC2はCFnで既にドメイン参加してるので、ADユーザーでログインできるようになっています。 MicrosoftADの初期ユーザーである Admin ユーザーで接続できます。 パスワードは、CFnのパラメーターとして入力したパスワードと同じものです。

Bastionサーバーに接続できたら、次にSQL Server Node用のEC2に接続してみます。 SQL Server Node用のEC2は、プライベートIPを 10.22.10.11 (別AZのEC2は 10.22.11.11 )に固定しているので、そのIPにリモートデスクトップで接続します。

SQL Server Node用のEC2もADユーザーでログインできるようになっているので、同じ様にCFnのパラメーターとして入力したパスワードを入力します。

そうすると、SQL Server Node用のEC2へリモートデスクトップでアクセスできます。

次に、SQL Server Node用のEC2からFSx for Windowsで作成した共有ファイルサーバーへアクセスしてみます。

マネジメントコンソール画面から、作成したFSx for Windowsの DNS名 を確認します。

該当のネットワークドライブにアクセスして、新規テキストファイルを作ってみて、共有ファイルサーバーへ接続できることが確認できました。

(例:\fs-099d3b7de55175cf4.example.local\share)

(例:\amznfsxoinytskh.example.local\share)

終わりに

ここまで、FSx for Windowsを使ったSQL Server FCIのAWSリソースを構築しました。

次回は、このAWSリソースに対して、Windowsクラスターの構築とSQL ServerのインストールをしてSQL Server FCI環境を作っていきます。

CloudFormationテンプレート

※2021/03/15 SQL Server Node用のENIを修正

template.yml

AWSTemplateFormatVersion: "2010-09-09"

Description: 'SQL Server FCI Sample'

Parameters:
  SsmParameterValueWindowsServer2016JapaneseFullBaseParameter:
    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
    Default: /aws/service/ami-windows-latest/Windows_Server-2016-Japanese-Full-Base

  VpcCidrPrefix:
    Type: String
    Description: '(Example: 10.22)'
    Default: 10.22

  MicrosoftAdDomainName:
    Type: String
    Description: '(Example: example.local)'
    Default: example.local

  MicrosoftAdPassword:
    Type: String
    NoEcho: True

  MyCidr:
    Type: String
    Description: '(Example: 192.0.2.100/32)'

  KeyName:
    Type: AWS::EC2::KeyPair::KeyName
    Default: cm-kitano-try

Resources:
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Sub '${VpcCidrPrefix}.0.0/16'
      EnableDnsHostnames: true
      EnableDnsSupport: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-vpc'

  VpcPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Sub '${VpcCidrPrefix}.0.0/24'
      VpcId: !Ref Vpc
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-subnet1'

  VpcPublicSubnet1RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-subnet1-routetable'

  VpcPublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref VpcPublicSubnet1RouteTable
      SubnetId: !Ref VpcPublicSubnet1

  VpcPublicSubnet1DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref VpcPublicSubnet1RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref VpcIGW
    DependsOn:
      - VpcVPCGW

  VpcPublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Sub '${VpcCidrPrefix}.1.0/24'
      VpcId: !Ref Vpc
      AvailabilityZone: !Select
        - 1
        - !GetAZs
          Ref: AWS::Region
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-subnet1'

  VpcPublicSubnet2RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-subnet2-routetable'

  VpcPublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref VpcPublicSubnet2RouteTable
      SubnetId: !Ref VpcPublicSubnet2

  VpcPublicSubnet2DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref VpcPublicSubnet2RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref VpcIGW
    DependsOn:
      - VpcVPCGW

  VpcPublicSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Sub '${VpcCidrPrefix}.2.0/24'
      VpcId: !Ref Vpc
      AvailabilityZone: !Select
        - 2
        - !GetAZs
          Ref: AWS::Region
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-subnet3'

  VpcPublicSubnet3RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-subnet3-routetable'

  VpcPublicSubnet3RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref VpcPublicSubnet3RouteTable
      SubnetId: !Ref VpcPublicSubnet3

  VpcPublicSubnet3DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref VpcPublicSubnet3RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref VpcIGW
    DependsOn:
      - VpcVPCGW

  VpcIGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-igw'

  VpcVPCGW:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref Vpc
      InternetGatewayId: !Ref VpcIGW

  VpcPrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Sub '${VpcCidrPrefix}.10.0/24'
      VpcId: !Ref Vpc
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-subnet1'

  VpcPrivateSubnet1RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-subnet1-routetable'

  VpcPrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref VpcPrivateSubnet1RouteTable
      SubnetId: !Ref VpcPrivateSubnet1

  VpcPrivateSubnet1DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref VpcPrivateSubnet1RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref VpcPublicSubnet2NATGateway

  VpcPrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Sub '${VpcCidrPrefix}.11.0/24'
      VpcId: !Ref Vpc
      AvailabilityZone: !Select
        - 1
        - !GetAZs
          Ref: AWS::Region
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-subnet1'

  VpcPrivateSubnet2RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-subnet2-routetable'

  VpcPrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref VpcPrivateSubnet2RouteTable
      SubnetId: !Ref VpcPrivateSubnet2

  VpcPrivateSubnet2DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref VpcPrivateSubnet2RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref VpcPublicSubnet2NATGateway

  VpcPrivateSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Sub '${VpcCidrPrefix}.12.0/24'
      VpcId: !Ref Vpc
      AvailabilityZone: !Select
        - 2
        - !GetAZs
          Ref: AWS::Region
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-subnet3'

  VpcPrivateSubnet3RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-subnet3-routetable'

  VpcPrivateSubnet3RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref VpcPrivateSubnet3RouteTable
      SubnetId: !Ref VpcPrivateSubnet3

  VpcPrivateSubnet3DefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref VpcPrivateSubnet3RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref VpcPublicSubnet2NATGateway

  VpcPublicSubnetNATGatewayEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-natgateway-eip'

  VpcPublicSubnet2NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt VpcPublicSubnetNATGatewayEIP.AllocationId
      SubnetId: !Ref VpcPublicSubnet2
      Tags:
        - Key: Name
          Value:  !Sub '${AWS::StackName}-natgateway'

  MicrosoftAD:
    Type: AWS::DirectoryService::MicrosoftAD
    Properties:
      Name: !Ref MicrosoftAdDomainName
      Password: !Ref MicrosoftAdPassword
      VpcSettings:
        SubnetIds:
          - !Ref VpcPrivateSubnet1
          - !Ref VpcPrivateSubnet2
        VpcId: !Ref Vpc
      Edition: Standard

  DHCPOptions:
    Type: AWS::EC2::DHCPOptions
    Properties:
      DomainName: !Ref MicrosoftAdDomainName
      DomainNameServers: !GetAtt MicrosoftAD.DnsIpAddresses
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-dhcp-options'

  VPCDHCPOptionsAssociation:
    Type: AWS::EC2::VPCDHCPOptionsAssociation
    Properties:
      DhcpOptionsId: !Ref DHCPOptions
      VpcId: !Ref Vpc

  SqlServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub '${AWS::StackName}-sql-server-sg'
      GroupName: !Sub '${AWS::StackName}-sql-server-sg'
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic by default
          IpProtocol: "-1"
      SecurityGroupIngress:
        - CidrIp: !GetAtt Vpc.CidrBlock
          Description: allow all access from vpc
          IpProtocol: "-1"
      VpcId: !Ref Vpc

  BastionSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub '${AWS::StackName}-bastion-sg'
      GroupName: !Sub '${AWS::StackName}-bastion-sg'
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic by default
          IpProtocol: "-1"
      SecurityGroupIngress:
        - CidrIp: !GetAtt Vpc.CidrBlock
          Description: allow all access from vpc
          IpProtocol: "-1"
        - CidrIp: !Ref MyCidr
          Description: allow all access from mycidr
          FromPort: 0
          IpProtocol: tcp
          ToPort: 65535
      VpcId: !Ref Vpc

  Ec2IamRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore'
        - 'arn:aws:iam::aws:policy/AmazonSSMDirectoryServiceAccess'
      RoleName: !Sub '${AWS::StackName}-iam-role'

  Ec2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref Ec2IamRole

  Bastion:
    Type: AWS::EC2::Instance
    Properties:
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
      IamInstanceProfile: !Ref Ec2InstanceProfile
      ImageId: !Ref SsmParameterValueWindowsServer2016JapaneseFullBaseParameter
      InstanceInitiatedShutdownBehavior: stop
      InstanceType: t3.small
      KeyName: !Ref KeyName
      SecurityGroupIds:
        - !Ref BastionSecurityGroup
      SsmAssociations:
        - AssociationParameters:
            - Key: directoryId
              Value:
                - !Ref MicrosoftAD
            - Key: directoryName
              Value:
                - !Ref MicrosoftAdDomainName
          DocumentName: AWS-JoinDirectoryServiceDomain
      SubnetId: !Ref VpcPublicSubnet1
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-bastion'

  BastionEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      InstanceId: !Ref Bastion
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-bastion-eip'

  SqlServer1:
    Type: AWS::EC2::Instance
    Properties:
      IamInstanceProfile: !Ref Ec2InstanceProfile
      ImageId: !Ref SsmParameterValueWindowsServer2016JapaneseFullBaseParameter
      InstanceInitiatedShutdownBehavior: stop
      InstanceType: t3.large
      KeyName: !Ref KeyName
      NetworkInterfaces:
        - DeleteOnTermination: true
          DeviceIndex: 0
          GroupSet:
            - !Ref SqlServerSecurityGroup
          PrivateIpAddresses:
            - Primary: true
              PrivateIpAddress: !Sub '${VpcCidrPrefix}.10.11'
            - Primary: false
              PrivateIpAddress: !Sub '${VpcCidrPrefix}.10.12'
            - Primary: false
              PrivateIpAddress: !Sub '${VpcCidrPrefix}.10.13'
          SubnetId: !Ref VpcPrivateSubnet1
      SsmAssociations:
        - AssociationParameters:
            - Key: directoryId
              Value:
                - !Ref MicrosoftAD
            - Key: directoryName
              Value:
                - !Ref MicrosoftAdDomainName
          DocumentName: AWS-JoinDirectoryServiceDomain
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-sqlserver1'

  SqlServer2:
    Type: AWS::EC2::Instance
    Properties:
      IamInstanceProfile: !Ref Ec2InstanceProfile
      ImageId: !Ref SsmParameterValueWindowsServer2016JapaneseFullBaseParameter
      InstanceInitiatedShutdownBehavior: stop
      InstanceType: t3.large
      KeyName: !Ref KeyName
      NetworkInterfaces:
        - DeleteOnTermination: true
          DeviceIndex: 0
          GroupSet:
            - !Ref SqlServerSecurityGroup
          PrivateIpAddresses:
            - Primary: true
              PrivateIpAddress: !Sub '${VpcCidrPrefix}.11.11'
            - Primary: false
              PrivateIpAddress: !Sub '${VpcCidrPrefix}.11.12'
            - Primary: false
              PrivateIpAddress: !Sub '${VpcCidrPrefix}.11.13'
          SubnetId: !Ref VpcPrivateSubnet2
      SsmAssociations:
        - AssociationParameters:
            - Key: directoryId
              Value:
                - !Ref MicrosoftAD
            - Key: directoryName
              Value:
                - !Ref MicrosoftAdDomainName
          DocumentName: AWS-JoinDirectoryServiceDomain
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-sqlserver2'

  FSXSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Sub '${AWS::StackName}-fsx-sg'
      GroupName: !Sub '${AWS::StackName}-fsx-sg'
      SecurityGroupEgress:
        - CidrIp: 0.0.0.0/0
          Description: Allow all outbound traffic by default
          IpProtocol: "-1"
      SecurityGroupIngress:
        - CidrIp: !GetAtt Vpc.CidrBlock
          Description: allow all access from vpc
          IpProtocol: "-1"
      VpcId: !Ref Vpc

  DataFSX:
    Type: AWS::FSx::FileSystem
    Properties:
      FileSystemType: WINDOWS
      SubnetIds:
        - !Ref VpcPrivateSubnet1
        - !Ref VpcPrivateSubnet2
      SecurityGroupIds:
        - !GetAtt FSXSecurityGroup.GroupId
      StorageCapacity: 32
      StorageType: SSD
      WindowsConfiguration:
        ActiveDirectoryId: !Ref MicrosoftAD
        AutomaticBackupRetentionDays: 0
        DeploymentType: MULTI_AZ_1
        PreferredSubnetId: !Ref VpcPrivateSubnet1
        ThroughputCapacity: 8

  QuorumFSX:
    Type: AWS::FSx::FileSystem
    Properties:
      FileSystemType: WINDOWS
      SubnetIds:
        - !Ref VpcPrivateSubnet3
      SecurityGroupIds:
        - !GetAtt FSXSecurityGroup.GroupId
      StorageCapacity: 32
      StorageType: SSD
      WindowsConfiguration:
        ActiveDirectoryId: !Ref MicrosoftAD
        AutomaticBackupRetentionDays: 0
        DeploymentType: SINGLE_AZ_1
        ThroughputCapacity: 8