Lambda-backed Custom Resourceを利用してCloudFormationにループ処理を擬似的に実装する
はじめに
こんにちは、中山です。
今回はLambda-backed Custome Resourceを利用したTipsをご紹介します。この機能そのものについては上記ドキュメントや弊社の他エントリで詳しくご紹介しているので、そちらを参照していただければと思います。簡単に説明するとCloudFormation(以下CFn)の中でLambda関数を実行し、その結果をCFnのテンプレートの中で参照できる機能です。
- [CloudFormation]Lambda-backedカスタムリソースを理解する
- CloudFormationでいつでも最新AMIからEC2を起動したい
- 【新機能】AWS CloudFormationのLambda-Backed Custom Resourcesを使ってBlue-Green Deploymentをより簡単に実現する
CFnのあるあるパターン
例えばCFnでSSH用セキュリティグループを作成し、特定のIPアドレスからのみアクセスを許可したいとしましょう。セキュリティグループ関連のリソースは現時点(2016年11月21日)で以下の3つがあります。
各リソースの CidrIp
プロパティを見ていただくと分かるのですが、指定可能な値のタイプが String
になっています。つまり、IPアドレスをリストとして一度に複数指定できないので、SSHの送信元IPアドレスが複数ある場合、IPアドレス毎にセキュリティグループのルールを作成する必要があるということです。
CidrIp
Specifies a CIDR range.
Required: Conditional. You must specify only one of the following properties: CidrIp, DestinationPrefixListId, DestinationSecurityGroupId,or SourceSecurityGroupId.
Type: String
CidrIp
Specifies a CIDR range.
For an overview of CIDR ranges, go to the Wikipedia Tutorial.
Type: String
Required: Conditional. If you specify SourceSecurityGroupName, do not specify CidrIp.
Update requires: Replacement
CidrIp
CIDR range.
Required: Conditional. You must specify only one of the following properties: DestinationPrefixListId, DestinationSecurityGroupId, or CidrIp.
Type: String
Update requires: Replacement
マネジメントコンソールでセキュリティグループを作成する場合、カンマ区切りで複数の送信元IPアドレスを指定可能です。それが「何故か」CFnだとできません。実際に以下のようなパターンでスタックを作成するとエラーになります。
- カンマ区切りで指定
Test1SG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "-" VpcId: !Ref VPCID SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: !Join [ ",", [ "10.0.100.1/32", "10.0.100.2/32"] ]
結果: CIDR block 10.0.100.1/32,10.0.100.2/32 is malformed
- スペースを1つ分入れてカンマ区切りで指定
Test2SG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "-" VpcId: !Ref VPCID SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: !Join [ ", ", [ "10.0.100.1/32", "10.0.100.2/32"] ]
結果: CIDR block 10.0.100.1/32, 10.0.100.2/32 is malformed
- スペース区切りで指定
Test3SG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "-" VpcId: !Ref VPCID SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: !Join [ " ", [ "10.0.100.1/32", "10.0.100.2/32"] ]
結果: CIDR block 10.0.100.1/32 10.0.100.2/32 is malformed
- リストで渡す(やけくそ)
Test4SG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "-" VpcId: !Ref VPCID SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: [ "10.0.100.1/32", "10.0.100.2/32"]
結果: Value of property CidrIp must be of type String
正直これは厳しいです。例えば、制限したい送信元IPアドレスが数十個ある場合、以下のように重複しまくりの長大なテンプレートを作成する必要があります。
Test5SG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "-" VpcId: !Ref VPCID SecurityGroupIngress: - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: "10.0.100.1/32" - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: "10.0.100.2/32" - <snip>
こういったCFnの苦手な箇所を克服するため、winebarrel/Kumogata2のようにプログラマブルにCFnテンプレートを作成するツールが生まれたのだと思います(余談ですが、セキュリティグループを作成するのであればwinebarrel/piculetは便利なので弊社でもよく使っています)。こういったツールは大変便利なのですが、できれば標準の機能を利用して問題を解決したいと思っています。新しいツールを導入するとチーム内での学習コストの上昇、そのツールの将来性など考慮するパラメータが増えてしまうためです。
この問題点を克服する1つの方法が、今回ご紹介するLambda-backed Custome Resourceを利用した疑似ループの実装です。
疑似ループの実装
一言で説明すると、「インラインで記述したLambda関数内でCFnのプロパティを作成し、カスタムリソースでそれを参照する」という方法です。実物を見た方が分かりやすいと思います。例えばセキュリティグループのイングレスルールを作成するのであれば以下のようになります。
Resources: LambdaBasicExecRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Sid: "IngressRuleHelper" Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Path: / ManagedPolicyArns: - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" IngressRuleHelper: Type: AWS::Lambda::Function Properties: Code: ZipFile: | import cfnresponse def handler(event, context): ssh_ips = event["ResourceProperties"]["SSHIps"] response_data = {} response_data["Rules"] = [{"IpProtocol": "tcp", "FromPort": 22, "ToPort": 22, "CidrIp": ssh_ip} for ssh_ip in ssh_ips] cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) Handler: index.handler MemorySize: 128 Role: !GetAtt LambdaBasicExecRole.Arn Runtime: python2.7 Timeout: 10 SSHIngressRules: Type: Custom::IngressRuleHelper Version: 1.0 Properties: ServiceToken: !GetAtt IngressRuleHelper.Arn SSHIps: [ "10.0.100.1/32", "10.0.100.2/32" ]
それぞれ解説します。
- Lambda関数用IAM Role
Lambda関数を利用するのであれば、それにひも付けるIAM Roleを作成する必要があります。今回のようなAWSリソースにアクセスする必要がないタイプのものは、マネージドポリシーである AWSLambdaBasicExecutionRole
を利用すると自分でポリシーを管理する必要がなく便利です。
- インラインで記述したLambda関数とカスタムリソースでの呼び出し
CFnでLambda関数のリソースを定義する方法は、S3に設置したコードを参照する方法と、今回のようにテンプレート中に直接埋め込んだ(インライン)方式の2種類があります。Lambda関数に標準で入っていない外部モジュールを利用する場合はS3にデプロイパッケージを設置する必要があります。しかし、今回のように数~十行程度のものであればインラインで記述した方が管理しやすいですし、S3上のLambdaでは標準で利用できないcfn-responseモジュールを使えるのでオススメです。
カスタムリソース(論理IDが SSHIngressRules
のリソース)からLambda関数(論理IDが IngressRuleHelper
)を呼び出しています。プロパティで指定している SSHIps
にはSSHの送信元となるIPアドレスをリストで渡しています。この値はハンドラの引数である event
内に格納され、 ResourceProperties
の値として参照可能です。カスタムリソースのリクエストタイプによって event
で渡される内容は異なります。ドキュメントに詳しいですが、例えばリクエストタイプが Create
の場合、以下のようなデータが event
に格納されます。Lambda関数の実行ログはCloudWatch Logsに出力されるのでそこから確認可能です。
{ "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/********-****-****-****-************", "ResourceProperties": { "ServiceToken": "arn:aws:lambda:ap-northeast-1:************:function:test-IngressRuleHelper-************", "SSHIps": [ "10.0.100.1/32", "10.0.100.2/32" ] }, "ResponseURL": "<Pre-signed URL>", "RequestId": "********-****-****-****-************", "ResourceType": "Custom::IngressRuleHelper", "RequestType": "Create", "ServiceToken": "arn:aws:lambda:ap-northeast-1:************:function:test-IngressRuleHelper-************", "LogicalResourceId": "SSHIngressRules" }
24行目の処理で ssh_ips
変数にカスタムリソースから渡されたIPアドレスをリストとして変数に格納し、次のリスト内包表記でイングレスルールを作成しています。ここが「擬似的なループ処理」と呼んでいる箇所になります。続いてイングレスルールを Rules
をキーとした辞書型に格納し、最後の処理、 cfn-response
モジュールの send
メソッドの引数に渡すことで、CFnにデータを返却し、テンプレートから参照可能にしています。CFnには以下のようなデータが返却されます。
{ "Status": "SUCCESS", "StackId": "arn:aws:cloudformation:ap-northeast-1:************:stack/test/********-****-****-****-************", "PhysicalResourceId": "2016/11/20/[$LATEST]********************************", "Reason": "See the details in CloudWatch Log Stream: 2016/11/20/[$LATEST]********************************", "RequestId": "********-****-****-****-************", "Data": { "Rules": [ { "ToPort": 22, "IpProtocol": "tcp", "CidrIp": "10.0.100.1/32", "FromPort": 22 }, { "ToPort": 22, "IpProtocol": "tcp", "CidrIp": "10.0.100.2/32", "FromPort": 22 } ] }, "LogicalResourceId": "SSHIngressRules" }
- イングレスルールの参照
send
メソッドで返却されたデータ中の Data
に格納された値が参照可能です。内容としては、 Rules
をキーとして、値にリストが格納されています。セキュリティグループのルール自体はリストで指定可能なので、以下のように !GetAtt
関数でそのまま参照すればOKです。
Test6SG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "-" VpcId: !Ref VPCID SecurityGroupIngress: !GetAtt SSHIngressRules.Rules
作成されたセキュリティグループを確認すると複数のルールが作成されていることを確認できます。
{ "PrefixListIds": [], "FromPort": 22, "IpRanges": [ { "CidrIp": "10.0.100.2/32" }, { "CidrIp": "10.0.100.1/32" } ], "ToPort": 22, "IpProtocol": "tcp", "UserIdGroupPairs": [] }
まとめ
いかがだったでしょうか。
Lambda-backed Custom ResourceのTipsをご紹介しました。この機能は今回ご紹介したように非常に応用範囲の広い優れた機能です。みなさんもぜひ使ってみてください。
少し注意点を書いておきます。Lambda関数に文法エラーなどのミスがあると、それを呼び出すカスタムリソースが長時間 IN_PROGRESS
の状態になるようです。Lambda側でタイムアウトの設定をしても何度もリトライしているようなので、デバッグに時間がかかりました。。。ドキュメントを見る限りカスタムリソース側で制御する方法が無いようなので改善して欲しい。。。より良いエラーハンドリングの方法が見つかったら追記しておきます。
本エントリがみなさんの参考になったら幸いに思います。