[AWS CDK] AWS CDK Intro Workshop for Java #reinvent

2018.12.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

今年のre:Inventで個人的にとても楽しかったセッションのデモがAWS CDKです。 すでにドキュメント AWS Cloud Development Kit - Docsが公開されていますが、まだDeveloper Preview版であるが故に以下の文言が強調して記述されています。

Do not use this version of the AWS CDK in production.

まだまだ破壊的変更が入るから、本番でまだ使うんじゃねえぞという開発側の強い意思を感じます。

個人的にはこれは 今後来ると思ういや来てくれるはずむしろ絶対来てほしい、という気持ちなので、是が非でも応援していきたい所存。お願い開発チーム!

CloudFormationはCloudのインフラリソースをjsonやyamlに記述し、コードで管理することができます。そのためInfrastracture as a Codeを体現しているサービスと言えます。しかし個人的に、 yamlやjsonをコードということに若干の違和感が・・・。プログラミング言語であるHTMLと言われているようななんとなくモヤモヤするようなものがありました。

しかし、AWS CDKのコンセプトは Infrastracture is Code です。紛れもない プログラミング言語 でインフラのリソースが記述、制御できます。素晴らしすぎる。CloudFormationをどうしても使いこなせなかった自分にインフラの制御・管理ができるようになるワンチャン到来です。

今回はAWS CDK Intro Workshop こちらを参考にして、AWS CDKの機能の一部を実際に利用してみます。

これを使えば、インフラ側のエンジニアの人たちとスムーズにやりとりを行うためのプログラマ側の良きツールとして活躍してくれるのでは、という淡い期待を胸にWorkshopを覗いてみましょう。

AWS CDKはCloudFormationテンプレートを生成する

AWS CDKは各種プログラミング言語でAWSのインフラリソースの構成を記述し、制御することができます。中間生成物としてCloudFormationテンプレートを出力するため、万が一ツールがオワコン化しても記述して生成したリソースはそのまま引き継ぐことが可能です。中間生成物としてCloudFormationテンプレートを出力することは、AWS CDKを知らない人でもCloudFormationテンプレートを読んで理解することができるという利点があります。

またAWS CDKをプログラマが利用するモチベーションとして、既存のプログラミング言語で記述ができるため、プログラミング言語の様々な制御構文や便利なライブラリを利用することが可能ということが大きいかと思います。CloudFormationテンプレートでの制御も可能ではありますが、プログラミング言語と比べるとどうしても可読性が落ちてしまうではないかというのが個人的な感想です。 *1

Workshopを試す

Workshopの内容はTypeScriptで構成されています。そのままWorkshopを試すのも良いですが、折角なので別の言語で試してみましょう。ということでJavaを選択します。大きな作業の流れは以下のとおりです。

  1. サンプルアプリケーションプロジェクトを作成する
  2. Tsサンプルアプリケーションと同じコードに変更する
  3. Compile & Deploy
  4. 不要なコードを削除
  5. Compile & Diffを実行して不要なResourceを削除をプレビュー
  6. Deployして不要なResourceを削除
  7. 必要なResourceをコードに追加
  8. Compile & DeployでResourceを作成
  9. Invoke

ではやっていきます。

執筆時点での環境

このツールは開発中のためすごい勢いでアップデートされています。現時点での自分の環境は以下のとおりです。

  • Apache Maven 3.6.0
  • cdk 0.18.1 (build 9f7af21)

Create Sample Application Project

AWS Intro Workshop > New Project > cdk init

手始めにプロジェクトの雛形を作成しましょう。language を指定するとともに、アプリケーションかライブラリを指定します。今回は実行可能な単独のアプリケーションとして構成したいので app を選びます。 *2

Workshopの手順に沿ってはじめにディレクトリを作成し、移動しておくことをおすすめします。

$ mkdir hello-java-cdk2
$ cd hello-java-cdk2/
$ cdk init app --language=java --app
Applying project template app for java
Initializing a new git repository...
Executing mvn package...

Welcome to your CDK Java project!

It is a Maven-based project, so you can open this directory with any Maven-compatible Java IDE,
and you should be able to build and run tests from your IDE.

You should explore the contents of this template. It demonstrates a CDK app with two instances of
a stack (`HelloStack`) which also uses a user-defined construct (`HelloConstruct`).

