Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Kotlin Lambda (CDK) example #50

Merged
merged 11 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
pcholakov marked this conversation as resolved.
Show resolved Hide resolved
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
Loading