Skip to content

Commit

Permalink
Add initial set of CDK constructs and Restate demo stack (#6)
Browse files Browse the repository at this point in the history
* Add minimal EC2 instance to host Restate daemon

* Add API Gateway for Lambda backend

* Use variable prefix from CDK context or env for stack name

* Add insecure internet-facing ALB exposing the Restate ingress endpoint

* Add custom resource that registers the Lambda handler via Restate discovery after deployment

* Fix: log discovery endpoint in custom resource handler

* Cleanup: remove some unnecessary outputs

* Discover the concrete AMI to use

* Switch to Secrets Manager secret for passing the GitHub token into EC2

* Send Restate JSON logs to CloudWatch logs

* Refactoring: Extract custom Restate constructs for all major components

* Update README with deployment & test instructions

* Add esbuild dev dependency (avoids Docker requirement for bundling Lambda packages)

* Minor cleanups and comment+TODO updates

* Allow Restate to invoke handlers via explicit version qualifier

* Ack warning - and actually silence it

* Switch to native Lambda invoke integration, remove API Gateway HTTP mapping

* Remove GitHub token secret as no longer needed
  • Loading branch information
pcholakov authored Nov 22, 2023
1 parent 121e4db commit 4750047
Show file tree
Hide file tree
Showing 13 changed files with 5,390 additions and 46 deletions.
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
.idea
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
cdk.out
7 changes: 7 additions & 0 deletions .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
}
46 changes: 36 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,40 @@
# Welcome to your CDK TypeScript project
# Restate CDK support

