CDKのcdk import機能により手動デプロイしたLambdaをIaC管理する記事

記事タイトルとURLをコピーする

CS1の石井です。

タイトルの通り、手動デプロイしたLambdaをcdk importでCDK管理下におき、CDK上でLambdaを編集しデプロイしてみたいと思います。

前書き(本記事を書いた経緯)

弊プロジェクトでは現在セキュリティの強化やCI/CD環境の構成を取り組んでおります。

取り組みの一環として、プログラムを自動で静的解析し自動デプロイされるCI/CD環境を構成しています。

その際、過去にマネージドコンソールから手動デプロイされたLambdaをどうやってCI/CD環境へ取り込むか?という課題が出てきました。

この課題に対し、cdk importという機能でCDKの管理下にLambdaを取り込めれば パイプラインからCloudFormationでデプロイするという手法で解決できそうだったので調査しました。

本記事は最初にcdk importの概要について触れ、次に実際にLambda(Java)をインポートしてIaC管理するシナリオを実践してcdk importを理解したいと思います。

前提と対象読者

  • CDKのワークショップを終わらせている人
  • mavenのビルド環境がある人またはdevcontainerを動作させることが可能な人

なお、本記事で扱うLambdaはJavaで記載されていますが、ディレクトリ構成やコードはサンプルコードをコピーしてもらえれば問題ないため、Javaに関する前提知識は不要です。

cdk import概要

本項ではcdk importの概要を記載します。

cdk importは実態はCloudFormation のresource importです。

CloudFormation(以下CFn)のresource importはデプロイ済みのCFnスタックに対し、CFn外で作成されたリソースをCFnの管理下におく機能です。

cdk importはcdkコマンドからCDKで作成されたCFnスタックをresource importを実行しているというイメージです。

インポート対象のLambdaを踏まえ、cdk importとCFnの関係性について簡単に図で記載しました。

環境準備

デプロイ対象となるLambdaを手動でデプロイし、その後cdk importを実行して動きを確認します。

まずはcdkとmavenを実行可能な環境をdevcontainerを用いて用意したいと思います。

適当な空のディレクトリを作成し、以下のコマンドでdevcontainer.jsonを作成してください。 ※本記事では「lambda-mvn」というディレクトリで作成、作業しています

 mkdir .devcontainer
 touch .devcontainer/devcontainer.json
// .devcontainer/devcontainer.json
{
    "name": "Node.js & TypeScript",
    "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye",
    "features": {
        "ghcr.io/devcontainers/features/aws-cli:1": {},
        "ghcr.io/devcontainers/features/docker-in-docker:2": {},
        "ghcr.io/devcontainers-contrib/features/aws-cdk:2": {},
        "ghcr.io/devcontainers/features/java:1": {},
        "ghcr.io/devcontainers-contrib/features/maven-sdkman:2": {},
    },
    // ローカルのクレデンシャルをコンテナにマウントします。クレデンシャルのパスは各自の環境に合わせて書き換えてください
    "mounts": [
        "source=【各自の環境に合わせてください】/.aws/config,target=/home/node/.aws/config,type=bind,consistency=cached",
        "source=【各自の環境に合わせてください】/.aws/credentials,target=/home/node/.aws/credentials,type=bind,consistency=cached",
    ],
       "postStartCommand": "npx cdk init --language typescript"
}

上記のファイルができたらVSCodeのコマンドパレットからrebuild containerでdevcontainerを起動してください。

テスト用のLambdaを手動でデプロイ

Java8のLambdaをビルドし手動デプロイを行います。

まずは以下のコマンドでLambda用のディレクトリやファイルを作成します。

mkdir -p  lambda/src/main/java/com/example/
touch lambda/pom.xml
touch lambda/src/main/java/com/example/LambdaHandler.java

以下のようなディレクトリ構造になっていればOKです

