Skip to content

Commit

Permalink
Add Kotlin Lambda (CDK) example (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcholakov authored Dec 14, 2023
1 parent e33067d commit 3503ed3
Show file tree
Hide file tree
Showing 24 changed files with 6,950 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .github/workflows/pre-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ jobs:
arguments: check
build-root-directory: kotlin/hello-world-lambda

# CDK projects are a hybrid of TypeScript CDK stack + platform-specific handler code; the top-level npm CDK build
# is responsible for verifying the language-specific handler cod. They are also not a part of the TypeScript
# examples workspace, so we test them separately here.
- name: Test kotlin/hello-world-lambda-cdk
if: github.event.inputs.sdkTypescriptVersion != '' && github.event.inputs.sdkJavaVersion != ''
run: npm --prefix kotlin/hello-world-lambda-cdk run verify

- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,5 @@ jobs:
java-hello-world-http.zip
java-hello-world-lambda.zip
kotlin-hello-world-http.zip
kotlin-hello-world-lambda.zip
kotlin-hello-world-lambda.zip
kotlin-hello-world-lambda-cdk.zip
8 changes: 8 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ jobs:
with:
arguments: check
build-root-directory: kotlin/hello-world-lambda

# CDK projects are a hybrid of TypeScript CDK stack + platform-specific handler code; the top-level npm CDK build
# is responsible for verifying the language-specific handler cod. They are also not a part of the TypeScript
# examples workspace, so we test them separately here.
- name: Test kotlin/hello-world-lambda-cdk
if: github.event.inputs.sdkTypescriptVersion != '' && github.event.inputs.sdkJavaVersion != ''
run: npm --prefix kotlin/hello-world-lambda-cdk run verify

build-ts:
# prevent from running on forks
if: github.repository_owner == 'restatedev'
Expand Down
3 changes: 3 additions & 0 deletions kotlin/hello-world-lambda-cdk/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
cdk.out
15 changes: 15 additions & 0 deletions kotlin/hello-world-lambda-cdk/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"env": {
"browser": true,
"commonjs": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"overrides": [],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest"
},
"plugins": ["@typescript-eslint"],
"rules": {}
}
10 changes: 10 additions & 0 deletions kotlin/hello-world-lambda-cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
!jest.config.js
node_modules
.cdk.staging
cdk.out
.gradle
lambda/build
*.js
*.d.ts
*.class
cdk.context.json
7 changes: 7 additions & 0 deletions kotlin/hello-world-lambda-cdk/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"arrowParens": "always",
"printWidth": 120
}
77 changes: 77 additions & 0 deletions kotlin/hello-world-lambda-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Hello world - Kotlin Lambda (CDK) example

Sample project deploying a Kotlin-based Restate service to AWS Lambda using the AWS Cloud Development Kit (CDK). This
is functionally equivalent to the [`hello-world-lambda`](../hello-world-lambda) example but uses CDK to automate the deployment of the
Lambda function to AWS, and to automate handler registration with a Restate.

For more information on CDK, please see [Getting started with the AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html).

* [CDK app entry point `lambda-jvm-cdk.ts`](bin/lambda-jvm-cdk.ts)
* [CDK stack consisting of a Lambda function and providing Restate service registration](cdk/lambda-jvm-cdk-stack.ts)
* [Kotlin Lambda handler](lambda) - based on [`hello-world-lambda`](../hello-world-lambda)

## Download the example

```shell
wget https://github.com/restatedev/examples/releases/latest/download/kotlin-hello-world-lambda-cdk.zip && unzip kotlin-hello-world-lambda-cdk.zip -d kotlin-hello-world-lambda-cdk && rm kotlin-hello-world-lambda-cdk.zip
```

## Deploy

**Pre-requisites:**

* npm
* gradle
* JDK >= 11
* Restate Cloud access (cluster id + API token)
* AWS account, bootstrapped for CDK use

Create a secret in Secrets Manager to hold the authentication token. The secret name is up to you -- we suggest
using `/restate/` and an appropriate prefix to avoid confusion:

```shell
export AUTH_TOKEN_ARN=$(aws secretsmanager create-secret \
--name /restate/${CLUSTER_ID}/auth-token --secret-string ${RESTATE_AUTH_TOKEN} \
--query ARN --output text
)
```

Once you have the ARN for the auth token secret, you can deploy the stack using:

```shell
npx cdk deploy \
--context clusterId=${CLUSTER_ID} \
--context authTokenSecretArn=${AUTH_TOKEN_ARN}
```

Alternatively, you can save this information in the `cdk.context.json` file:

```json
{
"clusterId": "...",
"authTokenSecretArn": "arn:aws:secretsmanager:us-east-1:123456789012:secret:/restate/.../auth-token-abc123"
}
```

In that case, you can simply run:

```shell
npm run deploy
```

In this example, the Lambda handler function name is dynamically generated by CDK. You can see what it is from the `HandlerFunction`
output of the CDK stack after a successful deployment.

### Test

You can send a test request to the Restate cluster ingress endpoint to call the newly deployed service:

```shell
curl --json '{}' -H "Authorization: Bearer ${RESTATE_API_TOKEN}" \
https://${CLUSTER_ID}.dev.restate.cloud:8080/greeter.Greeter/Greet
```

### Useful commands

* `npm run build` compile the Lambda handler and synthesize CDK deployment artifacts
* `npm run deploy` perform a CDK deployment
10 changes: 10 additions & 0 deletions kotlin/hello-world-lambda-cdk/bin/lambda-jvm-cdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { LambdaJvmCdkStack } from "../cdk/lambda-jvm-cdk-stack";

