🛠️

IaCジェネレーターを使って、既存のAWS Lambda関数をAWS CDKでデプロイしてみた。

2024/04/10に公開

Topicに CloudFormation とつける日は来ようとは・・・(実の所、CloudFormationはあまり使えていない人)

先日、発表されたIaCジェネレーターを試してみました。(色々やっているうちにだいぶ経ってしまった。)

https://aws.amazon.com/jp/blogs/news/import-entire-applications-into-aws-cloudformation/

自分の検証用の環境で試して、その後、業務で使っている環境でも試してみてます。※即実戦投入は結構珍しい。

手順自体はブログとかユーザーガイドに載っていますので、詳しくは書きません。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/generate-IaC.html

業務で使うにあたって、料金等発生するのかな?と思って、ブログとか料金ページを見てみましたが、
特に明記がなかったので、無料で使えるようですね。

試している時に、裏でAWS Lambda的なものが動いて、その中でSDKで情報集めているのかな・・・って妄想していました。

Scan

CloudFormationの左ペインに、IaCジェネレーター というのが出てきているので、こちらをクリックして、スキャンというエリアにある「新しいスキャンを開始」をクリックします。

途中で止められないみたいなので、終わるまで気長に待ちます。

説明書きには 1,000 個のリソースのスキャンには最大 10 分かかることがあります。 とありますが、
自分の検証環境の東京リージョンで試したところ、3,000ちょいありました(そんなにリソースないだろうと思ってたら、そこそこあった)が、10分ほどで終わっています。
別アカウントで、そこも10,000リソースほどありましたが(こっちはもっとあると思って、朝には終わってるかな・・・と思ってScan実行した人)、12分ほどで終わりました。結構早いですね。

終わると、有効日数とスキャンされたリソース数が表示されます。

Scan自体は、リージョン毎に実施しないとダメみたいなので、複数リージョンで色々作っている場合は、各リージョンでScanする必要がありそうです。
(自分の場合は、北バージニアとオレゴンリージョンにもそこそこリソースあるので、この辺りのリージョンでも実施しないとなぁ・・・と思っています。)

Scanの有効期限は30日とのことなので、定期的にアップデートする場合には、
AWS CLIとかAWS SDKを組み込んだLambda関数を作った方がいいのかな?と思ったりします。

テンプレート生成

以前、以下の記事で使ったリソースをテンプレート化しようと思います。

https://zenn.dev/keni_w/articles/bb74b3efbf4b7d

IaCジェネレーター下部にある、「テンプレートの生成」をクリックします。

テンプレート名等を入力し、次へ。
スクリーンショットでは、削除ポリシー置換ポリシーを更新 を変更しています。

テンプレートに含めたいリソースを探すのですが、
リソースを検索 という検索フィールド、ここで名前の検索できるといいんですが、できないみたいなので、できるようになると探しやすいなぁ・・・と思いました。
今後のアップデートに期待したいです。
タグでの検索はできるようなので、タグを付けてという対応はできそうです。
※ただ、Lambdaだとタグ付けしないんですよね・・・


すでにAWS CDK等でデプロイしているリソースは選択できないようになっています。
管理下にあるものを他のテンプレートでも使うというケースには使えないのかなと思います。

選んだら、次に進みます。

関連するリソースを表示してくれます。
Roleまで探すと面倒なので、持ってきてくれるのは嬉しいですね。

確認して、問題なければそのまま生成します。

ちょっと待つと完成します。
そのままStackを作ることもできます。

今回は、CDK化するのが目的なので、AWS CDK のタブに移って、YAMLのテンプレートファイルをダウンロードします。

CDK Migrate

気づいていなかったのですが、IaCジェネレーターと同じ時期に出たんですね。

https://aws.amazon.com/jp/blogs/news/announcing-cdk-migrate-a-single-command-to-migrate-to-the-aws-cdk/

実行コマンドは、テンプレート画面のAWS CDK のタブに出ている内容をそのまま使えます。

今回の場合こんな感じでした。
cdk migrate --stack-name csv-to-parquet-template --from-path ./csv-to-parquet-template-1708964869276.yaml --language typescript

注意点としては、cdk migrate コマンドは、テンプレートを生成した同じリージョンをデフォルトリージョンにしないとダメなようですね。
(つまりは、別リージョンで実施した人です)
元とするリソースがあるリージョンで実行しないとダメというのはわかります。
※デプロイ自体は別リージョンにするのは可能です。

実行が完了すると、CDK環境が構築されます。

L2化

