この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
ALBのバックエンドには処理毎に分れたLambdaが複数あります。ALBと複数のLambdaを1つのCloudFormationテンプレートにまとめることもできます。しかし、Lambda毎に開発しているチームが異なるなどでLambda単位で管理を分けたいです。ALBは独立して管理し、Lambdaは各開発チームで管理できるようにしたい場合、どこのリソースでテンプレートを分割すると都合がよいのか考えてみました。SAMでLambdaをデプロイする構成の一例として紹介します。
ALBはCloudFormationのテンプレートで管理し、各種LambdaはSAMテンプレートで管理しAWS SAMを利用してデプロイする状況を想定します。
まとめ
SAMで管理する個々のLambdaをELBのバックエンドに追加していくことが予想される場合、以下の箇所でテンプレートを区切りました。
- ALB本体とリスナーのベースとなる部分
- 各Lambda関数とリスナールール、ターゲットグループ
sam deploy
で作成したLambdaを既存ELBのリスナーに紐づけていけることを確認しました。
テンプレートの単位
青、緑色背景は個々にSAMで管理する単位、それ以外はALBのCloudFormationテンプレートで管理。
検証環境
項目 | 値 |
---|---|
AWS SAM | 1.36.0 |
本検証で使用したALBのCloudFormationテンプレート、SAMで管理するLambda一式は以下においてあります。
考えてみた
ALBのバックエンドで起動するLambdaの構成は構成図でみる分にはシンプルです。ALBと各Lambda単位にテンプレート分ければ済みそうと一見思えます。
もう少し細かくみたいのでリソース単位に分解します。こうするとALBに関連するリソースが地味にあって悩ましくなってきますリスナー、リスナールール、ターゲットグループ、この辺りのまとめ方が非常に悩ましいです。
SAMでLambdaをデプロイすることを考慮した結果
- ALB本体とリスナー
- Lambda関数とリスナールール、ターゲットグループ
上記のリソースを1単位として管理するとキレイな分け方ができるのではないかと思い、sam deploy
で既存ALBにLambdaの紐付け実現できるのか確認してみました。
ELBとリスナー作成
ELB本体とLambdaを紐付けていくリスナーをCloudFormationで作成します。リスナーのデフォルトルールはELB側に持たせます。 作成したリスナーはエクスポートし、Lambdaを作成するSAMテンプレートでクロススタック参照して利用することにします。
alb/alb.yaml
AWSTemplateFormatVersion: "2010-09-09"
Description: ALB*1
Parameters:
ProjectName:
Description: Project Name
Type: String
Default: unnamed
Environment:
Description: Environment
Type: String
Default: dev
AllowedValues:
- prod
- dev
- stg
VPCID:
Type: AWS::EC2::VPC::Id
PublicSubnet1:
Description: "ELB Subnet 1st"
Type: AWS::EC2::Subnet::Id
PublicSubnet2:
Description: "ELB Subnet 2nd"
Type: AWS::EC2::Subnet::Id
Resources:
# --------------------------------------------
# ELB
# --------------------------------------------
ELB1:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Type: "application"
Name: !Sub ${ProjectName}-${Environment}-elb
Scheme: "internet-facing"
SecurityGroups:
- !Ref SecurityGroup1
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
IpAddressType: "ipv4"
LoadBalancerAttributes:
- Key: "deletion_protection.enabled"
Value: "false"
Tags:
- Key: Name
Value: !Sub ${ProjectName}-${Environment}-elb
ELBListener1:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: fixed-response
FixedResponseConfig:
StatusCode: 404
MessageBody: Not Found.
ContentType: text/plain
LoadBalancerArn: !Ref ELB1
Port: 80
Protocol: HTTP
SecurityGroup1:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: !Sub ${ProjectName}-${Environment}-elb-sg
GroupDescription: ELB Security Group
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: "0.0.0.0/0"
Description: "Access from Public"
VpcId: !Ref VPCID
Tags:
- Key: Name
Value: !Sub ${ProjectName}-${Environment}-elb-sg
Outputs:
Listener1:
Description: "Linstener ARN"
Value: !GetAtt ELBListener1.ListenerArn
Export:
Name: !Sub ${AWS::StackName}-Listener1
ベースとなるリスナーが作成されました。
リスナールールは固定レスポンスを返すデフォルトルールのみ作成しています。
アクセステスト
ELBにアクセスすると固定レスポンスのNot Found.
が返ってくるだけのELBが完成しました。
$ curl http://sample-dev-elb-1162918276.ap-northeast-1.elb.amazonaws.com
Not Found.⏎
1個目のLambda作成
LambdaはSAMで管理します。Lambda関数本体と、リスナールール、ターゲットグループを同時に作成します。その他、ELBからLambdaを呼び出すために必要なリソースベースポリシー(AWS::Lambda::Permission
)も作成しLambdaに設定します。そして、SAMテンプレートで新規作成するリスナールールを既存リスナーに紐付けることでELBと連携させます
ポイント
- リスナールールの
Priority
は他のLambdaをデプロイするときに重複しないように管理が必要
first-lambda/template.yaml
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
ELBから呼び出されるLambdaはメッセージをレスポンスに返すだけです。
first-lambda/first-lambda/app.py
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message": "I am FIRST Lambda back of ALB. ",
}),
}
既存リスナーに新規作成のリスナールールとLambdaを作成するSAMテンプレートをビルドしてAWSへデプロイします。
$ sam build
...snip...
$ sam deploy --guided
...snip...
Stack Name [sam-app]: first-lambda
AWS Region [us-east-1]: ap-northeast-1
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: n
Save arguments to configuration file [Y/n]: y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:
...snip...
Successfully created/updated stack - first-lambda in ap-northeast-1
デプロイが完了すると、既存リスナーに新たなリスナールールが追加されました。
アクセステスト
ALBのURLに対してリスナールールで指定したパス(/v1/lambda1
)にアクセスすると、Lambdaからレスポンスが返ってきます。既存のALBにsam deploy
したLambdaを上手いこと連携できました。
$ curl http://sample-dev-elb-1162918276.ap-northeast-1.elb.amazonaws.com/v1/lambda1
{"message": "I am FIRST Lambda back of ALB. "}⏎
2個目のLambda作成
重要なところはリスナールールのプライオリティを重複させないことです。1個目のLambdaとほぼ同様のSAMテンプレートを作成しました。2個目のLambdaを呼ぶパスはv1/lambda2
としました。
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
second-lambda
Sample SAM Template for second-lambda
Resources:
Function:
Type: AWS::Serverless::Function
Properties:
FunctionName: "second-lambda"
CodeUri: ./second-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 Internal ALB
# ---------------------------
TargetGroup1:
Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
Properties:
HealthCheckIntervalSeconds: 15
HealthCheckPath: "/"
HealthCheckTimeoutSeconds: 10
UnhealthyThresholdCount: 2
TargetType: "lambda"
Matcher:
HttpCode: "200"
HealthyThresholdCount: 2
Name: "second-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: "2"
ListenerArn: !ImportValue blog-alb-Listener1
Conditions:
- Field: "path-pattern"
Values:
- "/v1/lambda2"
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
Lambdaのコードはレスポンスのメッセージを変更しただけなので省略します。リポジトリを参考ください。
またしても既存リスナーに新規のリスナールールと2個目のLambdaを作成するSAMテンプレートをビルドしてAWSへデプロイします。
$ sam build
...snip...
$ sam deploy --guided
...snip...
Stack Name [sam-app]: second-lambda
AWS Region [us-east-1]: ap-northeast-1
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
Confirm changes before deploy [y/N]: y
#SAM needs permission to be able to create roles to connect to the resources in your template
Allow SAM CLI IAM role creation [Y/n]: y
#Preserves the state of previously provisioned resources when an operation fails
Disable rollback [y/N]: n
Save arguments to configuration file [Y/n]: y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:
...snip...
Successfully created/updated stack - second-lambda in ap-northeast-1
デプロイが完了すると、2個目のリスナールールが新たに追加されました。
アクセステスト
ALBのURLに対して2個目のリスナールールで指定したパスにアクセスするとLambdaからのレスポンスが返ってきます。ALBから呼び出したいLambdaが増えた場合も対応していけそうです。
$ curl http://sample-dev-elb-1162918276.ap-northeast-1.elb.amazonaws.com/v1/lambda2
{"message": "I am SECOND Lambda back of ALB!!!!!! "}⏎
まとめ
ELBのリスナーに対してSAM管理のLambdaを紐付けて、ELBのパス違いで別々のLabmdaを呼び出すことを確認できました。
$ curl http://sample-dev-elb-1162918276.ap-northeast-1.elb.amazonaws.com/v1/lambda1
{"message": "I am FIRST Lambda back of ALB. "}⏎
$ curl http://sample-dev-elb-1162918276.ap-northeast-1.elb.amazonaws.com/v1/lambda2
{"message": "I am SECOND Lambda back of ALB!!!!!! "}⏎
先にELBを作成しSAMで管理するLambdaを次々に生み出す場合は、以下の箇所でテンプレートを区切るとキレイに分けることができたのではないでしょうか。
- ALB本体とリスナーのベース部分
- 各Lambda関数とリスナールール、ターゲットグループ
おわりに
テンプレートで管理する単位はライフサイクルに寄ってきます。最適解は都度考えないといけないのですが、どのようなパターンが考えられるのかいくつかの参考例を見た上で検討したかったのですがサンプル数が少なかったので一例としてあげました。なにかの参考になれば幸いです。