const app = new cdk.App();
new LambdaJvmCdkStack(app, "LambdaJvmCdkStack", {
clusterId: app.node.getContext("clusterId"),
authTokenSecretArn: app.node.getContext("authTokenSecretArn"),
});
59 changes: 59 additions & 0 deletions kotlin/hello-world-lambda-cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{
"app": "npx ts-node --prefer-ts-exts bin/lambda-jvm-cdk.ts",
"watch": {
"include": ["**"],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true
}
}
56 changes: 56 additions & 0 deletions kotlin/hello-world-lambda-cdk/cdk/lambda-jvm-cdk-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as restate from "@restatedev/restate-cdk";
import { Construct } from "constructs";

export class LambdaJvmCdkStack extends cdk.Stack {
constructor(
scope: Construct,
id: string,
props: {
clusterId: string;
authTokenSecretArn: string;
} & cdk.StackProps,
) {
super(scope, id, props);

const greeter: lambda.Function = new lambda.Function(this, "RestateKotlin", {
runtime: lambda.Runtime.JAVA_21,
architecture: lambda.Architecture.ARM_64,
code: lambda.Code.fromAsset("lambda/build/libs/lambda-all.jar"),
handler: "dev.restate.sdk.examples.LambdaHandler",
timeout: cdk.Duration.seconds(10),
logFormat: lambda.LogFormat.JSON,
applicationLogLevel: "DEBUG",
systemLogLevel: "DEBUG",
});

const restateInstance = new restate.RestateCloudEndpoint(this, "RestateCloud", {
clusterId: props.clusterId,
authTokenSecretArn: props.authTokenSecretArn,
});

// Alternatively, you can deploy Restate on your own infrastructure like this. See the Restate CDK docs for more.
// const restateInstance = new restate.SingleNodeRestateInstance(this, "Restate", {
// logGroup: new logs.LogGroup(this, "RestateLogs", {
// retention: logs.RetentionDays.THREE_MONTHS,
// }),
// });

const handlers = new restate.LambdaServiceRegistry(this, "RestateServices", {
serviceHandlers: {
"greeter.Greeter": greeter,
},
restate: restateInstance,
});
handlers.register({
metaEndpoint: restateInstance.metaEndpoint,
invokerRoleArn: restateInstance.invokerRole.roleArn,
authTokenSecretArn: restateInstance.authToken.secretArn,
});

new cdk.CfnOutput(this, "HandlerFunction", {
value: greeter.functionName,
});
}
}
91 changes: 91 additions & 0 deletions kotlin/hello-world-lambda-cdk/lambda/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
import com.google.protobuf.gradle.id

val restateVersion = "0.6.0"

plugins {
kotlin("jvm") version "1.9.10"

id("com.google.protobuf") version "0.9.1"

// To package the dependency for Lambda
id("com.github.johnrengelman.shadow") version "8.1.1"
}

repositories {
mavenCentral()
}

dependencies {
// Restate SDK
implementation("dev.restate:sdk-api-kotlin:$restateVersion")
implementation("dev.restate:sdk-lambda:$restateVersion")
// To use Jackson to read/write state entries (optional)
implementation("dev.restate:sdk-serde-jackson:$restateVersion")

// Protobuf and grpc dependencies (we need the Java dependencies as well because the Kotlin dependencies rely on Java)
implementation("com.google.protobuf:protobuf-java:3.24.3")
implementation("com.google.protobuf:protobuf-kotlin:3.24.3")
implementation("io.grpc:grpc-stub:1.58.0")
implementation("io.grpc:grpc-protobuf:1.58.0")
implementation("io.grpc:grpc-kotlin-stub:1.4.0") { exclude("javax.annotation", "javax.annotation-api") }
// This is needed to compile the @Generated annotation forced by the grpc compiler
// See https://github.com/grpc/grpc-java/issues/9153
compileOnly("org.apache.tomcat:annotations-api:6.0.53")

// To specify the coroutines dispatcher
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")

// AWS Lambda-specific logging, see https://docs.aws.amazon.com/lambda/latest/dg/java-logging.html#java-logging-log4j2
val log4j2version = "2.22.0"
implementation("org.apache.logging.log4j:log4j-core:$log4j2version")
implementation("org.apache.logging.log4j:log4j-layout-template-json:$log4j2version")
implementation("com.amazonaws:aws-lambda-java-log4j2:1.6.0")

// Testing (optional)
testImplementation("org.junit.jupiter:junit-jupiter:5.9.1")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
testImplementation("dev.restate:sdk-testing:$restateVersion")
}

// Setup Java/Kotlin compiler target
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}

// Configure protoc plugin
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.24.3" }

plugins {
id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.58.0" }
id("grpckt") { artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.0:jdk8@jar" }
}

generateProtoTasks {
all().forEach {
// We need both java and kotlin codegen(s) because the kotlin protobuf/grpc codegen depends on the java ones
it.plugins {
id("grpc")
id("grpckt")
}
it.builtins {
java {}
id("kotlin")
}
}
}
}

// Configure shadowJar plugin to properly transform Log4j plugin configurations - needed for AWS Lambda logger
tasks.withType<ShadowJar> {
transform(Log4j2PluginsCacheFileTransformer::class.java)
}

// Configure test platform
tasks.withType<Test> {
useJUnitPlatform()
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading

0 comments on commit 3503ed3

Please sign in to comment.