migration実行すると、以下のような stack.tsファイルができます。
作成されたStackファイルを見ると、L1 Constrctで定義されています。

import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as s3 from 'aws-cdk-lib/aws-s3';

export interface CsvToParquetStackProps extends cdk.StackProps {
  /**
   * An Amazon S3 bucket in the same AWS-Region as your function. The bucket can be in a different AWS-account.
   * This property can be replaced with other exclusive properties
   */
  readonly lambdaFunction00csvtoparquetarm6400KfnpuCodeS3BucketOneOflKi5g: string;
  /**
   * The Amazon S3 key of the deployment package.
   * This property can be replaced with other exclusive properties
   */
  readonly lambdaFunction00csvtoparquetx866400buz6XCodeS3KeyOneOf4UOzg: string;
  /**
   * The Amazon S3 key of the deployment package.
   * This property can be replaced with other exclusive properties
   */
  readonly lambdaFunction00csvtoparquetarm6400KfnpuCodeS3KeyOneOfMSjzZ: string;
  /**
   * An Amazon S3 bucket in the same AWS-Region as your function. The bucket can be in a different AWS-account.
   * This property can be replaced with other exclusive properties
   */
  readonly lambdaFunction00csvtoparquetx866400buz6XCodeS3BucketOneOfqYp8d: string;
}

