Skip to content

Commit

Permalink
Update Kotlin-on-Lambda with CDK template for 0.9 (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
pcholakov authored and slinkydeveloper committed Apr 25, 2024
1 parent 0b34d77 commit fac3a11
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 176 deletions.
1 change: 1 addition & 0 deletions .tools/run_jvm_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ PROJECT_ROOT="$(dirname "$SELF_PATH")/.."
pushd $PROJECT_ROOT/templates/java-gradle && ./gradlew check && popd || exit
pushd $PROJECT_ROOT/templates/java-maven && mvn verify && popd || exit
pushd $PROJECT_ROOT/templates/kotlin-gradle && ./gradlew check && popd || exit
pushd $PROJECT_ROOT/templates/kotlin-gradle-lambda-cdk/lambda && ./gradlew check && popd || exit

pushd $PROJECT_ROOT/patterns-use-cases/sagas/sagas-java && ./gradlew check && popd || exit

Expand Down
1 change: 1 addition & 0 deletions .tools/update_jvm_examples.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function search_and_replace_version_maven() {
search_and_replace_version_gradle $PROJECT_ROOT/templates/java-gradle
search_and_replace_version_maven $PROJECT_ROOT/templates/java-maven
search_and_replace_version_gradle $PROJECT_ROOT/templates/kotlin-gradle
search_and_replace_version_gradle $PROJECT_ROOT/templates/kotlin-gradle-lambda-cdk

search_and_replace_version_gradle $PROJECT_ROOT/patterns-use-cases/sagas/sagas-java

Expand Down
50 changes: 15 additions & 35 deletions templates/kotlin-gradle-lambda-cdk/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 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.
Sample project deploying a Kotlin-based Restate service to AWS Lambda using the AWS Cloud Development Kit (CDK).
The stack uses the Restate CDK constructs library to create an EC2-hosted Restate server in AWS, and to register the
service with this Restate environment.

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

Expand Down Expand Up @@ -34,56 +34,36 @@ For more information on CDK, please see [Getting started with the AWS CDK](https

* npm
* gradle
* JDK >= 11
* JDK >= 21
* Restate Cloud access (cluster id + API token)
* AWS account, bootstrapped for CDK use
* valid AWS credentials with sufficient privileges to create the necessary resources

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:
Install npm dependencies:

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

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:
To deploy the stack, 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.
The stack output will print out the Restate server ingress URL.

### Test

You can send a test request to the Restate cluster ingress endpoint to call the newly deployed service:
You can send a test request to the Restate 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
curl -k ${restateIngressUrl}/Greeter/greet \
-H 'content-type: application/json' -d '"Restate"'
```

### Useful commands

* `npm run build` compile the Lambda handler and synthesize CDK deployment artifacts
* `npm run deploy` perform a CDK deployment
* `npm run build` compile the Lambda handler and synthesize CDK deployment artifacts
* `npm run deploy` perform a CDK deployment
* `npm run destroy` delete the stack and all its resources
6 changes: 4 additions & 2 deletions templates/kotlin-gradle-lambda-cdk/bin/lambda-jvm-cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ 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"),
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
});
62 changes: 42 additions & 20 deletions templates/kotlin-gradle-lambda-cdk/cdk/lambda-jvm-cdk-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,49 +10,71 @@
*/

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 logs from "aws-cdk-lib/aws-logs";
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,
props: 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"),
code: lambda.Code.fromAsset("lambda/build/distributions/lambda.zip"),
handler: "dev.restate.sdk.examples.LambdaHandler",
timeout: cdk.Duration.seconds(10),
logFormat: lambda.LogFormat.JSON,
applicationLogLevel: "DEBUG",
systemLogLevel: "DEBUG",
});

const environment = new restate.RestateCloudEnvironment(this, "RestateCloud", {
clusterId: props.clusterId,
authTokenSecretArn: props.authTokenSecretArn,
const environment = new restate.SingleNodeRestateDeployment(this, "Restate", {
// restateImage: "docker.io/restatedev/restate",
// restateTag: "0.9",
logGroup: new logs.LogGroup(this, "RestateLogs", {
retention: logs.RetentionDays.ONE_MONTH,
removalPolicy: cdk.RemovalPolicy.DESTROY,
}),
});

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

new restate.LambdaServiceRegistry(this, "RestateServices", {
handlers: {
"greeter.Greeter": greeter,
},
environment,
new iam.Policy(this, "AssumeAnyRolePolicy", {
statements: [
new iam.PolicyStatement({
sid: "AllowAssumeAnyRole",
actions: ["sts:AssumeRole"],
resources: ["*"], // we don't know upfront what invoker roles we may be asked to assume at runtime
}),
],
}).attachToRole(environment.invokerRole);

const invokerRole = new iam.Role(this, "InvokerRole", {
assumedBy: new iam.ArnPrincipal(environment.invokerRole.roleArn),
});
invokerRole.grantAssumeRole(environment.invokerRole);

const restateEnvironment = restate.RestateEnvironment.fromAttributes({
adminUrl: environment.adminUrl,
invokerRole,
});

const deployer = new restate.ServiceDeployer(this, "ServiceDeployer", {
logGroup: new logs.LogGroup(this, "Deployer", {
retention: logs.RetentionDays.ONE_MONTH,
removalPolicy: cdk.RemovalPolicy.DESTROY,
}),
});

deployer.deployService("Greeter", greeter.currentVersion, restateEnvironment, {
insecure: true, // self-signed certificate
});

new cdk.CfnOutput(this, "restateIngressUrl", { value: environment.ingressUrl });
}
}
85 changes: 25 additions & 60 deletions templates/kotlin-gradle-lambda-cdk/lambda/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,93 +1,58 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer
import com.google.protobuf.gradle.id

val restateVersion = "0.8.0"
import java.net.URI

plugins {
kotlin("jvm") version "1.9.10"
kotlin("jvm") version "1.9.22"
// Kotlinx serialization (optional)
kotlin("plugin.serialization") version "1.9.22"

id("com.google.protobuf") version "0.9.1"
id("com.google.devtools.ksp") version "1.9.22-1.0.18"

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

repositories {
maven {
url = URI.create("https://s01.oss.sonatype.org/content/repositories/snapshots/")
}
mavenCentral()
}

val restateVersion = "0.9.0-SNAPSHOT"

dependencies {
// Annotation processor
ksp("dev.restate:sdk-api-kotlin-gen:$restateVersion")

// 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")
// Kotlinx serialization (optional)
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")

// AWS Lambda-specific logging, see https://docs.aws.amazon.com/lambda/latest/dg/java-logging.html#java-logging-log4j2
val log4j2version = "2.22.0"
val log4j2version = "2.23.1"
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))
languageVersion.set(JavaLanguageVersion.of(21))
}
}

// 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" }
}
tasks.register<Zip>("lambdaZip") {
from(tasks.compileKotlin.get())
from(tasks.processResources.get())

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")
}
}
into("lib") {
from(configurations.runtimeClasspath)
}
}

// Configure shadowJar plugin to properly merge SPI files and Log4j plugin configurations
tasks.withType<ShadowJar> {
transform(Log4j2PluginsCacheFileTransformer::class.java)
transform(ServiceFileTransformer::class.java)
}

// Configure test platform
tasks.withType<Test> {
useJUnitPlatform()
tasks.build {
dependsOn("lambdaZip")
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,21 @@
* https://github.com/restatedev/examples/
*/

package dev.restate.sdk.examples
package dev.restate.sdk.examples

import dev.restate.sdk.common.Component
import dev.restate.sdk.common.CoreSerdes
import dev.restate.sdk.common.StateKey
import dev.restate.sdk.examples.generated.*
import dev.restate.sdk.examples.generated.GreeterProto.GreetRequest
import dev.restate.sdk.examples.generated.GreeterProto.GreetResponse
import dev.restate.sdk.kotlin.ObjectContext
import kotlinx.coroutines.Dispatchers
import dev.restate.sdk.annotation.Handler
import dev.restate.sdk.annotation.Service
import dev.restate.sdk.kotlin.Context

class Greeter :
// Use Dispatchers.Unconfined as the Executor/thread pool is managed by the SDK itself.
GreeterGrpcKt.GreeterCoroutineImplBase(Dispatchers.Unconfined),
Component {

companion object {
private val COUNT = StateKey.of("count", CoreSerdes.JSON_INT)
}

override suspend fun greet(request: GreetRequest): GreetResponse {
val ctx = ObjectContext.current()

val count = ctx.get(COUNT) ?: 1
ctx.set(COUNT, count + 1)
/**
* Template of a Restate service and handler
* Have a look at the Kotlin QuickStart to learn how to run this: https://docs.restate.dev/get_started/quickstart?sdk=kotlin
*/
@Service
class Greeter {

return greetResponse {
message = "Hello ${request.name} for the $count time!"
}
@Handler
suspend fun greet(ctx: Context, name: String): String {
return "Hello, ${name ?: "Restate"}!"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ import dev.restate.sdk.lambda.RestateLambdaEndpointBuilder

class LambdaHandler : BaseRestateLambdaHandler() {
override fun register(builder: RestateLambdaEndpointBuilder) {
builder.withService(Greeter())
builder.bind(Greeter())
}
}
Loading

0 comments on commit fac3a11

Please sign in to comment.