node ➜ /workspaces/lambda-mvn (master) $ ls -l
total 172
drwxr-xr-x   3 node node     96 Dec 17 01:51 bin
-rw-r--r--   1 node node   2861 Dec 17 01:51 cdk.json
-rw-r--r--   1 node node    157 Dec 17 01:51 jest.config.js
drwxr-xr-x   4 node node    128 Dec 17 01:50 lambda
drwxr-xr-x   3 node node     96 Dec 17 01:51 lib
drwxr-xr-x 219 node node   7008 Dec 17 01:52 node_modules
-rw-r--r--   1 node node    536 Dec 17 01:51 package.json
-rw-r--r--   1 node node 154493 Dec 17 01:52 package-lock.json
-rw-r--r--   1 node node    536 Dec 17 01:51 README.md
drwxr-xr-x   3 node node     96 Dec 17 01:51 test
-rw-r--r--   1 node node    663 Dec 17 01:51 tsconfig.json

node ➜ /workspaces/lambda-mvn (master) $ tree lambda/
lambda/
├── pom.xml
└── src
    └── main
        └── java
            └── com
                └── example
                    └── LambdaHandler.java

5 directories, 2 files
node ➜ /workspaces/lambda-mvn (master) $ 

デプロイするLambdaを記載

適当にハローワールドが出るLambdaを作成します。

// lambda/src/main/java/com/example/LambdaHandler.java

package com.example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class LambdaHandler implements RequestHandler<Object, String> {

    @Override
    public String handleRequest(Object input, Context context) {
        context.getLogger().log("Input: " + input);
        return "Hello from Lambda!";
    }
}

次にmavenのビルド手順が記載されたpom.xmlを編集します。

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>hello-lambda</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- AWS Lambda Java core -->
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.2.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.4</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <createDependencyReducedPom>false</createDependencyReducedPom>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Lambdaをmavenからビルド

mvn -f lambda/pom.xml clean package コマンドでjarファイルを作成してJavaファイルをデプロイ可能な状態にします。

ビルド後に以下のようにLambdaディレクトリにtargetというディレクトリが新規に作成されます。

lambda/target/hello-lambda-1.0-SNAPSHOT.jarというファイルがあれば成功です。

node ➜ /workspaces/lambda-mvn (main) $ tree lambda
lambda
├── dependency-reduced-pom.xml
├── pom.xml
├── src
│   └── main
│       └── java
│           └── com
│               └── example
│                   └── LambdaHandler.java
└── target
    ├── classes
    │   └── com
    │       └── example
    │           └── LambdaHandler.class
    ├── generated-sources
    │   └── annotations
    ├── hello-lambda-1.0-SNAPSHOT.jar
    ├── maven-archiver
    │   └── pom.properties
    ├── maven-status
    │   └── maven-compiler-plugin
    │       └── compile
    │           └── default-compile
    │               ├── createdFiles.lst
    │               └── inputFiles.lst
    ├── original-hello-lambda-1.0-SNAPSHOT.jar
    └── test-classes

17 directories, 9 files
node ➜ /workspaces/lambda-mvn (main) $ 

JavaのLambdaを手動デプロイ

ビルドが完了したため、以下のaws cliのコマンドでLambdaをデプロイしてみます。 Lambdaの名前は「mvn-test-lambda」と指定しています。

# Lambdaが使用するiamロールの作成
aws iam create-role --role-name lambda-execution-role --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Principal": {"Service": "lambda.amazonaws.com"},"Action": "sts:AssumeRole"}]}'
aws iam attach-role-policy --role-name lambda-execution-role --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# アカウント番号を取得して変数に格納
account=$(aws sts get-caller-identity --query "Account" --output text)

# アカウント番号を使用してLambda関数を作成
aws lambda create-function --function-name mvn-test-lambda \
--runtime java8.al2 --role arn:aws:iam::${account}:role/lambda-execution-role \
--handler com.example.LambdaHandler::handleRequest \
--zip-file fileb://lambda/target/hello-lambda-1.0-SNAPSHOT.jar

上記でデプロイが完了したらmvn-test-lambdaという名前でデプロイされます。 早速以下のコマンドでテストしてみます。

aws lambda invoke --function-name mvn-test-lambda --payload '{}' output.txt

問題なければ以下のような表示になると思います。

node ➜ /workspaces/lambda-mvn (master) $ aws lambda invoke --function-name mvn-test-lambda --payload '{}' output.txt
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
node ➜ /workspaces/lambda-mvn (master) $ cat output.txt 
"Hello from Lambda!"
node ➜ /workspaces/lambda-mvn (master) $ 