The `cdk.json` file tells the CDK Toolkit how to execute your app. It uses a script called `app.sh`
to do that. Note that this script expects a local file called `.classpath.txt` to exist. This file
is automatically created by `mvn package`.

# Useful commands

 * `mvn package`     compile and run tests
 * `cdk ls`          list all stacks in the app
 * `cdk synth`       emits the synthesized CloudFormation template
 * `cdk deploy`      deploy this stack to your default AWS account/region
 * `cdk diff`        compare deployed stack with current state
 * `cdk docs`        open CDK documentation

Enjoy!

mavenプロジェクトとしてJavaアプリケーションのサンプルアプリケーションが構成されます。

サンプルアプリケーションの簡単な概要の説明もありますね。それぞれの言語で微妙に異なるようで、Javaの場合 HelloStack というStackクラスが1つ(さらにそこに関連するユーザー独自のConstructクラスの HelloConstruct)。HelloStack クラスから生成されるインスタンスが2つあるようです。

First Compile

Javaの場合はソースを変更した際にはCompileが必要です。Compileをスキップした場合コードの変更が反映されないのでご注意ください。

mavenプロジェクトで構成されているためCompileにはmavenを利用します。

$ mvn compile
[INFO] Scanning for projects...
[INFO] 
[INFO] ---------------------< com.myorg:hello-java-cdk2 >----------------------
[INFO] Building hello-java-cdk2 0.1
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-dependency-plugin:2.8:build-classpath (build-classpath) @ hello-java-cdk2 ---
...

jarファイルを生成する package を実行するとCompileと同時にテストも実行されます。

プロジェクト構造

構造をざっと確認します。

$ tree src/main/
src/main/
└── java
    └── com
        └── myorg
            ├── HelloApp.java
            ├── HelloConstruct.java
            ├── HelloConstructProps.java
            └── HelloStack.java
  • HelloApp.java アプリケーションのエントリポイントです。mainメソッドが定義されています。 同じStackクラスを2つインスタンス化しています。
  • HelloConstruct.java S3 Bucketをfor文を使って生成するコードが記述されています。
  • HelloConstructProps.java 上記のHelloConstruct で利用するプロパティビルダーのコードです。
  • HelloStack.java Stackのコードになります。SNS, SQSのリソースの作成、HelloConstructのインスタンス化などが記述されています。

Stackのコードを作り直す

Javaのサンプルアプリケーションは、SNS, SQS以外にS3 Bucketをいくつか生成するコードになっています。そのため、このままDeployすると不要なS3 Bucketまで作成されてしまいます。TypeScriptのサンプルアプリケーションに合わせてコードを削ります。

生成するStackは一つだけ

public class HelloApp {
    public static void main(final String argv[]) {
        App app = new App();

        new HelloStack(app, "hello-cdk-1");
//        new HelloStack(app, "hello-cdk-2");

        app.run();
    }
}

同じStackクラスを2つ生成する必要がないので削除。

S3 Bucketは不要なので削除

S3 Bucketを生成しているのはHelloConstruct クラスです。こちらをインスタンス化している箇所をHelloStackから削除します。

public class HelloStack extends Stack {
    public HelloStack(final App parent, final String name) {
        this(parent, name, null);
    }

    public HelloStack(final App parent, final String name, final StackProps props) {
        super(parent, name, props);

        Queue queue = new Queue(this, "MyFirstQueue", QueueProps.builder()
                .withVisibilityTimeoutSec(300)
                .build());

        Topic topic = new Topic(this, "MyFirstTopic", TopicProps.builder()
                .withDisplayName("My First Topic Yeah")
                .build());

        topic.subscribeQueue(queue);

//        HelloConstruct hello = new HelloConstruct(this, "Buckets", HelloConstructProps.builder()
//                .withBucketCount(5)
//                .build());
//
//        User user = new User(this, "MyUser", UserProps.builder().build());
//        hello.grantRead(user);
    }
}

ひとまずこれで最小限まで削りました。この段階でCloudFormationテンプレートを出力してみます。

まずはCompileから。

$ mvn compile
[INFO] Scanning for projects...
[INFO] 
[INFO] ---------------------< com.myorg:hello-java-cdk2 >----------------------
[INFO] Building hello-java-cdk2 0.1
[INFO] --------------------------------[ jar ]---------------------------------
Downloading from central: https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/maven-metadata.xml
Downloaded from central: https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/maven-metadata.xml (461 B at 269 B/s)
[INFO] 
...