export class CsvToParquetStack extends cdk.Stack {
  public constructor(scope: cdk.App, id: string, props: CsvToParquetStackProps) {
    super(scope, id, props);

    // Resources
    const iamRole00csvtoparquetarm64rolernwx829v00Qavjr = new iam.CfnRole(this, 'IAMRole00csvtoparquetarm64rolernwx829v00Qavjr', {
      path: '/service-role/',
      managedPolicyArns: [
        'arn:aws:iam::aws:policy/AmazonS3FullAccess',
        'arn:aws:iam::123456789012:policy/service-role/AWSLambdaBasicExecutionRole-3c5486e3-e00d-45d4-a59d-09f34b662a62',
      ],
      maxSessionDuration: 3600,
      roleName: 'csv-to-parquet-arm64-role-rnwx829v',
      assumeRolePolicyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Action: 'sts:AssumeRole',
            Effect: 'Allow',
            Principal: {
              Service: 'lambda.amazonaws.com',
            },
          },
        ],
      },
    });
    iamRole00csvtoparquetarm64rolernwx829v00Qavjr.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;

    const iamRole00csvtoparquetx8664rolel0xh7iad00lXoHe = new iam.CfnRole(this, 'IAMRole00csvtoparquetx8664rolel0xh7iad00lXoHe', {
      path: '/service-role/',
      managedPolicyArns: [
        'arn:aws:iam::aws:policy/AmazonS3FullAccess',
        'arn:aws:iam::123456789012:policy/service-role/AWSLambdaBasicExecutionRole-39a1af64-fb11-42a6-8b29-f509eff9a188',
      ],
      maxSessionDuration: 3600,
      roleName: 'csv-to-parquet-x86_64-role-l0xh7iad',
      assumeRolePolicyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Action: 'sts:AssumeRole',
            Effect: 'Allow',
            Principal: {
              Service: 'lambda.amazonaws.com',
            },
          },
        ],
      },
    });
    iamRole00csvtoparquetx8664rolel0xh7iad00lXoHe.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;

    const lambdaFunction00csvtoparquetarm6400Kfnpu = new lambda.CfnFunction(this, 'LambdaFunction00csvtoparquetarm6400Kfnpu', {
      memorySize: 1024,
      description: 'CSV To Parquet For arm64',
      tracingConfig: {
        mode: 'PassThrough',
      },
      timeout: 300,
      runtimeManagementConfig: {
        updateRuntimeOn: 'Auto',
      },
      handler: 'lambda_function.lambda_handler',
      code: {
        s3Bucket: props.lambdaFunction00csvtoparquetarm6400KfnpuCodeS3BucketOneOflKi5g!,
        s3Key: props.lambdaFunction00csvtoparquetarm6400KfnpuCodeS3KeyOneOfMSjzZ!,
      },
      role: iamRole00csvtoparquetarm64rolernwx829v00Qavjr.attrArn,
      fileSystemConfigs: [
      ],
      functionName: 'csv-to-parquet-arm64',
      runtime: 'python3.9',
      packageType: 'Zip',
      loggingConfig: {
        logFormat: 'Text',
        logGroup: '/aws/lambda/csv-to-parquet-arm64',
      },
      ephemeralStorage: {
        size: 512,
      },
      layers: [
        'arn:aws:lambda:ap-northeast-1:123456789012:layer:pyarrow_arm64:5',
      ],
      architectures: [
        'arm64',
      ],
    });
    lambdaFunction00csvtoparquetarm6400Kfnpu.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;

    const lambdaFunction00csvtoparquetx866400buz6X = new lambda.CfnFunction(this, 'LambdaFunction00csvtoparquetx866400buz6X', {
      memorySize: 1024,
      description: 'CSV To Parquet For x86_64',
      tracingConfig: {
        mode: 'PassThrough',
      },
      timeout: 300,
      runtimeManagementConfig: {
        updateRuntimeOn: 'Auto',
      },
      handler: 'lambda_function.lambda_handler',
      code: {
        s3Bucket: props.lambdaFunction00csvtoparquetx866400buz6XCodeS3BucketOneOfqYp8d!,
        s3Key: props.lambdaFunction00csvtoparquetx866400buz6XCodeS3KeyOneOf4UOzg!,
      },
      role: iamRole00csvtoparquetx8664rolel0xh7iad00lXoHe.attrArn,
      fileSystemConfigs: [
      ],
      functionName: 'csv-to-parquet-x86_64',
      runtime: 'python3.9',
      packageType: 'Zip',
      loggingConfig: {
        logFormat: 'Text',
        logGroup: '/aws/lambda/csv-to-parquet-x86_64',
      },
      ephemeralStorage: {
        size: 512,
      },
      layers: [
        'arn:aws:lambda:ap-northeast-1:123456789012:layer:pyarrow_x86_64:7',
      ],
      architectures: [
        'x86_64',
      ],
    });
    lambdaFunction00csvtoparquetx866400buz6X.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;

    const s3Bucket00csvparquesttest2021122300RmKwd = new s3.CfnBucket(this, 'S3Bucket00csvparquesttest2021122300RMKwd', {
      notificationConfiguration: {
        queueConfigurations: [
        ],
        topicConfigurations: [
        ],
        lambdaConfigurations: [
          {
            function: lambdaFunction00csvtoparquetx866400buz6X.attrArn,
            filter: {
              s3Key: {
                rules: [
                  {
                    value: 'sourcefile/x86_64/',
                    name: 'Prefix',
                  },
                  {
                    value: '.csv',
                    name: 'Suffix',
                  },
                ],
              },
            },
            event: 's3:ObjectCreated:*',
          },
          {
            function: lambdaFunction00csvtoparquetarm6400Kfnpu.attrArn,
            filter: {
              s3Key: {
                rules: [
                  {
                    value: 'sourcefile/arm64/',
                    name: 'Prefix',
                  },
                  {
                    value: '.csv',
                    name: 'Suffix',
                  },
                ],
              },
            },
            event: 's3:ObjectCreated:*',
          },
        ],
      },
      publicAccessBlockConfiguration: {
        restrictPublicBuckets: true,
        ignorePublicAcls: true,
        blockPublicPolicy: true,
        blockPublicAcls: true,
      },
      bucketName: 'csv-parquest-test-20211223',
      ownershipControls: {
        rules: [
          {
            objectOwnership: 'BucketOwnerEnforced',
          },
        ],
      },
      bucketEncryption: {
        serverSideEncryptionConfiguration: [
          {
            bucketKeyEnabled: false,
            serverSideEncryptionByDefault: {
              sseAlgorithm: 'AES256',
            },
          },
        ],
      },
    });
    s3Bucket00csvparquesttest2021122300RmKwd.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;

    const lambdaPermission00functioncsvtoparquetarm6400cMWpY = new lambda.CfnPermission(this, 'LambdaPermission00functioncsvtoparquetarm6400cMWpY', {
      functionName: lambdaFunction00csvtoparquetarm6400Kfnpu.attrArn,
      action: 'lambda:InvokeFunction',
      sourceArn: s3Bucket00csvparquesttest2021122300RmKwd.attrArn,
      principal: 's3.amazonaws.com',
      sourceAccount: '123456789012',
    });
    lambdaPermission00functioncsvtoparquetarm6400cMWpY.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;

    const lambdaPermission00functioncsvtoparquetx866400sjsDk = new lambda.CfnPermission(this, 'LambdaPermission00functioncsvtoparquetx866400sjsDk', {
      functionName: lambdaFunction00csvtoparquetx866400buz6X.attrArn,
      action: 'lambda:InvokeFunction',
      sourceArn: s3Bucket00csvparquesttest2021122300RmKwd.attrArn,
      principal: 's3.amazonaws.com',
      sourceAccount: '123456789012',
    });
    lambdaPermission00functioncsvtoparquetx866400sjsDk.cfnOptions.deletionPolicy = cdk.CfnDeletionPolicy.DELETE;
  }
}