cdk importの実践

ここからようやくCDKの話になります。 ※cdk自体はdevcontainerの中でinitを行っているため、このプロジェクトを使用する前提で記載します。

cdk importは冒頭でも触れた通り、CFnのresource importでCDKで記載したリソースと実際のリソースをCFnのスタックに取り込みます。

そのため、cdk importは以下の順番で作業を行います。

  1. 空スタックをデプロイ
  2. CDKにデプロイ中のLambdaと同様の記載を実施
  3. cdk importを実行
  4. CDK管理になったLambdaをCDK上で更新してデプロイ

上記の作業段取りのイメージ図が以下となります。

空スタックをデプロイ

まずはcdkの初期状態でデプロイを行い空スタックを作成してみます。

node ➜ /workspaces/lambda-mvn (master) $ npx cdk deploy

✨  Synthesis time: 8.06s

LambdaMvnStack:  start: Building 21f7d61d14a9c6052a3cc0ce95eef7291253c66144c95c3e78e1e34de00c2c26:current_account-current_region
LambdaMvnStack:  success: Built 21f7d61d14a9c6052a3cc0ce95eef7291253c66144c95c3e78e1e34de00c2c26:current_account-current_region
LambdaMvnStack:  start: Publishing 21f7d61d14a9c6052a3cc0ce95eef7291253c66144c95c3e78e1e34de00c2c26:current_account-current_region
LambdaMvnStack:  success: Published 21f7d61d14a9c6052a3cc0ce95eef7291253c66144c95c3e78e1e34de00c2c26:current_account-current_region
LambdaMvnStack: deploying... [1/1]
LambdaMvnStack: creating CloudFormation changeset...

 ✅  LambdaMvnStack

✨  Deployment time: 11.33s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/LambdaMvnStack/93607330-9c81-11ee-b0e5-069bcb95f15d

✨  Total time: 19.38s


node ➜ /workspaces/lambda-mvn (master) $

上記の空のスタックをデプロイしたあと、CFnの状態を確認してみます。

#スタック名を確認
npx cdk ls 
#スタック名はLambdaMvnStackを想定
aws cloudformation get-template --stack-name LambdaMvnStack

get-templateコマンドで見るとResourceの部分がメタデータぐらいしかなく、リソースは何も作成されていないことがわかります。

以下は表示結果です。

node ➜ /workspaces/lambda-mvn (master) $ npx cdk ls
LambdaMvnStack

node ➜ /workspaces/lambda-mvn (master) $ aws cloudformation get-template --stack-name LambdaMvnStack
{
    "TemplateBody": {
        "Resources": {
            "CDKMetadata": {
                "Type": "AWS::CDK::Metadata",
                "Properties": {
                    "Analytics": "v2:deflate64:H4sIAAAAAAAA/zPSs7TUM1RMLC/WTU7J1s3JTNKrDi5JTM7WcU7LC0otzi8tSk4FsZ3z81IySzLz82p18vJTUvWyivXLjAz0LPQMFLOKMzN1i0rzSjJzU/WCIDQAyER7cVgAAAA="
                },
                "Metadata": {
                    "aws:cdk:path": "LambdaMvnStack/CDKMetadata/Default"
                },
                "Condition": "CDKMetadataAvailable"
            }
        },
~以下省略~
node ➜ /workspaces/lambda-mvn (master) $ 

cdk にLambdaを記載

cdk importは事前にインポートする対象のリソースをIaCとして記載する必要があります。

以下のようにCDKのコードを編集します。

// /workspaces/lambda-mvn/lib/lambda-mvn-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as lambda from "aws-cdk-lib/aws-lambda";

export class LambdaMvnStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda関数の定義
    new lambda.Function(this, "HelloLambdaFunction", {
      functionName: "mvn-test-lambda",
      runtime: lambda.Runtime.JAVA_8,
      code: lambda.Code.fromAsset(
        "lambda/target/hello-lambda-1.0-SNAPSHOT.jar"
      ), 
      handler: "com.example.LambdaHandler::handleRequest", 
    });
  }
}


上記のコードはすでにjarファイルがビルドされた前提での記載になっています。 そのためpackage.jsonのscriptの項目に以下のbuild-lambdaというコマンドとdeployコマンドを追加します。

