お名前.comで購入したドメインをALBでECSにホストベースルーティングするCloudformation

# 概要
お名前.comで購入したドメインについて、ALBを使ってホストベースのルーティングを行う。
今回のルーティング先はECSのサービスとする。
複数のドメインを使ってホストベースルーティングする場合でも、1台のALBで対応できる。また、ACMのSSL証明書も1つで済む。

# 事前準備
事前にお名前.comで`example.com`と`test.com`のドメインを購入しているとする。
（もちろん、`example.com`だけでもOK）
お名前.comでは、NSレコードを使ってAWSのRoute53のホストゾーンにドメインないしはサブドメインの管理を委任することができる。
ここでは、Route53で`example.com`および`test.com`のホストゾーンを作成し、そのNSレコードをお名前.comで設定する。

https://blog.i-tale.jp/2020/04/13_02/

https://nori-life.com/subdomain-for-aws/

# 実装
Route53で`example.com`およびに`test.com`対してホストゾーンを作ったが、これらのドメインに対してはAliasレコード、サブドメインである`www.example.com`に対してはCNAMEレコードを設定し、向き先は全てALBとする。
ALBのリスナールールで、リクエストのヘッダーに含まれるドメインに応じたルーティングを設定できる。

ACMの設定も`SubjectAlternativeNames`を指定すると、複数のドメインに対して有効となる。

![スクリーンショット 2022-04-15 6.16.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/c2f73eef-b95c-985c-83c8-12f793019e28.png)




