【第28回】AWS CloudFormationを用いた基盤自動化(8)ApplicationLoadBalancer/TargetGroupの構築


本連載では、以下のイメージの構成にあるAWSリソース基盤自動化環境の構築を実践しています。


../_images/cloudformation-scope.png


前回は、各AWSリソースへ設定するセキュリティグループ、FrontendサブネットにアタッチするNATGatewayを構築するテンプレートを実装しました。 続く今回はFrontend、Backendサブネットに配置するアプリケーションロードバランサー(ALB)を構築するCloudFormationテンレプートを作成します。 実際のソースコードは GitHub 上にコミットしています。 ソースコード中で本質的でない記述を一部省略しているので、実行コードを作成する場合は、必要に応じて適宜GitHub上のソースコードも参照してください。


アプリケーションロードバランサー(ALB)スタック構築テンプレート


ALBは クラウドネイティブ基本第5回 で実施した要領で、FrontendサブネットとBackendサブネット双方に構築します。 CloudFormationで構築する場合、リソースタイプが、AWS::ElasticLoadBalancingV2::LoadBalancer となるロードバランサー自体の定義と、リクエストフォワード先の対象となるターゲットグループ定義 AWS::ElasticLoadBalancingV2::TargetGroup リクエストを受け付けるリスナー定義 AWS::ElasticLoadBalancingV2::Listener および URLパスパターンに応じたルーティング定義などを行うルール AWS::ElasticLoadBalancingV2::ListenerRule の設定が必要になります。 プロパティとして設定可能な属性は、各リンク先の通りですが、実装するテンプレートを以下のように2つに分けて実装してみます。

  • ベースとなるALBおよびデフォルトターゲットグループとリスナー定義
  • サービスアプリケーションごとに追加するターゲットグループ、リスナールール定義

1つのテンプレートで、全ての定義をひとまとめにしておくのもシンプルで分かりやすくて良いですが、サービスアプリケーションを追加する必要が出てきた場合など、後者のリソース定義を再利用して追加対象だけを再実行すればよいテンプレート構成としておいた方が効率的です。 また、開発環境をEnvTypeとしてパラメータ化します。前者のテンプレートのサンプルは以下の通りです。


AWSTemplateFormatVersion: '2010-09-09'

Mappings:                                                                #(A)※
  DeployEnvironmentMap:
    Production:
      "Protocol": "HTTPS"
      "Port": 443
    Staging:
      "Protocol": "HTTP"
      "Port": 80
    Dev:
      "Protocol": "HTTP"
      "Port": 80

Parameters:

  // omit

  EnvType:                                                               #(B)
    Description: Which environments to deploy your service.
    Type: String
    AllowedValues: ["Dev", "Staging", "Production"]
    Default: Dev

  FrontendHealthCheckPath:                                               #(C)
    Description: Frontend WebApp health check path
    Type: String
    MinLength: 1
    MaxLength: 255
    AllowedPattern: ^[-¥.¥/a-zA-Z0-9]*$
    Default: /frontend/portal

  BackendDefaultHealthCheckPath:                                         #(D)
    Description: Backend Service Default health check path
    Type: String
    MinLength: 1
    MaxLength: 255
    AllowedPattern: ^[-¥.¥/a-zA-Z0-9]*$
    Default: /backend/api/v1/healthcheck

Resources:
  FrontendALB:                                                           #(E)
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: FrontendALB
      Subnets:
        - Fn::ImportValue: !Sub ${VPCName}-PublicSubnet1
        - Fn::ImportValue: !Sub ${VPCName}-PublicSubnet2
      SecurityGroups:
        - Fn::ImportValue: !Sub ${VPCName}-SecurityGroupFrontendALB

  FrontendALBDefaultTargetGroup:                                         #(F)
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: FrontendALBTargetGroup
      VpcId:
        Fn::ImportValue: !Sub ${VPCName}-VPCID
      Port: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Port]        #(G)
      Protocol: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Protocol]
      HealthCheckPath: !Ref FrontendHealthCheckPath
      HealthyThresholdCount: 2
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: '20'

  FrontendALBListener:                                                   #(H)
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref FrontendALB
      Port: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Port]
      Protocol: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Protocol]
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref FrontendALBDefaultTargetGroup

  BackendALB:                                                            #(I)
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: BackendALB
      Scheme: internal
      Subnets:
        - Fn::ImportValue: !Sub ${VPCName}-PrivateSubnet1
        - Fn::ImportValue: !Sub ${VPCName}-PrivateSubnet2
      SecurityGroups:
        - Fn::ImportValue: !Sub ${VPCName}-SecurityGroupBackendALB

  BackendALBDefaultTargetGroup:                                          #(J)
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: BackendALBDefaultTargetGroup
      VpcId:
        Fn::ImportValue: !Sub ${VPCName}-VPCID
      Port: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Port]
      Protocol: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Protocol]
      HealthCheckPath: !Ref BackendDefaultHealthCheckPath
      HealthyThresholdCount: 2
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: '20'

  BackendALBListener:                                                    #(K)
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref BackendALB
      Port: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Port]
      Protocol: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Protocol]
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref BackendALBDefaultTargetGroup