続いてテンプレートの出力。テンプレートは $ cdk synth で標準出力されます。

$ cdk synth --profile YOUR_PROFILE
Resources:
  MyFirstQueueXXXXXXXX:
    Type: AWS::SQS::Queue
    Properties:
      VisibilityTimeout: 300
    Metadata:
      aws:cdk:path: hello-cdk-1/MyFirstQueue/Resource
  MyFirstQueuePolicyXXXXXXXX:
    Type: AWS::SQS::QueuePolicy
    Properties:
      PolicyDocument:
        Statement:
          - Action: sqs:SendMessage
            Condition:
              ArnEquals:
                aws:SourceArn:
                  Ref: MyFirstTopicXXXXXXXX
            Effect: Allow
            Principal:
              Service: sns.amazonaws.com
            Resource:
              Fn::GetAtt:
                - MyFirstQueueXXXXXXXX
                - Arn
        Version: "2012-10-17"
      Queues:
        - Ref: MyFirstQueueXXXXXXXX
    Metadata:
      aws:cdk:path: hello-cdk-1/MyFirstQueue/Policy/Resource
  MyFirstTopicXXXXXXXX:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: My First Topic Yeah
    Metadata:
      aws:cdk:path: hello-cdk-1/MyFirstTopic/Resource
  MyFirstTopicMyFirstQueueSubscriptionXXXXXXXX:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint:
        Fn::GetAtt:
          - MyFirstQueueXXXXXXXX
          - Arn
      Protocol: sqs
      TopicArn:
        Ref: MyFirstTopicXXXXXXXX
    Metadata:
      aws:cdk:path: hello-cdk-1/MyFirstTopic/MyFirstQueueSubscription/Resource
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Modules: "@aws-cdk/assets=0.18.1,@aws-cdk/aws-autoscaling-api=0.18.1,@aws-cdk/aw\
        s-cloudwatch=0.18.1,@aws-cdk/aws-codepipeline-api=0.18.1,@aws-cdk/aws-e\
        c2=0.18.1,@aws-cdk/aws-events=0.18.1,@aws-cdk/aws-iam=0.18.1,@aws-cdk/a\
        ws-kms=0.18.1,@aws-cdk/aws-lambda=0.18.1,@aws-cdk/aws-logs=0.18.1,@aws-\
        cdk/aws-s3=0.18.1,@aws-cdk/aws-s3-notifications=0.18.1,@aws-cdk/aws-sns\
        =0.18.1,@aws-cdk/aws-sqs=0.18.1,@aws-cdk/aws-stepfunctions=0.18.1,@aws-\
        cdk/cdk=0.18.1,@aws-cdk/cx-api=0.18.1"

Profileの指定にも対応しているのでいつもどおり --profile で指定します。Switch Roleにも対応していますので特に問題ありません。

Diff

AWS環境へ生成・破棄・変更されるResourceの一覧を確認します。

$ cdk diff --profile YOUR_PROFILE
Resources
[+] AWS::SQS::Queue MyFirstQueue MyFirstQueueXXXXXXXX 
[+] AWS::SQS::QueuePolicy MyFirstQueue/Policy MyFirstQueuePolicyXXXXXXXX 
[+] AWS::SNS::Topic MyFirstTopic MyFirstTopicXXXXXXXX 
[+] AWS::SNS::Subscription MyFirstTopic/MyFirstQueueSubscription MyFirstTopicMyFirstQueueSubscriptionXXXXXXXX

Deploy

この状態でDeployを実行します。実行されるCloudFormationは先程標準出力でプレビューしたテンプレートがそのまま実行されます。

