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

Create basic CDK support and minimalist demo stack #6

Merged
merged 18 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
aae5227
Add minimal EC2 instance to host Restate daemon
pcholakov Nov 14, 2023
7ec36bd
Add API Gateway for Lambda backend
pcholakov Nov 14, 2023
9b3d463
Use variable prefix from CDK context or env for stack name
pcholakov Nov 15, 2023
b589066
Add insecure internet-facing ALB exposing the Restate ingress endpoint
pcholakov Nov 15, 2023
c7ee2bd
Add custom resource that registers the Lambda handler via Restate dis…
pcholakov Nov 16, 2023
113c6fb
Fix: log discovery endpoint in custom resource handler
pcholakov Nov 16, 2023
44e7a25
Cleanup: remove some unnecessary outputs
pcholakov Nov 16, 2023
9d3bc7a
Discover the concrete AMI to use
pcholakov Nov 16, 2023
69262bf
Switch to Secrets Manager secret for passing the GitHub token into EC2
pcholakov Nov 16, 2023
1ada6ed
Send Restate JSON logs to CloudWatch logs
pcholakov Nov 17, 2023
b97a0bd
Refactoring: Extract custom Restate constructs for all major components
pcholakov Nov 19, 2023
2983cf7
Update README with deployment & test instructions
pcholakov Nov 20, 2023
642761d
Add esbuild dev dependency (avoids Docker requirement for bundling La…
pcholakov Nov 20, 2023
1da5f47
Minor cleanups and comment+TODO updates
pcholakov Nov 20, 2023
8831b6d
Allow Restate to invoke handlers via explicit version qualifier
pcholakov Nov 20, 2023
4d450f5
Ack warning - and actually silence it
pcholakov Nov 21, 2023
cf1b009
Switch to native Lambda invoke integration, remove API Gateway HTTP m…
pcholakov Nov 21, 2023
4496b93
Remove GitHub token secret as no longer needed
pcholakov Nov 21, 2023
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
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 });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit; can clean this up

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh, manage to miss this! Will clean up in the next PR.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if only we had some kind of orchestrator that did this for us ;)

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