Outputs:
  FrontendALB:                                                           #(L)
    Description: Frontend ALB
    Value: !Ref FrontendALB
    Export:
      Name: !Sub ${VPCName}-Frontend-ALB-${EnvType}

  BackendALB:                                                            #(M)
    Description: Backend ALB
    Value: !Ref BackendALB
    Export:
      Name: !Sub ${VPCName}-Backend-ALB-${EnvType}

  FrontendALBDNS:                                                        #(N)
    Description: Public DNS Name
    Value: !GetAtt FrontendALB.DNSName
    Export:
      Name: !Sub ${VPCName}-FrontendALBDNS-${EnvType}

  BackendALBDNS:                                                         #(O)
    Description: Private DNS Name
    Value: !GetAtt BackendALB.DNSName
    Export:
      Name: !Sub ${VPCName}-BackendALBDNS-${EnvType}

  FrontendALBDefaultTargetGroup:                                         #(P)
    Description: Frontend TagetGroup Default
    Value: !Ref FrontendALBDefaultTargetGroup
    Export:
      Name: !Sub ${VPCName}-Frontend-ALB-DefaultTargetGroup-${EnvType}

  BackendALBDefaultTargetGroup:                                          #(Q)
    Description: Backend TagetGroup Default
    Value: !Ref BackendALBDefaultTargetGroup
    Export:
      Name: !Sub ${VPCName}-Backend-ALB-DefaultTargetGroup-${EnvType}

  // omit


ALBのテンプレートの記述の基本となるポイントは(A)〜(Q)の通りです。


ALBのCloudFormationテンプレート記述のポイント
記述 説明
ALBでは通信で使うプロトコルを指定しますが、開発環境や商用環境でHTTP/HTTPSで分けて構成します。Mappings要素でそうした場合分けに応じてパラメータをまとめて定義しておくことで、見通しの良いテンプレート実装が可能です。なお、Mappingsの使用方法については下記の※で後述します。
(A)で定義したMappingsで使用するパラメータを引数で切り替えるためのパラメータをEnvTypeとして定義しておきます。ここではAllowedValuesで、Mappingsのキーとしていた要素の入力のみを許可するパラメータ定義にしておきます。
フロントエンドサブネットに配置するALBのデフォルトターゲットグループに設定するヘルスチェックパスをパラメータ定義します。AllowedPatternの正規表現で英数文字と記号「-(ハイフン)」、「.(ドット)」、「/(スラッシュ)」を含めた任意の文字列を許可するよう定義します。
バックエンドエンドサブネットに配置するALBのデフォルトターゲットグループに設定するヘルスチェックパスをパラメータ定義します。AllowedPatternの正規表現で英数文字と記号「-(ハイフン)」、「.(ドット)」、「/(スラッシュ)」を含めた任意の文字列を許可するよう定義します。
フロントエンドサブネットに配置するALBの定義を行います。詳細は AWS::ElasticLoadBalancingV2::LoadBalancer を参考にして下さい。サブネットやセキュリティグループは前回設定したテンプレートからクロススタックリファレンスで参照します。
フロントエンドサブネットALBのデフォルトターゲットグループの設定を行います。詳細は AWS::ElasticLoadBalancingV2::TargetGroup も参照してください。URLパスに応じて、ALBは設定したEC2やECSにフォワードできますが、それらの集合はターゲットグループとしてまとめられています。デフォルトターゲットグループは条件に一致するパスが存在しなかったときにフォワードされるEC2ないしはECSアプリケーションのターゲットグループだと捉えてください。
フロントエンドALBからターゲットグループへフォワードする際のポートやプロトコルを(A)で定義したMapping要素を使って取得するために、FindInMap関数を使って取得します。なお、関数の第2引数のみにかぎり、Ref参照が可能で、Parametersで指定したパラメータ引数に応じて、設定値を切り替えることができます。
フロントエンドALBのリスナーを定義します。詳細は AWS::ElasticLoadBalancingV2::Listener も参考にしてください。リスナーはALBに対するリクエストにおけるプロトコルやポート、フォワードするターゲットグループなどを決定します。なお、オプションとしてはフォワードの他に、リダイレクトやCoginitoやOpenIDConnectを使った認証処理などもあります。
(E)と同様、バックエンドエンドサブネットに配置するALBの定義を行います。詳細は AWS::ElasticLoadBalancingV2::LoadBalancer を参考にして下さい。サブネットやセキュリティグループは前回設定したテンプレートからクロススタックリファレンスで参照します。
(F)と同様、バックエンドエンドサブネットALBのデフォルトターゲットグループの設定を行います。詳細は AWS::ElasticLoadBalancingV2::TargetGroup も参照してください。URLパスに応じて、ALBは設定したEC2やECSにフォワードできますが、それらの集合はターゲットグループとしてまとめられています。デフォルトターゲットグループは条件に一致するパスが存在しなかったときにフォワードされるEC2ないしはECSアプリケーションのターゲットグループだと捉えてください。
(H)と同様、バックエンドALBのリスナーを定義します。詳細は AWS::ElasticLoadBalancingV2::Listener も参考にしてください。リスナーはALBに対するリクエストにおけるプロトコルやポート、フォワードするターゲットグループなどを決定します。なお、オプションとしてはフォワードの他に、リダイレクトやCoginitoやOpenIDConnectを使った認証処理などもあります。
(E)で定義したフロントエンドALBをクロススタックリファレンスの(他のテンプレートに値を渡す)ために出力します。
(I)で定義したバックエンドエンドALBをクロススタックリファレンスの(他のテンプレートに値を渡す)ために出力します。
(E)で定義したフロントエンドALBのDNSをクロススタックリファレンスの(他のテンプレートに値を渡す)ために出力します。
(I)で定義したバックエンドエンドALBのDNSをクロススタックリファレンスの(他のテンプレートに値を渡す)ために出力します。
(F)で定義したフロントエンドALBのターゲットグループをクロススタックリファレンスの(他のテンプレートに値を渡す)ために出力します。
(J)で定義したバックエンドエンドALBのターゲットグループをクロススタックリファレンスの(他のテンプレートに値を渡す)ために出力します。