$ cdk deploy --profile YOUR_PROFILE
hello-cdk-1: deploying...
hello-cdk-1: creating CloudFormation changeset...
 0/6 | 11:26:31 PM | CREATE_IN_PROGRESS   | AWS::SNS::Topic        | MyFirstTopic (MyFirstTopicXXXXXXXX) 
 0/6 | 11:26:32 PM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata     | CDKMetadata 
 0/6 | 11:26:32 PM | CREATE_IN_PROGRESS   | AWS::SNS::Topic        | MyFirstTopic (MyFirstTopicXXXXXXXX) Resource creation Initiated
 0/6 | 11:26:32 PM | CREATE_IN_PROGRESS   | AWS::SQS::Queue        | MyFirstQueue (MyFirstQueueXXXXXXXX) 
 0/6 | 11:26:32 PM | CREATE_IN_PROGRESS   | AWS::SQS::Queue        | MyFirstQueue (MyFirstQueueXXXXXXXX) Resource creation Initiated
 1/6 | 11:26:33 PM | CREATE_COMPLETE      | AWS::SQS::Queue        | MyFirstQueue (MyFirstQueueXXXXXXXX) 
 1/6 | 11:26:34 PM | CREATE_IN_PROGRESS   | AWS::CDK::Metadata     | CDKMetadata Resource creation Initiated
 2/6 | 11:26:34 PM | CREATE_COMPLETE      | AWS::CDK::Metadata     | CDKMetadata 
 3/6 | 11:26:43 PM | CREATE_COMPLETE      | AWS::SNS::Topic        | MyFirstTopic (MyFirstTopicXXXXXXXX) 
 3/6 | 11:26:45 PM | CREATE_IN_PROGRESS   | AWS::SNS::Subscription | MyFirstTopic/MyFirstQueueSubscription (MyFirstTopicMyFirstQueueSubscriptionXXXXXXXX) 
 3/6 | 11:26:45 PM | CREATE_IN_PROGRESS   | AWS::SQS::QueuePolicy  | MyFirstQueue/Policy (MyFirstQueuePolicyXXXXXXXX) 
 3/6 | 11:26:46 PM | CREATE_IN_PROGRESS   | AWS::SQS::QueuePolicy  | MyFirstQueue/Policy (MyFirstQueuePolicyXXXXXXXX) Resource creation Initiated
 3/6 | 11:26:46 PM | CREATE_IN_PROGRESS   | AWS::SNS::Subscription | MyFirstTopic/MyFirstQueueSubscription (MyFirstTopicMyFirstQueueSubscriptionXXXXXXXX) Resource creation Initiated
 4/6 | 11:26:46 PM | CREATE_COMPLETE      | AWS::SQS::QueuePolicy  | MyFirstQueue/Policy (MyFirstQueuePolicyXXXXXXXX) 
 5/6 | 11:26:46 PM | CREATE_COMPLETE      | AWS::SNS::Subscription | MyFirstTopic/MyFirstQueueSubscription (MyFirstTopicMyFirstQueueSubscriptionXXXXXXXX) 
 6/6 | 11:26:48 PM | CREATE_COMPLETE      | AWS::CloudFormation::Stack | hello-cdk-1 

 ✅  hello-cdk-1

Stack ARN:
arn:aws:cloudformation:us-east-1:XXXXXXXXXXXXXXXX:stack/hello-cdk-1/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXXXX

CloudFormationで実行結果を確認すると、実行されたテンプレートが確認できます。

テストコードを削る

まずはじめにテストコードを削っておきます。Javaのmavenプロジェクトの場合テストコードを記述できるのがとても良いのですが、このあとの作業で少々面倒になるので一旦削ります(全方面土下座)

public class HelloStackTest {
    private final static ObjectMapper JSON =
        new ObjectMapper().configure(SerializationFeature.INDENT_OUTPUT, true);

    @Test
    public void testStack() throws IOException {
//        App app = new App();
//        HelloStack stack = new HelloStack(app, "test");
//
//        // synthesize the stack to a CloudFormation template and compare against
//        // a checked-in JSON file.
//        JsonNode actual = JSON.valueToTree(app.synthesizeStack(stack.getName()).getTemplate());
//        JsonNode expected = JSON.readTree(getClass().getResource("expected.cfn.json"));
//        assertEquals(expected, actual);
    }
}

テストコードは mvn package の際に実行されます。

このテストは生成されるCloudFormationが正しいかを実際のCloudFormationのJSONテンプレートに基づいてテストしているため、最終的な形になるまではメンテナンスがとてつもなく手間がかかります。最終的な形が決まってからテストを記述する他ないかと思います(現時点では)

ここまでで準備完了。

Hello CDK Workshop

では、コードを修正してResourceを変更していきましょう。目指すはAPI Gatewayを経由してLambda Functionを実行する構成です。先程までDeployしているアプリケーションとは全く異なるアプリケーションです。

hello-workshop

Remove Resource

Cleanup sample

ではまずは今まであったSNS TopicとSQSのQueueを消します。

public class HelloStack extends Stack {
    public HelloStack(final App parent, final String name) {
        this(parent, name, null);
    }

    public HelloStack(final App parent, final String name, final StackProps props) {
        super(parent, name, props);
    }
}