```cloudformaiton.yml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  DomainName1:
    Description: FQDN of the HostZone　example.com
    Type: String
    Default: example.com
  DomainName2:
    Description: FQDN of the HostZone　test.com
    Type: String
    Default: test.com
  SubjectAlternativeName:
    Description: FQDN of the Subdomains. # www.example.com, acc.examle.comなど全てをカバー
    Type: String
    Default: '*.example.com'
  SubDomain:
    Description: FQDN of the Subdomain
    Type: String
    Default: www.example.com
  HostZoneId1:
    Description: host zone id of example.com
    Type: String
    Default: <input-your-hostzone-id>
  HostZoneId2:
    Description: host zone id of test.com
    Type: String
    Default: <input-your-hostzone-id>
  EnvironmentName:
    Type: String
    Default: alb-ecs
  VpcId:
    Type: String
    Default: <input-your-vpc-id>
  PublicSubnet1:
    Type: String
    Default: <input-your-subnet-id>
  PublicSubnet2:
    Type: String
    Default: <input-your-subnet-id>
  PublicSubnet3:
    Type: String
    Default: <input-your-subnet-id>
  PublicAlbSG:
    Type: String
    Default: <input-your-security-group-id-for-ALB>
  PrivateSubnet1:
    Type: String
    Default: <input-your-subnet-id>
  PrivateSubnet2:
    Type: String
    Default: <input-your-subnet-id>
  PrivateSubnet3:
    Type: String
    Default: <input-your-subnet-id>
  EcsTaskSG:
    Type: String
    Default: <input-your-security-group-id-for-ECS-task>
  ClusterArn:
    Type: String
    Default: <input-your-ECS-cluster-ARN>
  ExecutionRoleArn:
    Type: String
    Default: <input-your-ECS-role-ARN>
  TaskRoleArn:
    Type: String
    Default: <input-your-Task-role-ARN>
  ServiceName1:
    Type: String
    Default: <input-your-service-name-for-application1>
  ServiceName2:
    Type: String
    Default: <input-your-service-name-for-application2>
  ServiceName3:
    Type: String
    Default: <input-your-service-name-for-application3>
  ImageUrl1:
    Type: String
    Default: <input-your-image-url-of-application1>
  ImageUrl2:
    Type: String
    Default: <input-your-image-url-of-application2>
  ImageUrl3:
    Type: String
    Default: <input-your-image-url-of-application2>
  ContainerPort:
    Type: Number
    Default: <inout-your-port-number-of-task>
  ContainerCpu:
    Type: Number
    Default: 256
    Description: How much CPU to give the container. 1024 is 1 CPU
  ContainerMemory:
    Type: Number
    Default: 512
    Description: How much memory in megabytes to give the container
  DesiredCount:
    Type: Number
    Default: 1
    Description: How many copies of the service task to run

Resources:

  #　複数のドメインやサブドメインに対しても、ACMは一つで対応可
  ACMCertificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: !Sub ${DomainName1}
      SubjectAlternativeNames:
        - !Sub ${SubjectAlternativeName}
        - !Sub ${DomainName2}
      DomainValidationOptions:
        - HostedZoneId: !Sub ${HostZoneId1}
          DomainName: !Sub ${DomainName1}
      ValidationMethod: DNS

  PublicLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      LoadBalancerAttributes:
        - Key: idle_timeout.timeout_seconds
          Value: '30'
      Subnets:
        - !Ref PublicSubnet1
        - !Ref PublicSubnet2
        - !Ref PublicSubnet3
      SecurityGroups:
        - !Ref PublicAlbSG
       
  Domain1Record: # example.com
    Type: AWS::Route53::RecordSetGroup
    Properties:
      HostedZoneId: !Sub ${HostZoneId1}
      Comment: Zone apex alias targeted to public LoadBalancer.
      RecordSets:
        - Name: !Sub ${DomainName1}
          Type: A
          AliasTarget:
            HostedZoneId: !GetAtt PublicLoadBalancer.CanonicalHostedZoneID
            DNSName: !GetAtt PublicLoadBalancer.DNSName

  Domain1SubDomainRecord: # www.example.com
    Type: AWS::Route53::RecordSet
    DependsOn: PublicLoadBalancer
    Properties:
      HostedZoneId: !Sub ${HostZoneId1}
      Name: !Sub ${SubDomain}
      Type: CNAME
      TTL: 60
      ResourceRecords:
        - !GetAtt PublicLoadBalancer.DNSName

  Domain2Record: # test.com
    Type: AWS::Route53::RecordSetGroup
    Properties:
      HostedZoneId: !Sub ${HostZoneId2}
      Comment: Zone apex alias targeted to public LoadBalancer.
      RecordSets:
        - Name: !Sub ${DomainName2}
          Type: A
          AliasTarget:
            HostedZoneId: !GetAtt PublicLoadBalancer.CanonicalHostedZoneID
            DNSName: !GetAtt PublicLoadBalancer.DNSName

  LogGroup1:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ${EnvironmentName}-service-${ServiceName1}

  LogGroup2:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ${EnvironmentName}-service-${ServiceName2}

  LogGroup3:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ${EnvironmentName}-service-${ServiceName3}

  TaskDefinition1:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Ref ServiceName1
      Cpu: !Ref ContainerCpu
      Memory: !Ref ContainerMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !Ref ExecutionRoleArn
      TaskRoleArn: !Ref TaskRoleArn
      ContainerDefinitions:
        - Name: !Ref ServiceName1
          Cpu: !Ref ContainerCpu
          Memory: !Ref ContainerMemory
          Essential: true
          Image: !Ref ImageUrl1
          PortMappings:
            - ContainerPort: !Ref ContainerPort
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Sub ${EnvironmentName}-service-${ServiceName1}
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: !Ref ServiceName1

  TaskDefinition2:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Ref ServiceName2
      Cpu: !Ref ContainerCpu
      Memory: !Ref ContainerMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !Ref ExecutionRoleArn
      TaskRoleArn: !Ref TaskRoleArn
      ContainerDefinitions:
        - Name: !Ref ServiceName2
          Cpu: !Ref ContainerCpu
          Memory: !Ref ContainerMemory
          Essential: true
          Image: !Ref ImageUrl2
          PortMappings:
            - ContainerPort: !Ref ContainerPort
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-group: !Sub ${EnvironmentName}-service-${ServiceName2}
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: !Ref ServiceName2

  TaskDefinition3:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: !Ref ServiceName3
      Cpu: !Ref ContainerCpu
      Memory: !Ref ContainerMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ExecutionRoleArn: !Ref ExecutionRoleArn
      TaskRoleArn: !Ref TaskRoleArn
      ContainerDefinitions:
        - Name: !Ref ServiceName3
          Cpu: !Ref ContainerCpu
          Memory: !Ref ContainerMemory
          Essential: true
          Image: !Ref ImageUrl3
          PortMappings:
            - ContainerPort: !Ref ContainerPort
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-group: !Sub ${EnvironmentName}-service-${ServiceName3}
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: !Ref ServiceName3

  EcsService1:
    Type: AWS::ECS::Service
    DependsOn:
      - HTTPSLoadBalancerListener
    Properties:
      ServiceName: !Ref ServiceName1
      Cluster: !Ref ClusterArn
      LaunchType: FARGATE
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 75
      DesiredCount: !Ref DesiredCount
      HealthCheckGracePeriodSeconds: 300 
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED # in case of no public ip is needed
          SecurityGroups:
            - !Ref EcsTaskSG
          Subnets:
            - !Ref PrivateSubnet1
            - !Ref PrivateSubnet2
            - !Ref PrivateSubnet3
      TaskDefinition: !Ref TaskDefinition1
      LoadBalancers:
        - ContainerName: !Ref ServiceName1
          ContainerPort: !Ref ContainerPort
          TargetGroupArn: !Ref TargetGroup1

  EcsService2:
    Type: AWS::ECS::Service
    DependsOn:
      - HTTPSLoadBalancerListener
    Properties:
      ServiceName: !Ref ServiceName2
      Cluster: !Ref ClusterArn
      LaunchType: FARGATE
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 75
      DesiredCount: !Ref DesiredCount
      HealthCheckGracePeriodSeconds: 300
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED 
          SecurityGroups:
            - !Ref EcsTaskSG
          Subnets:
            - !Ref PrivateSubnet1
            - !Ref PrivateSubnet2
            - !Ref PrivateSubnet3
      TaskDefinition: !Ref TaskDefinition2
      LoadBalancers:
        - ContainerName: !Ref ServiceName2
          ContainerPort: !Ref ContainerPort
          TargetGroupArn: !Ref TargetGroup2

  EcsService3:
    Type: AWS::ECS::Service
    DependsOn:
      - HTTPSLoadBalancerListener
    Properties:
      ServiceName: !Ref ServiceName3
      Cluster: !Ref ClusterArn
      LaunchType: FARGATE
      DeploymentConfiguration:
        MaximumPercent: 200
        MinimumHealthyPercent: 75
      DesiredCount: !Ref DesiredCount
      HealthCheckGracePeriodSeconds: 300
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: DISABLED 
          SecurityGroups:
            - !Ref EcsTaskSG
          Subnets:
            - !Ref PrivateSubnet1
            - !Ref PrivateSubnet2
            - !Ref PrivateSubnet3
      TaskDefinition: !Ref TaskDefinition3
      LoadBalancers:
        - ContainerName: !Ref ServiceName3
          ContainerPort: !Ref ContainerPort
          TargetGroupArn: !Ref TargetGroup3

  HTTPLoadBalancerListener: # 80=>443に転送
    Type: AWS::ElasticLoadBalancingV2::Listener
    DependsOn:
      - PublicLoadBalancer
    Properties:
      DefaultActions:
        - Type: redirect
          RedirectConfig:
            Protocol: HTTPS
            Port: 443
            Host: '#{host}'
            Path: '/#{path}'
            Query: '#{query}'
            StatusCode: HTTP_301
      LoadBalancerArn: !Ref PublicLoadBalancer
      Port: 80
      Protocol: HTTP

  HTTPSLoadBalancerListener: # SSL設定してTargetGroupに転送
    Type: AWS::ElasticLoadBalancingV2::Listener
    DependsOn: PublicLoadBalancer
    Properties:
      LoadBalancerArn: !Ref PublicLoadBalancer
      Port: 443
      Protocol: HTTPS
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup1
      SslPolicy: ELBSecurityPolicy-2016-08
      Certificates:
        - CertificateArn: !Ref ACMCertificate

  ListenerRule1:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      ListenerArn: !Ref HTTPSLoadBalancerListener
      Priority: 2
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - !Ref DomainName1
      Actions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup1

  TargetGroup1:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    DependsOn: PublicLoadBalancer
    Properties:
      HealthCheckIntervalSeconds: 300 
      HealthCheckPath: / # input healthcheck path
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 120
      HealthyThresholdCount: 2
      TargetType: ip #fargateなのでVPC内のipをtargetにする。EC2であればinstanceがtargetになる
      Name: !Ref ServiceName1
      Port: !Ref ContainerPort
      Protocol: HTTP
      UnhealthyThresholdCount: 2
      VpcId: !Ref VpcId

  ListenerRule2:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      ListenerArn: !Ref HTTPSLoadBalancerListener
      Priority: 1
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - !Ref SubDomain
      Actions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup2

  TargetGroup2:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    DependsOn: PublicLoadBalancer
    Properties:
      HealthCheckIntervalSeconds: 300
      HealthCheckPath: / # input healthcheck path
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 120
      HealthyThresholdCount: 2
      TargetType: ip #fargateなのでVPC内のipをtargetにする。EC2であればinstanceがtargetになる
      Name: !Ref ServiceName2
      Port: !Ref ContainerPort
      Protocol: HTTP
      UnhealthyThresholdCount: 2
      VpcId: !Ref VpcId

  ListenerRule3:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      ListenerArn: !Ref HTTPSLoadBalancerListener
      Priority: 3
      Conditions:
        - Field: host-header
          HostHeaderConfig:
            Values:
              - !Ref DomainName2
      Actions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup3

  TargetGroup3:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    DependsOn: PublicLoadBalancer
    Properties:
      HealthCheckIntervalSeconds: 300 
      HealthCheckPath: / # input healthcheck path
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 120
      HealthyThresholdCount: 2
      TargetType: ip 
      Name: !Ref ServiceName3
      Port: !Ref ContainerPort
      Protocol: HTTP
      UnhealthyThresholdCount: 2
      VpcId: !Ref VpcId

```


# Xserverでドメインを取得した場合

Xserverでドメインを取得した場合、お名前.comのようにRoute53のNSレコードを設定することができない。
つまりRoute53のホストゾーンは使わない。
(上記Cloudformationで言えば、`Domain1Record`,`Domain1SubDomainRecord`,`Domain2Record`が不要。)

この場合、Xserverの設定画面で、使用する全てのDNSに対してCNAMEを設定してALBに向けさせる。

![スクリーンショット 2022-04-15 6.16.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/572098/a0d4936b-b952-dd63-ccf4-f4b787ebfd62.png)



ACMの証明書を使う場合、Xserverの設定画面で証明書の認証設定が必要。下記参照。

https://blog.serverworks.co.jp/tech/2017/12/04/acm-dns/

https://omohikane.com/aws_acm_dns_validate_xserver/