注釈

※Mappingsについて

Mappingsは単純な連想配列(キーバリュー型のマップ)ではなく、もう1階層の構造が加わった2次元連想配列です。下記の例のように、Mappingsでは複数の2次元連想配列構造を取り、各マップごとに、複数のキーで様々なキーバリューデータを取得できるようになっています。


Mappings:
  DeployEnvironmentMap:
    Production:
      "Protocol": "HTTPS"
      "Port": 443
    Staging:
      "Protocol": "HTTP"
      "Port": 80

Parameters:
  EnvType:
    Description: Which environments to deploy your service.
    Type: String
    AllowedValues: ["Dev", "Staging", "Production"]
    Default: Dev


上記のデータを取得する際は、以下のようなFindInMap関数を使用します。ここで、ポイントとなるのは第2引数で、パラメータなどテンプレートの外から外部入力値に応じて、設定値を切り替えたい場合に効果を発揮します。


Port: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Port]


上記の例では、第1引数で参照するマッピング定義を選択し、第2引数はパラメータEnvTypeを参照しています。EnvTypeはパラメータ要素に定義されている項目でデフォルトは"Dev"ですが、実行時に"Production"などの値を引数にしてテンプレート実行することで、第3引数として設定する値を容易に切り替えられる仕組みです。


作成したテンプレートに対して、ヘルパースクリプトを以下のように、スタック名とテンプレートパスを変更して実行します。パラメータはデフォルト値を利用するので省略します。


#!/usr/bin/env bash

stack_name="mynavi-sample-alb"
template_path="sample-alb-cfn.yml"

aws cloudformation deploy --stack-name ${stack_name} --template-file ${template_path} --capabilities CAPABILITY_IAM


実行が正常に終了すると、ALBおよびデフォルトターゲットグループ、リスナーが作成されます。


../_images/management_console_cloudformation_stack_alb.png


続いて、サービスアプリケーションごとに追加するターゲットグループとリスナールールのテンプレートを作成します。各サービスごとに追加すればよいので、パスパターンやヘルスチェックパス、条件適用のプライオリティ等、必要なパラメータをMappingsに定義します。 インプットパラメータに応じて、適用されるキーバリューデータが変わるように実装します。加えて、フロントエンド、バックエンドへいずれかに配置するのもオプションで選べるように作成します。


AWSTemplateFormatVersion: '2010-09-09'

Mappings:
  TargetGroupDefinitionMap:                                              #(A)
    FrontendWebApp:
      "PathPattern": "/frontend/*"
      "HealthCheckPath": "/frontend/healthcheck"
      "Priority": 1
    BackendUserService:
      "PathPattern": "/backend/api/v1/user*"
      "HealthCheckPath": "/backend/api/v1/healthcheck"
      "Priority": 1
    BackendSampleService:
      "PathPattern": "/backend/api/v1/sample*"
      "HealthCheckPath": "/backend/api/v1/healthcheck"
      "Priority": 2
  DeployEnvironmentMap:                                                  #(B)
    Production:
      "Protocol": "HTTPS"
      "Port": 443
    Staging:
      "Protocol": "HTTP"
      "Port": 80
    Dev:
      "Protocol": "HTTP"
      "Port": 80

