-
Notifications
You must be signed in to change notification settings - Fork 4
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
Changes from 15 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 7ec36bd
Add API Gateway for Lambda backend
pcholakov 9b3d463
Use variable prefix from CDK context or env for stack name
pcholakov b589066
Add insecure internet-facing ALB exposing the Restate ingress endpoint
pcholakov c7ee2bd
Add custom resource that registers the Lambda handler via Restate dis…
pcholakov 113c6fb
Fix: log discovery endpoint in custom resource handler
pcholakov 44e7a25
Cleanup: remove some unnecessary outputs
pcholakov 9d3bc7a
Discover the concrete AMI to use
pcholakov 69262bf
Switch to Secrets Manager secret for passing the GitHub token into EC2
pcholakov 1ada6ed
Send Restate JSON logs to CloudWatch logs
pcholakov b97a0bd
Refactoring: Extract custom Restate constructs for all major components
pcholakov 2983cf7
Update README with deployment & test instructions
pcholakov 642761d
Add esbuild dev dependency (avoids Docker requirement for bundling La…
pcholakov 1da5f47
Minor cleanups and comment+TODO updates
pcholakov 8831b6d
Allow Restate to invoke handlers via explicit version qualifier
pcholakov 4d450f5
Ack warning - and actually silence it
pcholakov cf1b009
Switch to native Lambda invoke integration, remove API Gateway HTTP m…
pcholakov 4496b93
Remove GitHub token secret as no longer needed
pcholakov File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,47 @@ | ||
# 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 | ||
In order to access the Restate Docker image, you need to make a GitHub access token with permission to retrieve the | ||
Restate beta distribution available in your AWS account. | ||
|
||
* `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 | ||
aws secretsmanager create-secret --name /restate/docker/github-token --secret-string $GITHUB_TOKEN | ||
``` | ||
|
||
To deploy the self-hosted Restate instance, run: | ||
|
||
```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"; | ||
|
||
interface RegistrationProperties { | ||
ingressEndpoint?: string; | ||
metaEndpoint?: string; | ||
serviceEndpoint?: string; | ||
functionVersion?: number; | ||
} | ||
|
||
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.ingressEndpoint}/grpc.health.v1.Health/Check`; | ||
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 }); | ||
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: ${await discoveryResponse.text()} (${discoveryResponse.status})`); | ||
|
||
if (!(healthResponse.status >= 200 && healthResponse.status < 300)) { | ||
// TODO: retry until successful, or some overall timeout is reached | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(`Health check 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,79 @@ | ||
import { Construct } from "constructs"; | ||
import * as lambda from "aws-cdk-lib/aws-lambda"; | ||
import * as api_gw from "aws-cdk-lib/aws-apigateway"; | ||
import { SingleNodeRestateInstance } from "./single-node-restate-instance"; | ||
import * as cdk from "aws-cdk-lib"; | ||
|
||
/** | ||
* 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 | ||
}) { | ||
service.handler.grantInvoke(restate.instanceRole); | ||
service.handler.currentVersion.grantInvoke(restate.instanceRole); // Allow explicit version invocation | ||
|
||
const serviceHttpResource = restate.serviceApi.root.addResource(service.path); | ||
serviceHttpResource.addProxy({ | ||
defaultIntegration: new api_gw.LambdaIntegration(service.handler), | ||
anyMethod: true, | ||
}); | ||
|
||
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: { | ||
ingressEndpoint: props.restate.privateIngressEndpoint, | ||
metaEndpoint: props.restate.metaEndpoint, | ||
serviceEndpoint: props.restate.serviceApi.urlForPath(`/${props.service.path}`), | ||
functionVersion: props.service.handler.currentVersion.version, | ||
// TODO: force a refresh on EC2 instance configuration changes, too (https://github.com/restatedev/restate-cdk-support/issues/4) | ||
}, | ||
}); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should be using the health endpoint on the meta here, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're quite right, for pre-registration the meta health check is probably the one that matters! I'm going to revisit this entire handler to make it more robust but switching over to meta health check for now. (Separately, the ALB is continuously monitoring the ingress health check.)