AWS CloudFormation で Lambda リソースベースポリシー と ターゲットグループのARN指定 が循環依存関係になる問題を Lambda-backend カスタムリソースを使わないで回避する方法

2021.12.12

Lambdaとターゲットグループを1つのテンプレート作成する機会がありました。実現したいことは1つのテンプレートで...

  • ELBのターゲットグループ(AWS::ElasticLoadBalancingV2::TargetGroup
  • Lambdaのリソースベースポリシー(AWS::Lambda::Permission

上記リソースを作成する必要がありました。すると循環依存関係の問題が起きます。この問題とどう向き合ったかを紹介します。

まとめ

AWS公式のワークアラウンドはLambda-Backend カスタムリソースを利用する。

Lambda-Backend カスタムリソースを使いたくないときはリソースベースポリシーにワイルドカードを使う。

  • リソースベースポリシー(AWS::Lambda::Permission)で指定するターゲットグループARNの箇所をワイルドカードに置き換える。
  • ワイルドカードを置く位置によってはバリデーションエラーが出力されるケース確認された。

リソースベースポリシーの指定例

  LambdaPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt Function.Arn
      Principal: "elasticloadbalancing.amazonaws.com"
      SourceArn: !Sub "arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:*" # ワイルドカードを採用

検証環境

項目
AWS SAM 1.36.0

問題と向き合った

Lambdaのリソースベースポリシーと、ターゲットグループを同一のテンプレートで作成したい具体的な理由、構成は以下のリンクで紹介しています。

循環依存関係で問題となる箇所はターゲットグループARNをリソースベースポリシーで指定して明示的にターゲットグループを許可するために、ターゲットグループのリソースを先に作成してターゲットグループのARNを確定させたいです。

それならばDepndsOn, !Ref, !Subあたりでなんとかなりそうな気もしますがなんとかなりませんでした。

AWS公式の回避策

AWS公式の回避策はLambda-Backend カスタムリソースを利用する方法を紹介されています。

ライフサイクルの関係上、1つテンプレートにまとめて管理したい上にどうしてもLambda-Backend カスタムリソースを使いたくない場合、他に回避方法はないか考えましたので紹介していきます。

AWS SAMを利用していますがCloudFormationのテンプレート作成しても同様です。

ワイルドカードを利用した回避策

普通にテンプレート作成して循環依存問題に直面するケースを先に確認し、その後ワイルドカードを利用した回避策を試してみます。

まず、リソースベースポリシーとは

Lambda関数にはリソースベースポリシーというアクセス権限設定があります。たとえばAPI Gatewayや、EventBridgeなどのリソースからLambda関数を呼び出すときには、リソースベースポリシーで呼び出し元のリソース名(ARN)を許可しておく必要があります。

リソースベースポリシーの詳細については以下のリンクを参照ください。

問題のあるテンプレート記述

リソースベースポリシーで指定するSorceArn!RefでターゲットグループのARNを渡したいと思います。

# リソースベースポリシーの作成
  LambdaPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt Function.Arn
      Principal: "elasticloadbalancing.amazonaws.com"
      SourceArn: !Ref TargetGroup1 # ELBからLambadaを呼び出すためにはターゲットグループARNを指定して明示的な許可を与える必要がある

# ターゲットグループの作成
  TargetGroup1: # ここで作成されるターゲットグループのARNが欲しい
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      HealthCheckIntervalSeconds: 15
      HealthCheckPath: "/"
      HealthCheckTimeoutSeconds: 10
      UnhealthyThresholdCount: 2
      TargetType: "lambda"
      Matcher:
        HttpCode: "200"
      HealthyThresholdCount: 2
      Name: "first-lambda-tg"
      HealthCheckEnabled: false
      TargetGroupAttributes:
        - Key: "lambda.multi_value_headers.enabled"
          Value: "false"
      Targets:
        - Id: !GetAtt Function.Arn
          AvailabilityZone: "all"

sam deployでLambdaをデプロイしてみます。以下のエラーによりデプロイに失敗します。

...snip...
CREATE_FAILED                   AWS::ElasticLoadBalancingV2::   TargetGroup1                    API: elasticloadbalancingv2:R
                                TargetGroup                                                     egisterTargets
                                                                                                elasticloadbalancing
                                                                                                principal does not have
                                                                                                permission to invoke
                                                                                                arn:aws:lambda:ap-
...snip...

ワイルドカード指定のテンプレート記述

事前にターゲットグループARNを入力してしまえばよいのでは?と最初考えました。しかし、ターゲットグループのARNの末尾には英数字ランダムが付与されます。マネージメントコンソールから作成しても、CloudFormationから作成しても英数字ランダムの箇所を削除することはできませんでした。

つまり、事前にターゲットグループのARN決め打ちして入力しておく案は使えませんでした。

リソースベースポリシーのSourceArn指定は明示的に許可を与えるため最小特権の原則にしたがって通常設定しているかと思います。英数字ランダム要素を回避するためにワイルドカードを指定できるのでしょうか?

ここでひとつ問題がありワイルドカードを含めるとバリデーションエラーが表示されるケースを何パターンか確認できました。しかし、バリデーションエラーのパターンに規則性は見つけられないこと、設定時にはエラーにならないがマネージメントコンソールから確認するとバリデーションエラーが表示される状況でした。

切り分けの詳細は以下のリンクをご確認ください。

最終的にバリデーションエラーが確認されなかった以下のパターンに落ち着きました。権限の範囲が広くなり同アカウントのELBからの呼び出しを許可することになります。特定のターゲットグループからに絞りたかったのですがバリデーションエラーが確認されたため無難な設定にしています。

# リソースベースポリシーの作成
  LambdaPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt Function.Arn
      Principal: "elasticloadbalancing.amazonaws.com"
      SourceArn: !Sub "arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:*" # ワイルドカードを採用

# ターゲットグループの作成
  TargetGroup1:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      HealthCheckIntervalSeconds: 15
      HealthCheckPath: "/"
      HealthCheckTimeoutSeconds: 10
      UnhealthyThresholdCount: 2
      TargetType: "lambda"
      Matcher:
        HttpCode: "200"
      HealthyThresholdCount: 2
      Name: "first-lambda-tg"
      HealthCheckEnabled: false
      TargetGroupAttributes:
        - Key: "lambda.multi_value_headers.enabled"
          Value: "false"
      Targets:
        - Id: !GetAtt Function.Arn
          AvailabilityZone: "all"

この記述であればsam deployしても問題なくリソース作成できました。

まとめ

参考にワイルドカード指定のテンプレート全文を載せておきます。

折りたたみ
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  first-lambda

  Sample SAM Template for first-lambda

Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: "first-lambda"
      CodeUri: ./first-lambda
      Handler: app.lambda_handler
      Runtime: python3.9
      Architectures:
        - arm64
      Timeout: 5

  LambdaPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt Function.Arn
      Principal: "elasticloadbalancing.amazonaws.com"
      SourceArn: !Sub "arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:*"

  # ---------------------------
  # Resources associated with the ALB
  # ---------------------------
  TargetGroup1:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      HealthCheckIntervalSeconds: 15
      HealthCheckPath: "/"
      HealthCheckTimeoutSeconds: 10
      UnhealthyThresholdCount: 2
      TargetType: "lambda"
      Matcher:
        HttpCode: "200"
      HealthyThresholdCount: 2
      Name: "first-lambda-tg"
      HealthCheckEnabled: false
      TargetGroupAttributes:
        - Key: "lambda.multi_value_headers.enabled"
          Value: "false"
      Targets:
        - Id: !GetAtt Function.Arn
          AvailabilityZone: "all"

  ListenerRule:
    Type: "AWS::ElasticLoadBalancingV2::ListenerRule"
    Properties:
      Priority: "1"
      ListenerArn: !ImportValue blog-alb-Listener1
      Conditions:
        - Field: "path-pattern"
          Values:
            - "/v1/lambda1"
      Actions:
        - Type: "forward"
          TargetGroupArn: !Ref TargetGroup1
          Order: 1
          ForwardConfig:
            TargetGroups:
              - TargetGroupArn: !Ref TargetGroup1
                Weight: 1
            TargetGroupStickinessConfig:
              Enabled: false

Outputs:
  Function:
    Description: "Lambda Function ARN"
    Value: !GetAtt Function.Arn
  FunctionIamRole:
    Description: "Implicit IAM Role created for function"
    Value: !GetAtt FunctionRole.Arn
  • リソースベースポリシー(AWS::Lambda::Permission)で指定するターゲットグループARNの箇所をワイルドカードに置き換える。
  • ワイルドカードを置ける位置によってはバリデーションエラーが出力されるケース確認された。

リソースベースポリシーの指定例

  LambdaPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt Function.Arn
      Principal: "elasticloadbalancing.amazonaws.com"
      SourceArn: !Sub "arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:*" # ワイルドカードを採用

おわりに

循環依存関係が問題だったのですが、厄介だったのはリソースベースポリシーのSorceArnにワイルドカードが含めるとバリデーションエラーになる条件がわからないことでした。