{
  "name": "lambda-mvn",
  "version": "0.1.0",
  "bin": {
    "lambda-mvn": "bin/lambda-mvn.js"
  },
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "build-lambda": "mvn -f lambda/pom.xml clean package ",
    "deploy": "npm run build-lambda && npx cdk deploy",
    "cdk": "cdk"
  },

//以下省略

※CDKのコードにビルドするコマンドをshellで実行することも可能だと思いますが、飽くまでCDKはAWSリソースに限定したいため OS上の操作はpackage.jsonに寄せています。

cdk import実行

CDKのコードが書けたため、npx cdk importコマンドでcdk importを実行します。

以下は実行結果例です。

node ➜ /workspaces/lambda-mvn (master) $ npx cdk import
The 'cdk import' feature is currently in preview.
LambdaMvnStack
LambdaMvnStack/HelloLambdaFunction/ServiceRole/Resource (AWS::IAM::Role): enter RoleName [undefined]: lambda-execution-role
LambdaMvnStack/HelloLambdaFunction/Resource (AWS::Lambda::Function): import with FunctionName=mvn-test-lambda (yes/no) [default: yes]? yes
LambdaMvnStack: importing resources into stack...
LambdaMvnStack: creating CloudFormation changeset...

 ✅  LambdaMvnStack
Import operation complete. We recommend you run a drift detection operation to confirm your CDK app resource definitions are up-to-date. Read more here: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/detect-drift-stack.html

node ➜ /workspaces/lambda-mvn (master) $ 

npx cdk importを実行すると LambdaMvnStack/HelloLambdaFunction/ServiceRole/Resource (AWS::IAM::Role): enter RoleName [undefined]: と聞かれますが、実行Roleの名前の入力を求められます。

ここは先ほどaws cliで作成した「lambda-execution-role」を入力しEnterを押します。

次にLambdaMvnStack/HelloLambdaFunction/Resource (AWS::Lambda::Function): import with FunctionName=mvn-test-lambda (yes/no) [default: yes]?とプロンプト上で聞かれますが

これは、mvn-test-lambda という名前の Lambda 関数を現在の CDK スタックにインポートするかどうかを尋ねています。yes を選択すると、その関数がスタックにインポートされます。

問題ないのでyesと回答してインポートを行います。

インポートされたCFnの状態を確認

これでCFnはインポートが完了した状態となります。 GUIの画面でも以下のように表示されています。

また、テンプレートも初期の状態からLambdaリソースが追記されています。

かなり見辛いのですが、HelloLambdaFunctionがテンプレートの中に存在していると思います。