Compile & Diff

$ mvn compile
[INFO] Scanning for projects...
[INFO] 
[INFO] ---------------------< com.myorg:hello-java-cdk2 >----------------------
[INFO] Building hello-java-cdk2 0.1
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-dependency-plugin:2.8:build-classpath (build-classpath) @ hello-java-cdk2 ---
...
$ cdk diff --profile YOUR_PROFILE
Resources
[-] AWS::SQS::Queue MyFirstQueue MyFirstQueueXXXXXXXX destroy
[-] AWS::SQS::QueuePolicy MyFirstQueue/Policy MyFirstQueuePolicyXXXXXXXX destroy
[-] AWS::SNS::Topic MyFirstTopic MyFirstTopicXXXXXXXX destroy
[-] AWS::SNS::Subscription MyFirstTopic/MyFirstQueueSubscription MyFirstTopicMyFirstQueueSubscriptionXXXXXXXX destroy

SNS, SQSが削除対象です。

Deploy

$ cdk deploy --profile YOUR_PROFILE
hello-cdk-1: deploying...
hello-cdk-1: creating CloudFormation changeset...
 0/6 | 11:44:48 PM | UPDATE_IN_PROGRESS   | AWS::CDK::Metadata | CDKMetadata 
 1/6 | 11:44:50 PM | UPDATE_COMPLETE      | AWS::CDK::Metadata | CDKMetadata 
 1/6 | 11:44:52 PM | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack | hello-cdk-1 
 1/6 | 11:44:53 PM | DELETE_IN_PROGRESS   | AWS::SNS::Subscription | MyFirstTopicMyFirstQueueSubscriptionXXXXXXXX 
 1/6 | 11:44:53 PM | DELETE_IN_PROGRESS   | AWS::SQS::QueuePolicy | MyFirstQueuePolicyXXXXXXXX 
 2/6 | 11:44:54 PM | DELETE_COMPLETE      | AWS::SQS::QueuePolicy | MyFirstQueuePolicyXXXXXXXX 
 3/6 | 11:44:54 PM | DELETE_COMPLETE      | AWS::SNS::Subscription | MyFirstTopicMyFirstQueueSubscriptionXXXXXXXX 
 3/6 | 11:44:55 PM | DELETE_IN_PROGRESS   | AWS::SQS::Queue    | MyFirstQueueXXXXXXXX 
 3/6 | 11:44:55 PM | DELETE_IN_PROGRESS   | AWS::SNS::Topic    | MyFirstTopicXXXXXXXX 
 4/6 | 11:44:56 PM | DELETE_COMPLETE      | AWS::SNS::Topic    | MyFirstTopicXXXXXXXX 
4/6 Currently in progress: hello-cdk-1, MyFirstQueueXXXXXXXX

 ✅  hello-cdk-1

Stack ARN:
arn:aws:cloudformation:us-east-1:XXXXXXXXXXXXXXXX:stack/hello-cdk-1/XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXXXX

Deployを実行すると先程プレビューで表示していたリソースが差分として削除されているのがわかります。

Hello Lambda

Hello Lambda

Lambdaのリソースを追加します。手順を確認してみるとFunctionハンドラを別ファイルで用意し、Stack内で定義するLambda Functionリソースを生成する際に別ファイルで定義したFunctionハンドラを文字列で指定してるようです。

依存関係を追加

手順では $ npm install @aws-cdk/aws-lambda でライブラリのインストールを実行しています。JavaのMavenプロジェクトなので pom.xml に依存関係を追加します。<dependencies> のセクションを確認します。

<dependencies>
    <!-- AWS Cloud Development Kit -->
    <dependency>
        <groupId>software.amazon.awscdk</groupId>
        <artifactId>cdk</artifactId>
        <version>0.18.1</version>
    </dependency>

    <!-- Respective AWS Construct Libraries -->
    <dependency>
        <groupId>software.amazon.awscdk</groupId>
        <artifactId>iam</artifactId>
        <version>0.18.1</version>
    </dependency>
    <dependency>
        <groupId>software.amazon.awscdk</groupId>
        <artifactId>s3</artifactId>
        <version>0.18.1</version>
    </dependency>
    <dependency>
        <groupId>software.amazon.awscdk</groupId>
        <artifactId>sns</artifactId>
        <version>0.18.1</version>
    </dependency>
    <dependency>
        <groupId>software.amazon.awscdk</groupId>
        <artifactId>sqs</artifactId>
        <version>0.18.1</version>
    </dependency>
    ...