Parameters:

  // omit
  SubnetType:                                                            #(C)
    Description: Which subnet to deploy your service.
    Type: String
    AllowedValues: ["Frontend", "Backend"]
    Default: Backend
  EnvType:                                                               #(D)
    Description: Which environments to deploy your service.
    Type: String
    AllowedValues: ["Dev", "Staging", "Production"]
    Default: Dev
  ServiceName:                                                           #(E)
    Description: Deploy service name
    Type: String
    MinLength: 1
    MaxLength: 255
    AllowedPattern: ^[a-zA-Z][-a-zA-Z0-9]*$
    Default: BackendUserService

Resources:
  TargetGroup:                                                           #(F)
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${EnvType}-${ServiceName}-tg
      VpcId:
        Fn::ImportValue: !Sub ${VPCName}-VPCID
      Port: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Port]
      Protocol: !FindInMap [DeployEnvironmentMap, !Ref EnvType, Protocol]
      HealthCheckPath: !FindInMap [TargetGroupDefinitionMap, !Ref ServiceName, HealthCheckPath]
      HealthyThresholdCount: 2
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: '20'

  ListenerRule:                                                          #(G)
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - !FindInMap [TargetGroupDefinitionMap, !Ref ServiceName, PathPattern]
      ListenerArn:
        Fn::ImportValue: !Sub ${VPCName}-${SubnetType}-ALB-Listener-${EnvType}
      Priority: !FindInMap [TargetGroupDefinitionMap, !Ref ServiceName, Priority]

Outputs:                                                                 #(H)
  TargetGroup:
    Description: TargetGroup Service
    Value: !Ref TargetGroup
    Export:
      Name: !Sub ${VPCName}-${SubnetType}-${ServiceName}-TargetGroup-${EnvType}


サービスアプリケーションごとにALBへターゲットグループとして追加するリソースのCloudFormationテンプレート記述のポイント
記述 説明
フロントエンドに配置するWebアプリケーションやバックエンドサービスごとにフォワード先を決定するパスパターンや、ヘルスチェックパス、パスマッチングのプライオリティをMappingsで定義します。
ベースとなるALBの定義と同様、デプロイ環境に応じてプロトコルやポートをMappingsで定義します。
ターゲットグループやリスナーを設定するALBがあるサブネットをパラメータとして設定できるよう、Parameters要素に定義します。
ベースとなるALBの定義と同様、開発環境や商用環境でHTTP/HTTPSにするか入力パラメータで切り替えるために、Parameters要素に定義します。
入力パラメータに応じて、(A)で定義した設定値を切り替えれるように、Parameters要素に定義します。
追加するターゲットグループを定義します。詳細は AWS::ElasticLoadBalancingV2::TargetGroup も参照してください。なお、定義はFindInMap関数で入力パラメータに応じて動的に切り替えて設定されます。
URLパスパターンやフォワードのルール定義を行います。詳細は、 AWS::ElasticLoadBalancingV2::ListenerRule を参照してください。なお、定義はFindInMap関数で入力パラメータに応じて動的に切り替えて設定されます。
(F)で定義したターゲットグループをクロススタックリファレンスの(他のテンプレートに値を渡す)ために出力します。


作成したテンプレートに対して、ヘルパースクリプトを以下のように、スタック名とテンプレートパスを変更して実行します。


#!/usr/bin/env bash

stack_name="mynavi-sample-tg-userservice"
template_path="sample-tg-cfn.yml"
parameters="SubnetType=Backend EnvType=Dev ServiceName=BackendUserService"

aws cloudformation deploy --stack-name ${stack_name} --template-file ${template_path} --parameter-overrides ${parameters} --capabilities CAPABILITY_IAM


実行が正常に終了すると、ターゲットグループ、リスナールールが作成されます。作成するアプリケーション、サービスに応じて、サブネットやサービス名を変更して複数回パラメータを変えて実行します。


../_images/management_console_cloudformation_stack_tg.png


今回はMappings要素を使いながら、ALBおよびサービスアプリケーションごとのターゲットグループ、リスナーをCloudFormationテンプレートで構築しました。次回は、RDSを構築するスタックテンプレートを作成します。


著者紹介

川畑 光平(KAWABATA Kohei) - NTTデータ 課長代理

../_images/pic_image01.jpg

金融機関システム業務アプリケーション開発・システム基盤担当を経て、現在はソフトウェア開発自動化関連の研究開発・推進に従事。

Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなど様々な開発プロジェクト支援にも携わる。

2019 APN AWS Top Engineers & Ambassadors 選出。