CloudFormation一撃でWeb上のファイル(Python)を指定してLambda関数を作成してみた

2021.07.08

創立記念日のブログの大波にのらなきゃ、このビッグウェーブに。
とおもって、ウエーブ -> ウエブ(ちょと違う) -> WWWeb
Web上のコードをLambdaにしてみるか。と、久しぶりにコーディングしてみました

前から思っていたのですが、URLを指定してLambda関数作れたらいいなと。 CloudFormationのテンプレートに直接書くこともできるんですが、こまかくいろいろ制限がありますし
かといって、S3にzipとして、毎回置くのもちょっと手間なんすよね。

やりたいことは、URLを指定して、それが、Lambda関数でほしいんよ!!!(純正実装はよ)
とわがまま、言ってもしょうがないので、ないものは作るか。と作りはじめたら創立記念日は過ぎていました。

例のごとく、カスタムリソースでの処理なので一見さんお断りなんですが、コードはオープンにしてますので、ご自由に改変、お使いください。

なお、URLで指定するファイルは、Pythonのコードを前提としてます、作成する関数もPythonとしていますので、ご了承ください おそらく、このブログが必要な人はこのままで使うことはないと思いますので、適時調整してください

処理概要

  1. URLで指定された複数ファイルをLambdaでダウンロードします
  2. S3にzipで固めてアップロードします
  3. zipからLambda関数(Python)を作成します

カスタムリソースで、1.2の処理を実装してます。

テンプレート

パラメータPython

Url に作成するソースとなるファイルを指定します。デフォルトではまさにこのカスタムリソースのPythonファイルのURLを指定しています。
UrlCfnResponse はpythonのcfn-responseモジュールを指定しています(URLを増やすと、まとめてzipにするようになっています)
LambdaFunctionHandler はURLで指定するファイル名また呼び出す関数を指定してくださいLambda関数のHandlerです。

テンプレートファイル

https://github.com/ambasad/create-lambda-from-urls/blob/main/create-lambda-from-urls.yml

AWSTemplateFormatVersion: '2010-09-09'

Parameters:

  ZipFileName:
    Type: String
    Default: my-deployment-package.zip

  LambdaFunctionHandler:
    Type: String
    Default: copy-to-s3-zip.lambda_handler
    
  Url:
    Type: String
    Default: https://raw.githubusercontent.com/ambasad/create-lambda-from-urls/master/copy-to-s3-zip.py

  UrlCfnResponse:
    Type: String
    Default: https://raw.githubusercontent.com/awslabs/aws-cloudformation-templates/master/aws/services/CloudFormation/MacrosExamples/StackMetrics/lambda/cfnresponse.py

Resources:

  MyFunction:
    DependsOn:
      - S3CopyAndZipped
    Type: AWS::Lambda::Function
    Properties:
      Role: !GetAtt MyFunctionRole.Arn
      Runtime: python3.8
      Handler: !Ref LambdaFunctionHandler
      Code:
        S3Bucket: !Ref S3CopyAndZippedBucket
        S3Key: !Ref ZipFileName

  MyFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      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"

  S3CopyAndZippedBucket:
    Type: AWS::S3::Bucket

  S3CopyAndZipped:
    Type: Custom::CopyToS3
    Properties:
      ServiceToken: !GetAtt CopyFilesToS3AndZipped.Arn
      S3BucketName: !Ref S3CopyAndZippedBucket
      ZipFileName: !Ref ZipFileName
      Urls:
        - !Ref Url
        - !Ref UrlCfnResponse
  CopyFilesToS3AndZipped:
    Type: AWS::Lambda::Function
    Properties:
      Role: !GetAtt CopyFilesToS3AndZippednRole.Arn
      Runtime: python3.8
      Handler: index.lambda_handler
      Code:
        ZipFile: |

          import os
          import urllib.request
          from urllib.parse import urlparse
          import json
          import boto3
          import cfnresponse
          import zipfile

          print('Loading function')

          s3 = boto3.resource('s3')


          def save_to_local(url):
              urlPath = urlparse(url).path
              fileName = os.path.basename(urlPath)
              filePath = '/tmp/' + fileName
              urllib.request.urlretrieve(url, filePath)
              return filePath, fileName


          def upload_to_s3(filePath, bucket):
              fileName = os.path.basename(filePath)
              s3.Object(bucket, fileName).put(Body=open(filePath, 'rb'))


          def copy_to_s3(url, bucket, zipFilePath):
              filePath, fileName = save_to_local(url)
              upload_to_s3(filePath, bucket)
              with zipfile.ZipFile(zipFilePath, "a", zipfile.ZIP_DEFLATED) as zf:
                  zf.write(filePath, fileName)

          def lambda_handler(event, context):
              print('Received event: ' + json.dumps(event, indent=2))

              if event['RequestType'] == 'Create':
                  # get the properties set in the CloudFormation resource
                  properties = event['ResourceProperties']
                  urls = properties['Urls']
                  bucket = properties['S3BucketName']
                  zipFileName = properties['ZipFileName']

                  try:
                      zipFilePath = '/tmp/' + zipFileName
                      for url in urls:
                          copy_to_s3(url, bucket, zipFilePath)
                      s3.Bucket(bucket).upload_file(zipFilePath, zipFileName)

                  except Exception as e:
                      print(e)
                      cfnresponse.send(event, context, cfnresponse.FAILED, {
                                      'Response': 'Failure'})
                      return

              cfnresponse.send(event, context, cfnresponse.SUCCESS,
                              {'Response': 'Success'})
      
  CopyFilesToS3AndZippednRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:*
                Resource: arn:aws:logs:*:*:*
              - Effect: Allow
                Action:
                  - s3:PutObject
                Resource: '*'
Outputs:
  MyFunction:
    Value: !Ref MyFunction
  S3CopyAndZippedBucket:
    Value: !Ref S3CopyAndZippedBucket

作成後のリソース

Lambda関数

S3 バケット

スタックを削除する場合は、バケットにURLから取得したファイル、また圧縮したzipが残っているので、オブジェクトを手動で削除してください

まとめ

処理自体は、参考情報に上げているURLのコードがほとんどです。 エラー処理などはいれてないので、ぜひブラッシュアップして使ってください。 作成する関数のRoleの権限は適時調整ください。 Githubにソースはあるんだけど、Lambdaにするのにひと手間かかるんだよねっていう時につかいますかね?

どうだろう・・・ピンポイントで1人くらいに刺さると嬉しいです。
カスタムリソース部分を外部のプロバイダ化すると思ったよりも潰しが効きそうな気はします。

参考情報

https://github.com/lrakai/lambda-copy-to-s3

https://www.dcom-web.co.jp/lab/cloud/aws/compress_files_uploaded_to_s3_using_lambda