node ➜ /workspaces/lambda-mvn (master) $ aws cloudformation get-template --stack-name LambdaMvnStack
{
    "TemplateBody": "Resources:\n  CDKMetadata:\n    Type: AWS::CDK::Metadata\n    Properties:\n      Analytics: v2:deflate64:H4sIAAAAAAAA/zPSs7TUM1RMLC/WTU7J1s3JTNKrDi5JTM7WcU7LC0otzi8tSk4FsZ3z81IySzLz82p18vJTUvWyivXLjAz0LPQMFLOKMzN1i0rzSjJzU/WCIDQAyER7cVgAAAA=\n    Metadata:\n      aws:cdk:path: LambdaMvnStack/CDKMetadata/Default\n    Condition: CDKMetadataAvailable\n  HelloLambdaFunctionServiceRole82D29796:\n    Type: AWS::IAM::Role\n    Properties:\n      AssumeRolePolicyDocument:\n        Statement:\n          - Action: sts:AssumeRole\n            Effect: Allow\n            Principal:\n              Service: lambda.amazonaws.com\n        Version: \"2012-10-17\"\n      ManagedPolicyArns:\n        - Fn::Join:\n            - \"\"\n            - - \"arn:\"\n              - Ref: AWS::Partition\n              - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole\n    Metadata:\n      aws:cdk:path: LambdaMvnStack/HelloLambdaFunction/ServiceRole/Resource\n    DeletionPolicy: Retain\n  HelloLambdaFunction3DCA9067:\n    Type: AWS::Lambda::Function\n    Properties:\n      Code:\n        S3Bucket:\n          Fn::Sub: cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}\n        S3Key: 5f3ed4d0c23161d4e9dc5a603dfa7837fca5e4b5e0871e35ea066250ac840b3c.jar\n      FunctionName: mvn-test-lambda\n      Handler: com.example.LambdaHandler::handleRequest\n      Role:\n        Fn::GetAtt:\n          - HelloLambdaFunctionServiceRole82D29796\n          - Arn\n      Runtime: java8\n    DependsOn:\n      - HelloLambdaFunctionServiceRole82D29796\n    Metadata:\n      aws:cdk:path
: LambdaMvnStack/HelloLambdaFunction/Resource\n      aws:asset:path: asset.5f3ed4d0c23161d4e9dc5a603dfa7837fca5e4b5e0871e35ea066250ac840b3c.jar\n      aws:asset:is-bundled: false\n      aws:asset:property: Code\n    DeletionPolicy: Retain\nConditions:\n  CDKMetadataAvailable:\n    Fn::Or:\n      - Fn::Or:\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - af-south-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - ap-east-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - ap-northeast-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - ap-northeast-2\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - ap-south-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - ap-southeast-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - ap-southeast-2\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - ca-central-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - cn-north-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - cn-northwest-1\n      - Fn::Or:\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - eu-central-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - eu-north-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - eu-south-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - eu-west-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - eu-west-2\n          - Fn::Equals:\n              - Ref:
 AWS::Region\n              - eu-west-3\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - me-south-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - sa-east-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - us-east-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - us-east-2\n      - Fn::Or:\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - us-west-1\n          - Fn::Equals:\n              - Ref: AWS::Region\n              - us-west-2\nParameters:\n  BootstrapVersion:\n    Type: AWS::SSM::Parameter::Value<String>\n    Default: /cdk-bootstrap/hnb659fds/version\n    Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]\nRules:\n  CheckBootstrapVersion:\n    Assertions:\n      - Assert:\n          Fn::Not:\n            - Fn::Contains:\n                - - \"1\"\n                  - \"2\"\n                  - \"3\"\n                  - \"4\"\n                  - \"5\"\n                - Ref: BootstrapVersion\n        AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.\n",
    "StagesAvailable": [
        "Original",
        "Processed"
    ]
}
node ➜ /workspaces/lambda-mvn (master) $ 

CDKからLambdaを編集してデプロイする

手動デプロイしたLambdaがCDK管理下になったため、早速Lambdaを編集してみます。

// workspaces/lambda-mvn/lambda/src/main/java/com/example/LambdaHandler.java
package com.example;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class LambdaHandler implements RequestHandler<Object, String> {

    @Override
    public String handleRequest(Object input, Context context) {
        context.getLogger().log("Input: " + input);
        return "Hello from Lambda! for CDK";
    }
}

文字を"Hello from Lambda! for CDK";に変更しただけですが、これでデプロイできるか試してみます。

ただし、mavenによるビルドを経てデプロイしたいため、先ほどpackage.jsonに記載したdeployコマンドを使ってデプロイしてみます。

以下コマンドを実行し、デプロイを実行します。

npm run deploy

以下は実行結果例です。成功すれば以下のログが出力されます。

node ➜ /workspaces/lambda-mvn (master) $ npm run deploy

> lambda-mvn@0.1.0 deploy
> npm run build-lambda && npx cdk deploy


> lambda-mvn@0.1.0 build-lambda
> mvn -f lambda/pom.xml clean package

