背景
AWSのLambda FunctionをPython3で書いて実行するとき、Boto3のドキュメントには使用方法が書かれているのに、いざ実行してみるとメソッドが存在していないと怒られることがあります。
例えば、RDSのstop_db_clusterは、LambdaのBoto3では実行できませんでした。(2019/5/30現在)
def stop_aurora_cluster(cluster_identifier):
rds = boto3.client('rds')
response = rds.stop_db_cluster(DBClusterIdentifier=cluster_identifier)
上記関数をLambda Functionで実行すると、以下のような結果となり、失敗します。
'RDS' object has no attribute 'stop_db_cluster': AttributeError
(略)
AttributeError: 'RDS' object has no attribute 'stop_db_cluster'
しかしboto3のドキュメントには、stop_db_clusterが定義されています。
メソッドは存在するのに、Lambdaからは使えないという状況です。
何故このようなことが起こるのかというと、Lambdaに搭載されているBoto3のバージョンが、Boto3の最新バージョンに比べて古いからです。
ただ、Lambda Layersを使えば、最新のBoto3を利用できます。
詳しくは以下のリンク先が参考になりますが、Lambdaに標準搭載されているBoto3を、より新しいバージョンのBoto3のLambda Layersで上書きするという方法です。
(参考) Lambda Layers で最新の AWS SDK を使用する
このように、最新のBoto3を使いたい場合、都度、上記リンク先の方法でLambda Layersを作る必要がありますが、これは面倒な作業です。
zipを作ってマネジメントコンソールでアップロードするのも、zipをS3にアップロードしてCloudFormationのテンプレートを実行するのも、手作業が多くて非常に面倒です。
- Lambda Functionをデプロイする度にLambda Layersを手動でデプロイし直すのは嫌だ!
- なんとか自動でやりたい!
ということで、
- 最新のBoto3 Lambda Layersを自動で生成するLambda Function
を作ってしまいました。
このLambda FunctionをAWS CloudFormationのLambda-backed カスタムリソース (CloudFormationのテンプレート実行時に、テンプレート内でLambda Functionを実行できる機能) で実行すれば、毎回、自動で最新のBoto3 Lambda Layersを生成し、別のLambda Functionにアタッチすることができるようになります。
CloudFormationテンプレート(YAML)
S3 Bucketを作っておきます。
ぶっちゃけそのS3 BucketもCloudFormationで作れば良いのですが、今回は割愛しています。
IAM Role
Lambda Functionで実行するs3:PutObjectを許可します。
DeployLatestBoto3LambdaLayerRole:
Type: AWS::IAM::Role
Properties:
RoleName: "DeployLatestBoto3LambdaLayerRole"
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: "DeployLatestBoto3LambdaLayerPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Sid: "DeployLatestBoto3LambdaLayerPolicy"
Action:
- s3:PutObject
Resource: "*"
Lambda Function (Boto3 Lambda Layersを生成するLambda Function)
まず、Lambda-backed カスタムリソースのお作法として、cfnresponse.send()でシグナルを返すというものがありますので、必ず書くようにします。
Pythonのコードにcfnresponse.send()を書き忘れますと、CloudFormationのテンプレートを実行したとき、カスタムリソースの作成時にLambda Functionからシグナルが返らずに1時間待ちとなって、果てには作成に失敗します。
さらに、ロールバック時にもLambda Functionが実行され、そこでもシグナルが返らずに1時間待ちとなって、合計2時間待ちを食らいます。
次に、以下のコードでどのようにLambda Layersを生成しているかですが、まず、Lambda FunctionがLinux上で動いていることを利用し、Lambda Functionに書き込み権限がある/tmpに、subprocessとpipを駆使して、最新Boto3を書き込んでいます。
その後Boto3をzipに圧縮しますが、Lambda Functionが動くLinuxではzipコマンドが使えなかったため、Pythonのshutilを使って圧縮しています。
(ちなみにgzip等は使えるようです。何故標準zipが使えないかはよく分かりません。。。)
最後の処理で、出来上がったzipファイルをS3にアップロードします。
DeployLatestBoto3LambdaLayerLambdaFunction:
Description: "To make the Latest boto3 Lambda Layers and upload it to s3 bucket"
Type: "AWS::Lambda::Function"
Properties:
Code:
ZipFile: !Sub |
import boto3
import subprocess
import shutil
import cfnresponse
# Lambdaハンドラ
def lambda_handler(event, context):
try:
# 引数(event)チェック
if event['RequestType'] == 'Delete':
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
return
# mkdir /tmp/python
mkdir_command = subprocess.run(["mkdir", "-p", "/tmp/pip/python"], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
# pip install -t /tmp/python/ boto3
boto3_install_command = subprocess.run(["pip", "install", "-t", "/tmp/pip/python", "boto3"], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
# ls -al /tmp/python | grep boto3- | grep .dist-info
ls_boto3_command = subprocess.Popen(["ls", "/tmp/pip/python"], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
grep_boto3_command = subprocess.Popen(["grep", "boto3-"], stdin = ls_boto3_command.stdout, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
dist_boto3_command = subprocess.Popen(["grep", ".dist-info"], stdin = grep_boto3_command.stdout, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
dist_boto3_command_stdout = dist_boto3_command.communicate()[0]
boto3_version_str = dist_boto3_command_stdout.decode('utf-8').replace('boto3-', '').replace('.dist-info', '').replace('\n', '')
ls_boto3_command.stdout.close()
grep_boto3_command.stdout.close()
dist_boto3_command.stdout.close()
# /tmp/pythonをzipに圧縮
boto3_zip_path = '/tmp/boto-'+boto3_version_str
shutil.make_archive(boto3_zip_path, 'zip', root_dir='/tmp/pip')
# ls /tmp/
ls_boto3_command = subprocess.run(["ls", "/tmp/"], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
# boto3-x.x.xxx.zip をS3にアップロード
s3Resource = boto3.resource('s3')
s3FileUpload = s3Resource.meta.client.upload_file(Filename=boto3_zip_path+'.zip', Bucket=event['ResourceProperties']['S3Bucket'], Key='boto3-'+boto3_version_str+'.zip')
# 戻り値の生成
responseData = {
'S3Bucket': event['ResourceProperties']['S3Bucket'],
'S3Key': 'boto3-'+boto3_version_str+'.zip'
}
cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
except Exception:
# 戻り値の生成
cfnresponse.send(event, context, cfnresponse.FAILED, {})
FunctionName: DeployLatestBoto3LambdaLayer
Handler: index.lambda_handler
MemorySize: 256
ReservedConcurrentExecutions: 1
Role: !GetAtt DeployLatestBoto3LambdaLayerRole.Arn
Runtime: "python3.6"
Timeout: 300
Tags:
- Key: Name
Value: 'DeployLatestBoto3LambdaLayer'
- Key: CloudformationArn
Value: !Ref 'AWS::StackId'
カスタムリソース (Boto3 Lambda Layersを生成するLambda Functionを呼び出す)
Lambda Functionが書ければ、カスタムリソースはこれだけです。
ServiceTokenでLambda Functionを指定して呼び出します。
S3Bucketは、Lambda Functionの内部で利用するための独自パラメータです。
LatestBoto3LambdaLayerCustomResource:
Type: Custom::LatestBoto3LambdaLayerCustomResource
Properties:
ServiceToken: !GetAtt DeployLatestBoto3LambdaLayerLambdaFunction.Arn
S3Bucket: 'hogehoge'
Lambda Layers
カスタムリソースの実行が正常に完了すると、Lambda LayersのzipファイルがアップロードされたS3 Bucket名と、zipファイルのパスを示すS3Keyが返ります。
これらの値をContentのパラメータに代入します。
LatestBoto3LambdaLayer:
Type: "AWS::Lambda::LayerVersion"
Properties:
Description: !GetAtt LatestBoto3LambdaLayerCustomResource.S3Key
CompatibleRuntimes:
- python3.6
- python3.7
Content:
S3Bucket: !GetAtt LatestBoto3LambdaLayerCustomResource.S3Bucket
S3Key: !GetAtt LatestBoto3LambdaLayerCustomResource.S3Key
LayerName: 'boto3-python-layer'
テンプレート実行結果
CloudFormationテンプレート内で、Lambda Layersを生成するLambda FunctionがLambda-backed カスタムリソースとして実行され、結果、最新のBoto3を含むLambda Layersがアップロードされました。
まとめ
Lambda Layersを生成するLambda FunctionをLambda-backed カスタムリソースで実行することで、Lambda Layersを手作業で作ることなく、自動生成できるようになりました。
手作業でのzip圧縮&アップロードがなくなり、AttributeErrorも回避でき、非常に快適となります。
また、Lambda Function内でLinuxのコマンドが実行できること、特にpipが実行できることは、覚えておけば今後何かと役に立ちそうです。
Boto3に限らず、あらゆるパッケージをCI/CDパイプラインの中で自動的にLambda Layers化できると、非常に捗ります。
なお、Lambda-backed カスタムリソースをupdate-stackの度に毎回実行する方法については、CloudFormationでスタック内の特定リソースを毎回変更する方法 (超簡単で地味に実現)に記載した超地味なやり方で実現しています。