このままでも、デプロイできると思いますが、管理等をL2 Constrctできるところはやってしまった方がいいと思いますので、やってみました。

サンプルで作ったリソース群としては

  • Lambda関数 x2
  • それに付随するRole
  • S3バケット x1(既存のバケットを利用してますが)
    で、そんなには難しいないので、サクッとやってしまいましょう。

Lambdaのコードは、個人的にはsrcなりlambdaなりでディレクトリを作って、その中に置いて、デプロイするのがいいかなと思いますので、それも一緒にやっています。

こんな感じになりました。だいぶコード減りましたし、見やすくなったと思います。

import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { LambdaDestination } from 'aws-cdk-lib/aws-s3-notifications';
import path = require('path');

export class CsvToParquetStack extends cdk.Stack {
  public constructor(scope: cdk.App, id: string, props: cdk.StackProps) {
    super(scope, id, props);


    // Amazon S3 (既存バケットを使う)
    const s3BucketName = 'csv-parquest-test-20211223';
    const s3Bucket = s3.Bucket.fromBucketName(this, 'S3Bucket', s3BucketName);

    // AWS Lambda
    // Layer(すでにあるLayerを利用)
    const layerArm64 = lambda.LayerVersion.fromLayerVersionArn(this, 'LambdaLayerArm64', 'arn:aws:lambda:ap-northeast-1:123456789012:layer:pyarrow_arm64:1');
    const layerX86 = lambda.LayerVersion.fromLayerVersionArn(this, 'LambdaLayerX86', 'arn:aws:lambda:ap-northeast-1:123456789012:layer:pyarrow_x86_64:1');

    // Lambda関数
    const csvToParquetForArm64 = new lambda.Function(this, 'CsvToParquetForArm64', {
      memorySize: 1024,
      description: 'CSV To Parquet For arm64',
      tracing: lambda.Tracing.ACTIVE,
      timeout: cdk.Duration.seconds(300),
      code: lambda.Code.fromAsset('src'),
      handler: 'lambda_function.lambda_handler',
      runtime: lambda.Runtime.PYTHON_3_12,
      layers: [layerArm64]
    });
    s3Bucket.grantReadWrite(csvToParquetForArm64);

    const csvToParquetForX86 = new lambda.Function(this, 'CsvToParquetForX86', {
      memorySize: 1024,
      description: 'CSV To Parquet For arm64',
      tracing: lambda.Tracing.ACTIVE,
      timeout: cdk.Duration.seconds(300),
      code: lambda.Code.fromAsset('src'),
      handler: 'lambda_function.lambda_handler',
      runtime: lambda.Runtime.PYTHON_3_12,
      layers: [layerX86]
    });
    s3Bucket.grantReadWrite(csvToParquetForX86);

    // バケット既存の場合は、addEventNotificationを使う
    // See https://dev.classmethod.jp/articles/cdk-s3notification-kick-lambda/
    s3Bucket.addEventNotification(
      s3.EventType.OBJECT_CREATED,
      new LambdaDestination(csvToParquetForX86),
      {
        prefix: 'sourcefile/x86_64/',
        suffix: '.csv'
      },
    );
    s3Bucket.addEventNotification(
      s3.EventType.OBJECT_CREATED,
      new LambdaDestination(csvToParquetForArm64),
      {
        prefix: 'sourcefile/arm64/',
        suffix: '.csv'
      }
    )
  }
}

