-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add initial set of CDK constructs and Restate demo stack (#6)
* 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
Showing
13 changed files
with
5,390 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"trailingComma": "all", | ||
"tabWidth": 2, | ||
"semi": true, | ||
"arrowParens": "always", | ||
"printWidth": 120 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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! :-)" | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); | ||
} | ||
} |
Oops, something went wrong.