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

本連載で構築していく基盤自動化環境のイメージ

前回は、各AWSリソースへ設定するセキュリティグループ、FrontendサブネットにアタッチするNATGatewayを構築するテンプレートを実装しました。続く今回は、Frontend/Backendサブネットに配置するアプリケーションロードバランサー(ALB)を構築するCloudFormationテンプレートを作成します。

なお、実際のソースコードはGitHub上にコミットしています。以降のソースコードでは本質的でない記述を一部省略しているので、実行コードを作成する場合は、必要に応じて適宜GitHub上のソースコードも参照してください。

ALBスタック構築テンプレート

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

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

1つのテンプレートで全ての定義をひとまとめにしておくのもシンプルでわかりやすくて良いのですが、サービスアプリケーションを追加する必要が生じるケースなどもあるので、リソース定義を再利用して追加対象だけを再実行すればよいテンプレート構成にしておいたほうが効率的です。また、同様の理由から実装の際、開発環境は「EnvType」としてパラメータ化しておきます。

まず、ALBとデフォルトターゲットグループ、リスナー定義のテンプレートのサンプルは以下の通りです。

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
      HealthCheckIntervalSeconds: 60
      HealthyThresholdCount: 2
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: '60'

  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
      HealthCheckIntervalSeconds: 60
      HealthyThresholdCount: 2
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: '60'

  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

上記のテンプレートの記述の基本となるポイントは、以下の通りです。

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

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

ここでポイントとなるのは第2引数で、パラメータなどテンプレートの外から外部入力値に応じて、設定値を切り替えたい場合に効果を発揮します。

上記の例では、第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とデフォルトターゲットグループ、リスナーが作成されます。

ALB、デフォルトターゲットグループ、リスナーが作成される

続いて、サービスアプリケーションごとに追加するターゲットグループとリスナールールのテンプレートを作成します。各サービスごとに追加すればよいので、パスパターンやヘルスチェックパス、条件適用のプライオリティなど、必要なパラメータを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]
      HealthCheckIntervalSeconds: 60
      HealthyThresholdCount: 2
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: '60'

  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}

上記のテンプレートの記述のポイントは、以下の通りです。

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

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

実行が正常に終了すると、ターゲットグループとリスナールールが作成される

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

著者紹介


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

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

Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなどさまざまな開発プロジェクト支援にも携わる。2019 APN AWS Top Engineers & Ambassadors選出。

本連載の内容に対するご意見・ご質問は Facebook まで。