This is a blank project for CDK development with TypeScript.
CDK construct library for deploying standalone [Restate](https://restate.dev) and Restate service handlers to AWS.

The `cdk.json` file tells the CDK Toolkit how to execute your app.
## Deploying the demo project

## Useful commands
To deploy the self-hosted Restate instance, run:

* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk synth` emits the synthesized CloudFormation template
```shell
npx cdk deploy
```

By default, this will create a stack prefixed with the value of `$USER` - you can override this using the CDK context
parameter `prefix` like this:

```shell
npx cdk deploy --context prefix="dev"
```

You will be prompted to confirm the creation of new security-sensitive resources.

Use the value of the `RestateIngressEndpoint` output of the CloudFormation stack to communicate with the Restate
broker (if you customized the deployment prefix above, you will need to update the stack name):

```shell
export RESTATE_INGRESS_ENDPOINT=$(aws cloudformation describe-stacks --stack-name ${USER}-RestateStack \
--query "Stacks[0].Outputs[?OutputKey=='RestateIngressEndpoint'].OutputValue" --output text)
```

```shell
curl -X POST -w \\n ${RESTATE_INGRESS_ENDPOINT}/Greeter/greet -H 'content-type: application/json' -d '{"key": "Restate"}'
```

You will get output similar to the following:

```json
{
"response": "Hello, Restate! :-)"
}
```
22 changes: 7 additions & 15 deletions bin/restate-cdk-support.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { RestateCdkSupportStack } from '../lib/restate-cdk-support-stack';
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { RestateSelfHostedStack } from "../lib/restate-self-hosted-stack";

const app = new cdk.App();
new RestateCdkSupportStack(app, 'RestateCdkSupportStack', {
/* If you don't specify 'env', this stack will be environment-agnostic.
* Account/Region-dependent features and context lookups will not work,
* but a single synthesized template can be deployed anywhere. */

/* Uncomment the next line to specialize this stack for the AWS Account
* and Region that are implied by the current CLI configuration. */
// env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
const githubPat = app.node.tryGetContext("githubTokenSecretName");
const prefix = app.node.tryGetContext("prefix") ?? process.env["USER"];

/* Uncomment the next line if you know exactly what Account and Region you
* want to deploy the stack to. */
// env: { account: '123456789012', region: 'us-east-1' },

/* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
new RestateSelfHostedStack(app, [prefix, "RestateStack"].filter(Boolean).join("-"), {
githubTokenSecretName: githubPat,
});
16 changes: 0 additions & 16 deletions lib/restate-cdk-support-stack.ts

This file was deleted.

76 changes: 76 additions & 0 deletions lib/restate-constructs/lambda/register-service-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { CloudFormationCustomResourceResponse } from "aws-lambda";
import { Handler } from "aws-lambda/handler";
import { CloudFormationCustomResourceEvent } from "aws-lambda/trigger/cloudformation-custom-resource";
import fetch from "node-fetch";

export interface RegistrationProperties {
metaEndpoint?: string;
serviceEndpoint?: string;
serviceLambdaArn?: string;
}

export const handler: Handler<CloudFormationCustomResourceEvent, Partial<CloudFormationCustomResourceResponse>> =
async function(event) {
console.log({ event });

if (event.RequestType === "Delete") {
return {
// TODO: deregister service on delete (https://github.com/restatedev/restate-cdk-support/issues/5)
Reason: "No-op",
Status: "SUCCESS",
} satisfies Partial<CloudFormationCustomResourceResponse>;
}

try {
const props = event.ResourceProperties as RegistrationProperties;

const controller = new AbortController();
const healthCheckTimeout = setTimeout(() => controller.abort(), 3000);
const healthCheckUrl = `${props.metaEndpoint}/health`;
console.log(`Performing health check against: ${healthCheckUrl}`);
const healthResponse = await fetch(healthCheckUrl,
{
signal: controller.signal,
})
.finally(() => clearTimeout(healthCheckTimeout));
console.log(`Got health response back: ${await healthResponse.text()}`);

if (!(healthResponse.status >= 200 && healthResponse.status < 300)) {
// TODO: retry until service is healthy, or some overall timeout is reached
throw new Error(`Health check failed: ${healthResponse.statusText} (${healthResponse.status})`);
}

const registerCallTimeout = setTimeout(() => controller.abort(), 3000);
const discoveryEndpointUrl = `${props.metaEndpoint}/endpoints`;
// const registrationRequest = JSON.stringify({ uri: props.serviceEndpoint });
const registrationRequest = JSON.stringify({ arn: props.serviceLambdaArn });
console.log(`Triggering registration at ${discoveryEndpointUrl}: ${registrationRequest}`);
const discoveryResponse = await fetch(discoveryEndpointUrl,
{
signal: controller.signal,
method: "POST",
body: registrationRequest,
headers: {
"Content-Type": "application/json",
},
})
.finally(() => clearTimeout(registerCallTimeout));
console.log(`Got registration response back: ${discoveryResponse.status}`);

if (!(healthResponse.status >= 200 && healthResponse.status < 300)) {
// TODO: retry until successful, or some overall timeout is reached
throw new Error(`Service registration failed: ${healthResponse.statusText} (${healthResponse.status})`);
}

console.log("Returning success.");
} catch (err) {
console.error("Ignoring unhandled error: " + err);
}

return {
Data: {
// it would be neat if we could return a unique Restate event id back to CloudFormation to close the loop
},
Status: "SUCCESS",
} satisfies Partial<CloudFormationCustomResourceResponse>;
};
72 changes: 72 additions & 0 deletions lib/restate-constructs/restate-lambda-services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { SingleNodeRestateInstance } from "./single-node-restate-instance";
import * as cdk from "aws-cdk-lib";
import { RegistrationProperties } from "./lambda/register-service-handler";

/**
* A Restate RPC service path. Example: `greeter`.
*/
type RestatePath = string;

/**
* A collection of Lambda Restate RPC Service handlers.
*/
export type LambdaServiceCollectionProps = {
/**
* Mappings from service path to Lambda handler.
*/
serviceHandlers: Record<RestatePath, lambda.Function>;
}

/**
* Creates a Restate service deployment backed by a single EC2 instance,
* suitable for development and testing purposes.
*/
export class RestateLambdaServiceCollection extends Construct {
private readonly serviceHandlers: Record<RestatePath, lambda.Function>;

constructor(scope: Construct, id: string, props: LambdaServiceCollectionProps) {
super(scope, id);
this.serviceHandlers = props.serviceHandlers;
}

public register(restate: SingleNodeRestateInstance) {
for (const [path, handler] of Object.entries(this.serviceHandlers)) {
this.registerHandler(restate, { path, handler });
}
}

private registerHandler(restate: SingleNodeRestateInstance, service: {
path: RestatePath,
handler: lambda.Function
}) {
cdk.Annotations.of(service.handler).acknowledgeWarning("@aws-cdk/aws-lambda:addPermissionsToVersionOrAlias",
"We specifically want to grant invoke permissions on all handler versions, " +
"not just the currently deployed one, as there may be suspended invocations against older versions");
service.handler.currentVersion.grantInvoke(restate.instanceRole); // CDK ack doesn't work, silence above warning
service.handler.grantInvoke(restate.instanceRole); // Grants access to all handler versions for ongoing invocations

new RestateServiceRegistrar(this, service.handler.node.id + "Discovery", { restate, service });
}
}

class RestateServiceRegistrar extends Construct {
constructor(scope: Construct, id: string, props: {
restate: SingleNodeRestateInstance, service: {
path: RestatePath,
handler: lambda.Function
}
}) {
super(scope, id);

new cdk.CustomResource(this, props.service.handler.node.id + "Discovery", {
serviceToken: props.restate.serviceDiscoveryProvider.serviceToken,
resourceType: "Custom::RestateServiceRegistrar",
properties: {
metaEndpoint: props.restate.metaEndpoint,
serviceLambdaArn: props.service.handler.currentVersion.functionArn,
} satisfies RegistrationProperties,
});
}
}
Loading

0 comments on commit 4750047

Please sign in to comment.