[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------------< com.example:hello-lambda >----------------------
[INFO] Building hello-lambda 1.0-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- clean:3.2.0:clean (default-clean) @ hello-lambda ---
[INFO] Deleting /workspaces/lambda-mvn/lambda/target
[INFO] 
[INFO] --- resources:3.3.1:resources (default-resources) @ hello-lambda ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /workspaces/lambda-mvn/lambda/src/main/resources
[INFO] 
[INFO] --- compiler:3.11.0:compile (default-compile) @ hello-lambda ---
[INFO] Changes detected - recompiling the module! :source
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file with javac [debug target 1.8] to target/classes
[WARNING] bootstrap class path not set in conjunction with -source 8
[WARNING] source value 8 is obsolete and will be removed in a future release
[WARNING] target value 8 is obsolete and will be removed in a future release
[WARNING] To suppress warnings about obsolete options, use -Xlint:-options.
[INFO] 
[INFO] --- resources:3.3.1:testResources (default-testResources) @ hello-lambda ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /workspaces/lambda-mvn/lambda/src/test/resources
[INFO] 
[INFO] --- compiler:3.11.0:testCompile (default-testCompile) @ hello-lambda ---
[INFO] No sources to compile
[INFO] 
[INFO] --- surefire:3.2.2:test (default-test) @ hello-lambda ---
[INFO] No tests to run.
[INFO] 
[INFO] --- jar:3.3.0:jar (default-jar) @ hello-lambda ---
[INFO] Building jar: /workspaces/lambda-mvn/lambda/target/hello-lambda-1.0-SNAPSHOT.jar
[INFO] 
[INFO] --- shade:3.2.4:shade (default) @ hello-lambda ---
[INFO] Including com.amazonaws:aws-lambda-java-core:jar:1.2.1 in the shaded jar.
[WARNING] aws-lambda-java-core-1.2.1.jar, hello-lambda-1.0-SNAPSHOT.jar define 1 overlapping resource: 
[WARNING]   - META-INF/MANIFEST.MF
[WARNING] maven-shade-plugin has detected that some class files are
[WARNING] present in two or more JARs. When this happens, only one
[WARNING] single version of the class is copied to the uber jar.
[WARNING] Usually this is not harmful and you can skip these warnings,
[WARNING] otherwise try to manually exclude artifacts based on
[WARNING] mvn dependency:tree -Ddetail=true and the above output.
[WARNING] See http://maven.apache.org/plugins/maven-shade-plugin/
[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing /workspaces/lambda-mvn/lambda/target/hello-lambda-1.0-SNAPSHOT.jar with /workspaces/lambda-mvn/lambda/target/hello-lambda-1.0-SNAPSHOT-shaded.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.365 s
[INFO] Finished at: 2023-12-17T02:57:54Z
[INFO] ------------------------------------------------------------------------

✨  Synthesis time: 5.17s

LambdaMvnStack:  start: Building 771c920b8d6456ee3ac4c68b37eeda125fc1c70fc0d41a16a47661521c55c504:current_account-current_region
LambdaMvnStack:  success: Built 771c920b8d6456ee3ac4c68b37eeda125fc1c70fc0d41a16a47661521c55c504:current_account-current_region
LambdaMvnStack:  start: Building aa3575a18417d9772b316f300f661a4782480693b933e732e224ca84a77ced2c:current_account-current_region
LambdaMvnStack:  success: Built aa3575a18417d9772b316f300f661a4782480693b933e732e224ca84a77ced2c:current_account-current_region
LambdaMvnStack:  start: Publishing 771c920b8d6456ee3ac4c68b37eeda125fc1c70fc0d41a16a47661521c55c504:current_account-current_region
LambdaMvnStack:  start: Publishing aa3575a18417d9772b316f300f661a4782480693b933e732e224ca84a77ced2c:current_account-current_region
LambdaMvnStack:  success: Published 771c920b8d6456ee3ac4c68b37eeda125fc1c70fc0d41a16a47661521c55c504:current_account-current_region
LambdaMvnStack:  success: Published aa3575a18417d9772b316f300f661a4782480693b933e732e224ca84a77ced2c:current_account-current_region
LambdaMvnStack: deploying... [1/1]
LambdaMvnStack: creating CloudFormation changeset...

 ✅  LambdaMvnStack

✨  Deployment time: 21.75s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXX:stack/LambdaMvnStack/93607330-9c81-11ee-b0e5-069bcb95f15d

✨  Total time: 26.91s


node ➜ /workspaces/lambda-mvn (master) $

テストを実行

正常にデプロイができれば下記コマンドで実行確認を行ってみます。

aws lambda invoke --function-name mvn-test-lambda --payload '{}' output.txt

以下は実行例ですが、想定通りHello from Lambda! for CDKという文字列が返ってきました。

node ➜ /workspaces/lambda-mvn (master) $ aws lambda invoke --function-name mvn-test-lambda --payload '{}' output.txt
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
node ➜ /workspaces/lambda-mvn (master) $ cat output.txt 
"Hello from Lambda! for CDK"node ➜ /workspaces/lambda-mvn (master) $ 

パイプラインスタックを経由していた場合どうすべきか?

ここからは補足ですがリソースかパイプラインを経由してスタックがデプロイされている場合を考えてみます。

同じ要領で「デプロイスタックを対象にimportコマンドを使えば良いんじゃないか?」と思っていたのですが・・・実際やってみたらできませんでした。

node ➜ /workspaces/lambda-mvn (main) $ npx cdk import PipelinesStack/DeployStage/LambdaMvnStack
The 'cdk import' feature is currently in preview.
PipelinesStack/DeployStage/LambdaMvnStack (DeployStage-LambdaMvnStack)
PipelinesStack/DeployStage/LambdaMvnStack/HelloLambdaFunction/ServiceRole/Resource (AWS::IAM::Role): enter RoleName [undefined]: lambda-execution-role
PipelinesStack/DeployStage/LambdaMvnStack/HelloLambdaFunction/Resource (AWS::Lambda::Function): import with FunctionName=mvn-test-lambda (yes/no) [default: yes]? 
PipelinesStack/DeployStage/LambdaMvnStack (DeployStage-LambdaMvnStack): importing resources into stack...
DeployStage-LambdaMvnStack: creating CloudFormation changeset...

 ❌  PipelinesStack/DeployStage/LambdaMvnStack (DeployStage-LambdaMvnStack) failed: Error [ValidationError]: As part of the import operation, you cannot modify or add [RoleArn]
    at Request.extractError (/workspaces/lambda-mvn/node_modules/aws-cdk/lib/index.js:362:46430)
    at Request.callListeners (/workspaces/lambda-mvn/node_modules/aws-cdk/lib/index.js:362:90083)
    at Request.emit (/workspaces/lambda-mvn/node_modules/aws-cdk/lib/index.js:362:89531)
    at Request.emit (/workspaces/lambda-mvn/node_modules/aws-cdk/lib/index.js:362:196289)
    at Request.transition (/workspaces/lambda-mvn/node_modules/aws-cdk/lib/index.js:362:189841)
    at AcceptorStateMachine.runTo (/workspaces/lambda-mvn/node_modules/aws-cdk/lib/index.js:362:154713)
    at /workspaces/lambda-mvn/node_modules/aws-cdk/lib/index.js:362:155043
    at Request.<anonymous> (/workspaces/lambda-mvn/node_modules/aws-cdk/lib/index.js:362:190133)
    at Request.<anonymous> (/workspaces/lambda-mvn/node_modules/aws-cdk/lib/index.js:362:196364)
    at Request.callListeners (/workspaces/lambda-mvn/node_modules/aws-cdk/lib/index.js:362:90251) {
  code: 'ValidationError',
  time: 2023-12-17T06:49:51.139Z,
  requestId: 'e82ab66c-c210-42b2-badd-c8a0b37e1dc5',
  statusCode: 400,
  retryable: false,
  retryDelay: 617.9338013557012
}

As part of the import operation, you cannot modify or add [RoleArn]
node ➜ /workspaces/lambda-mvn (main)

どうやら実行ロールのarnの組み立てができないようです。

パイプライン経由したスタックのため「PipelinesStack/DeployStage/LambdaMvnStack」で指定しています。

おそらくこれが原因でエラーになっているようです。

本来、cdk importによって生成されるロールはスタック名の「LambdaMvnStack」の要素だけ使う前提だと思うのですが、指定したスタック名にパイプラインのidも含まれていたため失敗しているのだと考えています。

そのため、appでインポート用のスタックを一時的に作成してインポートを試みます。

この時注意するべきポイントはパイプライン経由でデプロイされたスタックはstageのidを引き継ぐ仕様のため「DeployStage-LambdaMvnStack」のようなスタック名にする必要があります。

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { PipelinesStack } from '../lib/pipeline-stack';

const app = new cdk.App();
// パイプライン作成用のスタックはコメントアウト
// new PipelinesStack(app, 'PipelinesStack', {
//   env: {
//     account: "XXXXXX",
//     region: "ap-northeast-1",
//   },
// });
//本来はパイプラインのステージからデプロイされるスタックをbinのtsファイルに指定する
import { LambdaMvnStack } from '../lib/lambda-mvn-stack';
new LambdaMvnStack(app, 'DeployStage-LambdaMvnStack', {}) //パイプライン経由でデプロイされる想定のidを指定

この状態でcdk importを行いインポートし、あとはパイプラインからデプロイし直して問題なくデプロイできました。

また、パイプラインだとsynthした結果をdeployステージに渡すため、package.jsonのビルドスクリプトも以下のように修正します。

  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "build-lambda": "mvn -f lambda/pom.xml clean package",
    "synth": "npm run build-lambda && npx cdk synth",
    "cdk": "cdk"
  },

なお、パイプライン自体は以下のstackファイルで作成しています。

import { Stack, StackProps, Stage, StageProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import {
  aws_codecommit as codecommit,
  pipelines as pipelines,
} from "aws-cdk-lib";
import { LambdaMvnStack } from "./lambda-mvn-stack";

export class PipelinesStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const repo = new codecommit.Repository(this, `ccr-jlambdarepo`, {
      repositoryName: "jlambdarepo",
    });

    const LivePipeline = new pipelines.CodePipeline(this, "jlambdaPipeline", {
      pipelineName: "jlambdaPipeline",
      selfMutation: true,
      synth: new pipelines.CodeBuildStep("Synth", {
        input: pipelines.CodePipelineSource.codeCommit(repo, "main"),
        installCommands: ["npm ci"],
        commands: [                
          'npm run build',
          'npm run synth' 
      ],
      }),
    });

    const liveStage = LivePipeline.addStage(
      new AppStage(this, `DeployStage`, {
        env: {
          account: "XXXXXXXXXXX",
          region: "ap-northeast-1",
        },
      })
    );
  }
}