</dependencies>

Construct Libraryのセクションがありました。Lambdaを追加します。

<dependency>
    <groupId>software.amazon.awscdk</groupId>
    <artifactId>lambda</artifactId>
    <version>0.18.1</version>
</dependency>

これを追加。Import Changesで変更を適用。

実はこの手順は不要でLambdaのConstruct LibraryはSNSをインポートと同時に依存関係に含まれているためLibrary自体は存在します。そのため、実はこの手順自体はスキップしてしまってもLambda Functionのリソース定義は出来てしまいます(あとで気づいた)

Add Lambda Function

Lambda Functionのリソースを追加します。

まずはLambda Handlerのコードを作成します。JavaでLambdaを記述する際には RequestHandlerRequestStreamHandlerインタフェースを実装したクラスを作成します。

今回はAPI GatewayからJSONをパラメータとして入力されるようなので RequestStreamHandler を利用します。com.myorg パッケージ以下に lambda パッケージを追加し、その中に RequestStreamHandler をimplementsした Hello クラスを作成しましょう。

public class Hello implements RequestStreamHandler {

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
        String path = JsonPath.read(inputStream, "$.path");
        String body = String.format("Hello, CDK! You've hit %s \n", path);

        // https://aws.amazon.com/jp/blogs/compute/error-handling-patterns-in-amazon-api-gateway-and-aws-lambda/
        Map<String, Object> payload = new HashMap<>();
        payload.put("Content-type", "text/plain");
        payload.put("httpStatus", 200);
        payload.put("requestId", context.getAwsRequestId());
        payload.put("body", body);
        String message = new ObjectMapper().writeValueAsString(payload);

        // write response
        OutputStreamWriter writer = new OutputStreamWriter(outputStream, "UTF-8");
        writer.write(message);
        writer.flush();
        writer.close();
    }
}

こちら を参考にTutorialと同等のStatusCodeとContent-Type等を定義したレスポンスをOutputStreamに書き込みます。これでLambdaの方はOK。

続いてHelloStack クラスにLambdaリソースを定義するコードを追加します。

public class HelloStack extends Stack {
    public HelloStack(final App parent, final String name) {
        this(parent, name, null);
    }

    public HelloStack(final App parent, final String name, final StackProps props) {
        super(parent, name, props);

        FunctionProps functionProps = FunctionProps.builder()
                .withRuntime(Runtime.JAVA8)
                .withCode(Code.asset(System.getProperty("user.dir") + "/target/hello-java-cdk.zip"))
                .withHandler("com.myorg.lambda.Hello")
                .withFunctionName("hello-lambda")
                .withTimeout(10)
                .build();

        Function function = new Function(this, "HelloHandler", functionProps);
    }
}

FunctionProps が少々厄介だったので解説します。

メソッド名 役割
.withRuntime LambdaのRuntimeを指定。Javaは Runtime.JAVA8 を指定。
.withCode Lambdaへアップロードするzipファイルかもしくはzipしたいディレクトリ, AmazonS3のパスなどを指定する。directory, file 指定は deprecatedで利用できないのでそれらを利用したい場合は、 Code.asset を利用する。(後述)
.withHandler Handlerとなるクラスを指定。 Hello::requestHandler とメソッド名も指定できるが省略可能。
.withFunctionName 新たに追加するLambdaの名称を指定。
.withTimeout Timeoutの時間を秒で指定します。Bootstrapに時間がかかりそうなので一応10秒に延長しました。

全て指定が必須です。

.withCode() の指定について

.withCode の指定は注意が必要です。Code.asset(System.getProperty("user.dir") + "/target/hello-java-cdk.zip") について解説します。 Code.asset を使い、ローカルで生成される成果物を指定しそれをLambdaへのUpload対象物として扱います。今回は生成したzipファイル(これも一工夫必要)を指定していますが、他にディレクトリを指定することができます。

また Code.bucket を利用してS3バケットを指定でき、 Code.inline で4kB以下のコードならば文字列で指定できます。ケースによって使い分けます。

Dependency libraryをjarの中に全部含める

テンプレートのmavenプロジェクトではpom.xmlに追加した依存ライブラリがjarに含まれず、Lambdaの実行時に NoClassDefFoundError がスローされます。以下のPluginを追加