cdk diff をして、さて、問題ないっすね。ということで、
徐に cdk deploy を実行します・・・エラー。

 ❌  csvToParquet failed: Error [ValidationError]: The logical resource ids [LambdaFunction00csvtoparquetarm6400Kfnpu, LambdaPermission00functioncsvtoparquetx866400sjsDk, IAMRole00csvtoparquetarm64rolernwx829v00Qavjr, LambdaPermission00functioncsvtoparquetarm6400cMWpY, LambdaFunction00csvtoparquetx866400buz6X, IAMRole00csvtoparquetx8664rolel0xh7iad00lXoHe, S3Bucket00csvparquesttest2021122300RMKwd] provided in ResourceToImport do not exist in the template.
    at Request.extractError (/usr/local/share/npm-global/lib/node_modules/aws-cdk/lib/index.js:376:46692)
    at Request.callListeners (/usr/local/share/npm-global/lib/node_modules/aws-cdk/lib/index.js:376:91437)
    at Request.emit (/usr/local/share/npm-global/lib/node_modules/aws-cdk/lib/index.js:376:90885)
    at Request.emit (/usr/local/share/npm-global/lib/node_modules/aws-cdk/lib/index.js:376:199281)
    at Request.transition (/usr/local/share/npm-global/lib/node_modules/aws-cdk/lib/index.js:376:192833)
    at AcceptorStateMachine.runTo (/usr/local/share/npm-global/lib/node_modules/aws-cdk/lib/index.js:376:157705)
    at /usr/local/share/npm-global/lib/node_modules/aws-cdk/lib/index.js:376:158035
    at Request.<anonymous> (/usr/local/share/npm-global/lib/node_modules/aws-cdk/lib/index.js:376:193125)
    at Request.<anonymous> (/usr/local/share/npm-global/lib/node_modules/aws-cdk/lib/index.js:376:199356)
    at Request.callListeners (/usr/local/share/npm-global/lib/node_modules/aws-cdk/lib/index.js:376:91605) {
  code: 'ValidationError',
  time: 2024-04-08T16:38:54.396Z,
  requestId: '9f647823-0bb2-48a6-ae2d-0fde10ad2708',
  statusCode: 400,
  retryable: false,
  retryDelay: 191.10195176978496
}

The logical resource ids [LambdaFunction00csvtoparquetarm6400Kfnpu, LambdaPermission00functioncsvtoparquetx866400sjsDk, IAMRole00csvtoparquetarm64rolernwx829v00Qavjr, LambdaPermission00functioncsvtoparquetarm6400cMWpY, LambdaFunction00csvtoparquetx866400buz6X, IAMRole00csvtoparquetx8664rolel0xh7iad00lXoHe, S3Bucket00csvparquesttest2021122300RMKwd] provided in ResourceToImport do not exist in the template.

いや、stack.ts からLambdaFunction00csvtoparquetarm6400Kfnpu とか消したよね?ってなりまして、
よくよく見てみると、
migrate.json というファイルがあることに気づきました。
そして、そこに LambdaFunction00csvtoparquetarm6400Kfnpu 等定義されておりました。

無論、そのまま使うケースもあるかと思いますが、別途ソースコードを用意しましたので、このファイルが不要です。
なので、migrate.json を消して、再度 cdk deploy を実行します。

はい。デプロイ完了です。

csvToParquet: creating CloudFormation changeset...

 ✅  csvToParquet

✨  Deployment time: 73.26s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/csvToParquet/b362d8f0-f5cb-11ee-a2e7-0eaa6d8266f1

✨  Total time: 78.29s

コンソールでもちゃんとデプロイできていることがわかります。

あとは動作確認して問題なければOKです。

試してみて

1から作るよりは、短時間でProjectを形にできると思います。
L2 Constrct化は必須なような気がします(私は少なくともします)
実際に業務でも使ってみましたが、
Step Functionsのステートマシンなどもあり、Lambda関数もそこそこあるというものをimportとする形でしたので、同じものを作る&IaCツール管理下に置くという観点では、非常に役に立ちました。
※なお、フローの組み直しなどが発生したので、ほぼ見る影なしですがw

IaCジェネレーター自体は、既存リソースの洗い出しにも便利というXのPOSTを見ましたが、その通りだと思います。

まとめ

昨年末に書いた記事で、結構初期からVPC Lambdaを使っていたことを書いていますが・・・。

https://zenn.dev/keni_w/articles/2b815f7918211e#結構初期にvpc-lambda使ってた

実は、機能別に関数を用意していたので、機能追加とともに、Lambda関数が増えていって、
最終的には結構な数のLambda関数を管理していて、デプロイとかもマネコンでやってたので、めっちゃ大変(CLIでやればよかったという話もありますが)でした。
Serverless FrameworkとかAWS SAMが出てきて、こういうツールで管理できたらいいよねぇ・・・っていう話があったんですが、なかなか既存リソースをそのまま管理下に置くのは大変という話もあり、挫折したんですが、その時にこれ欲しかったです!w(まあ、この機能でも置き換えが必要にはなりますが)

と言いつつ、現在、自身の検証環境でも、業務で使っている環境でもIaC管理下にないAWS Lambdaが結構ある(ついでに言うと、古いNode.jsのランタイムのLambdaがあったりする)ので、これを使って、IaCツールというかAWS CDKで管理できるように進めていこうかなと思います。

Discussion