export class AppStage extends Stage {
  constructor(scope: Construct, id: string, props: StageProps) {
    super(scope, id, props);

    new LambdaMvnStack(this, "LambdaMvnStack", props);
  }
}

注意点

このパイプラインでの注意点としては、必ずセルフミューテーションをtrueにしてください。

このオプションは通常何も記載がなければtrueで設定されるのですが、敢えてselfMutation: true,を記載しています。

selfMutatioのいい評判を聞かないのですが、今回のような毎度ビルドし直してzipファイルを生成する構成だと逆にこの機能がないとパイプラインが動きません。

理由

パイプラインはビルドステージにて、毎度jarファイルを出力するため、CFn上のassets番号もビルドするたびに変化しています。

assetsステージは生成したzipファイルをS3にアップロードする役割を担っています。

上記のように毎度assetsの数値がランダムで変わってしまうと、「何かしらの方法」でassetsステージのアップロードするzipファイルの番号を書き換える必要があります。

その「何かしらの方法」というのが「selfMutation」というわけです。

なお、selfmutationを無効にしてデプロイした場合、CFn上で以下のエラーが出力されます。 assetsステージで実行されるCodeBuildのbuildspecにアップロードするassetsファイルが存在しない、あるいは初回デプロイ時のassets番号で毎回アップロードする挙動になってしまいます。

Resource handler returned message: "Error occurred while GetObject. S3 Error Code: NoSuchKey. S3 Error Message: The specified key does not exist. (Service: Lambda, Status Code: 400, Request ID: 96ce0fb7-e75a-46ee-a37a-7593ede5c2f4)" (RequestToken: 34a87ca3-83d7-5038-4090-8f8419a656e4, HandlerErrorCode: InvalidRequest)

なお、このassetsステージの挙動は以下の記事の「検証3: liveのみLambdaを更新してデプロイする」で詳細な挙動を記載しました。よければご一読ください。

CDKのpipelinesモジュールで無限ループした内容をまとめた記事 - サーバーワークスエンジニアブログ

まとめ

かなりあっさりしてますが、以上でCDK importの話は以上です。

今まで手動デプロイしたものをIaC管理にするのは結構きついと思っていたのですが 意外と簡単にできてしまったため、記事にしてみました。

なお、当然ですがCDK管理になってしまったLambdaに対してcdk destroyを実行するとLambdaごと削除されます。ご注意ください。