<!-- jar with dependency library -->
<plugin>
    <artifactId>maven-assembly-plugin</artifactId>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
    </configuration>
</plugin>

生成されるjarの名称が hello-java-cdk2-0.1-jar-with-dependencies.jar に変更されます。

mavenで生成物のjarファイルを変換する

上記のCode.asset で指定できるのは zipファイルディレクトリ のみです。つまりjarファイルは指定ができません。jarファイルを指定すると以下のようになります。

$ cdk deploy --profile YOUR_PROFILE
Exception in thread "main" software.amazon.jsii.JsiiException: Asset must be a .zip file or a directory (/XXXXXXXX/Develop/cdk/hello-java-cdk2/target/hello-java-cdk2-0.1-jar-with-dependencies.jar)
Error: Asset must be a .zip file or a directory 
...

さてどうしよう。

とボヤいていたら強者からアドヴァイスが(ありがたや

実際やるならこういうのを使ったほうが良さそう。非公開アカウントからリネームしろやとのこと。

とりあえず動かすために雑に対応。 *3renameプラグインを使いました。

<plugin>
    <groupId>com.coderplus.maven.plugins</groupId>
    <artifactId>copy-rename-maven-plugin</artifactId>
    <version>1.0</version>
    <executions>
        <execution>
            <id>copy-and-rename-file</id>
            <phase>package</phase>
            <goals>
                <goal>rename</goal>
            </goals>
            <configuration>
                <sourceFile>${project.build.directory}/hello-java-cdk2-0.1-jar-with-dependencies.jar</sourceFile>
                <destinationFile>${project.build.directory}/hello-java-cdk.zip</destinationFile>
            </configuration>
        </execution>
    </executions>
</plugin>

pom.xmlに追加。<phase> はjarが作成される package のタイミングを指定。<sourceFile> に生成されるjarファイル名を直値で指定。 <destinationFile> にzipにリネームしたファイル名を直値で指定。

直値で指定しているため、柔軟性にかけますがひとまずこれで。

Compile + package

変更を適用とLambdaへアップロードするZipファイルを生成ためPackageを実行します。Classファイルが更新されZipファイルが target ディレクトリ以下に生成されます。

$ mvn package
[INFO] Scanning for projects...
[INFO] 
[INFO] ---------------------< com.myorg:hello-java-cdk2 >----------------------
[INFO] Building hello-java-cdk2 0.1
[INFO] --------------------------------[ jar ]---------------------------------
Downloading from central: https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/maven-metadata.xml
Downloaded from central: https://repo.maven.apache.org/maven2/com/google/code/findbugs/jsr305/maven-metadata.xml (461 B at 494 B/s)
[INFO]

成功したら、diffを確認。

$ cdk diff --profile YOUR_PROFILE
Parameters
[+] Parameter HelloHandler/Code/S3Bucket HelloHandlerCodeS3BucketXXXXXXXX: {"Type":"String","Description":"S3 bucket for asset \"hello-cdk-1/HelloHandler/Code\""}
[+] Parameter HelloHandler/Code/S3VersionKey HelloHandlerCodeS3VersionKeyXXXXXXXX: {"Type":"String","Description":"S3 key for asset version \"hello-cdk-1/HelloHandler/Code\""}

Resources
[+] AWS::IAM::Role HelloHandler/ServiceRole HelloHandlerServiceRole11EF7C63 
[+] AWS::Lambda::Function HelloHandler HelloHandlerXXXXXXXX

Lambda FunctionとIAMのRoleが追加、さらにパラメータがいくつか追加されています。

Deploy

$ cdk deploy --profile YOUR_PROFILE
hello-cdk-1: deploying...

 ❌  hello-cdk-1 failed: Error: This stack uses assets, so the toolkit stack must be deployed to the environment (Run "cdk bootstrap xxxxxxxxxxxx/us-east-1")
This stack uses assets, so the toolkit stack must be deployed to the environment (Run "cdk bootstrap xxxxxxxxxxxx/us-east-1

あら失敗しました。 cdk bootstrap を実行せよとのこと。

$ cdk bootstrap xxxxxxxxxxxx/us-east-1 --profile YOUR_PROFILE
 ⏳  Bootstrapping environment xxxxxxxxxxxx/us-east-1...
CDKToolkit: creating CloudFormation changeset...
 0/2 | 1:00:32 AM | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket 
 0/2 | 1:00:32 AM | CREATE_IN_PROGRESS   | AWS::S3::Bucket | StagingBucket Resource creation Initiated
 1/2 | 1:00:53 AM | CREATE_COMPLETE      | AWS::S3::Bucket | StagingBucket 
 2/2 | 1:00:55 AM | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit 
 ✅  Environment xxxxxxxxxxxx/us-east-1 bootstrapped.

いくつかのS3 Bucketを作成している様子。LambdaにアップロードするAssetsバケットを作成しています。

再度Deployを実行します。

$ cdk deploy --profile YOUR_PROFILE
hello-cdk-1: deploying...
Updated:  (zip)
hello-cdk-1: creating CloudFormation changeset...
 0/4 | 1:03:40 AM | CREATE_IN_PROGRESS   | AWS::IAM::Role        | HelloHandler/ServiceRole (HelloHandlerServiceRolexxxxxxxx) 
 0/4 | 1:03:40 AM | CREATE_IN_PROGRESS   | AWS::IAM::Role        | HelloHandler/ServiceRole (HelloHandlerServiceRolexxxxxxxx) Resource creation Initiated
 0/4 | 1:03:41 AM | UPDATE_IN_PROGRESS   | AWS::CDK::Metadata    | CDKMetadata 
 1/4 | 1:03:43 AM | UPDATE_COMPLETE      | AWS::CDK::Metadata    | CDKMetadata 
 2/4 | 1:03:51 AM | CREATE_COMPLETE      | AWS::IAM::Role        | HelloHandler/ServiceRole (HelloHandlerServiceRolexxxxxxxx) 
 2/4 | 1:03:54 AM | CREATE_IN_PROGRESS   | AWS::Lambda::Function | HelloHandler (HelloHandlerxxxxxxxx) 
 2/4 | 1:03:55 AM | CREATE_IN_PROGRESS   | AWS::Lambda::Function | HelloHandler (HelloHandlerxxxxxxxx) Resource creation Initiated
 3/4 | 1:03:55 AM | CREATE_COMPLETE      | AWS::Lambda::Function | HelloHandler (HelloHandlerxxxxxxxx) 
 3/4 | 1:03:57 AM | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack | hello-cdk-1 
 4/4 | 1:03:58 AM | UPDATE_COMPLETE      | AWS::CloudFormation::Stack | hello-cdk-1 

 ✅  hello-cdk-1

Stack ARN:
arn:aws:cloudformation:us-east-1:xxxxxxxxxxxx:stack/hello-cdk-1/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx

Deployできました。AWSコンソールで確認してみます。

実行して確認

Lambdaを実行してみましょう。API Gatewayから呼び出される前提なのでTestのパラメータテンプレートからAmazon API Gateway AWS Proxy を選択し入力パラメータとしてセットします。

実行結果。

Conguraturation

API Gatewayのセットアップ

API Gateway

pom.xmlにいつものように依存関係を追加して・・・。

あれ?

Maven Repository - software.amazon.awscdk

mavenリポジトリにapigatewayが存在しませんでした。このセクションは現状Javaでできなさそう。ということで残念ながらここで終了!

まとめ

aws-cdkのJavaプロジェクトによるチュートリアルを実行してみました。TypeScriptに比べてかなりいろいろと準備する必要があって苦労しました。JavaでCDKを実行するためには

  1. pom.xml に依存関係を追加する
  2. Dependency LibraryをJar内に含めるために maven-assembly-plugin を追加する
  3. jarファイルのままだとDeployできないのでrenameするか自前でzipにするassemblyの設定を書くかが必要です。
  4. Assetを使うため $ cdk bootstrap を実行
  5. Deploy前にかならず package を実行
  6. package する際にはテストが必ず実行されるので変更が多い開発中は外しておいたほうが良さそう

といった注意事項が必要でした。思った以上にハマりポイントが多かった印象。

これらを鑑みて個人的には

  • Lambda書くならTypeScriptのほうがいい
  • AWS CDKいじるならTypeScriptのほうがいい

ということでAWS CDKはTypeScriptをやっていこうと思います!

参照

脚注

  1. CloudFormationテンプレート職人の皆さんにとっては超複雑な制御構文を組んでも全く問題ないかもしれませんが。
  2. Workshopの手順にある sample-app というコマンドは存在しないようです。app に読み替えます。Developers Preview版にはよくあること(多分)
  3. 良い子は真似をしてはいけません。