Skip to content

Commit

Permalink
Introduce RestateCloudEnvironment construct (#39)
Browse files Browse the repository at this point in the history
* Introduce RestateCloudEnvironment construct

Convenience construct for deploying to Restate Cloud hosted environments using just an environment id and API key.

* Update dependencies

* Update ServiceDeployer log retention

By default, only retain deployment logs for 30 days.

* Update e2e tests

* Update AWS+CDK peer dependencies
  • Loading branch information
pcholakov authored Sep 2, 2024
1 parent c1fe75e commit 1ab611b
Show file tree
Hide file tree
Showing 12 changed files with 2,078 additions and 1,395 deletions.
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
*.d.ts
node_modules
.cdk.staging
cdk.out
dist
cdk*.out
cdk.context.json
dist
1 change: 1 addition & 0 deletions lib/restate-constructs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@

export * from "./service-deployer";
export * from "./restate-environment";
export * from "./restate-cloud-environment";
export * from "./deployments-common";
export * from "./single-node-restate-deployment";
101 changes: 101 additions & 0 deletions lib/restate-constructs/restate-cloud-environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as iam from "aws-cdk-lib/aws-iam";
import * as secrets from "aws-cdk-lib/aws-secretsmanager";
import { Construct } from "constructs";
import { IRestateEnvironment, RestateEnvironment } from "./restate-environment";
import { ServiceDeployer } from "./service-deployer";

/**
* Configuration for a Restate Cloud environment.
*/
export interface RestateCloudEnvironmentProps {
/**
* Unique id of the environment (including the `env_` prefix).
*/
readonly environmentId: EnvironmentId;

/**
* API key with administrative permissions. Used to manage services to the environment, see {@link ServiceDeployer}.
*/
readonly apiKey: secrets.ISecret;
}

/**
* A distinct Restate Cloud environment reference. This is a convenience utility for deploying to the
* [Restate Cloud](https://cloud.restate.dev/) hosted service.
*/
export class RestateCloudEnvironment extends Construct implements IRestateEnvironment {
readonly adminUrl: string;
readonly ingressUrl: string;
readonly authToken: secrets.ISecret;
readonly invokerRole: iam.IRole;

/**
* Constructs a Restate Cloud environment reference along with invoker. Note that this construct is only a pointer to
* an existing Restate Cloud environment and does not create it. However, it does create an invoker role that is used
* invoking Lambda service handlers. If you would prefer to directly manage the invoker role permissions, you can
* override the {@link createInvokerRole} method or construct one yourself and define the environment properties with
* {@link RestateEnvironment.fromAttributes} directly.
*
* @param scope parent construct
* @param id construct id
* @param props environment properties
* @returns Restate Cloud environment
*/
constructor(scope: Construct, id: string, props: RestateCloudEnvironmentProps) {
super(scope, id);
this.invokerRole = this.createInvokerRole(this, props);
this.authToken = props.apiKey;
this.adminUrl = adminEndpoint(RESTATE_CLOUD_REGION_US, props.environmentId);
this.ingressUrl = ingressEndpoint(RESTATE_CLOUD_REGION_US, props.environmentId);
}

/**
* This role is used by Restate to invoke Lambda service handlers; see https://docs.restate.dev/deploy/cloud for
* information on deploying services to Restate Cloud environments. For standalone environments, the EC2 instance
* profile can be used directly instead of creating a separate role.
*/
protected createInvokerRole(scope: Construct, props: RestateCloudEnvironmentProps): iam.IRole {
const invokerRole = new iam.Role(scope, "InvokerRole", {
assumedBy: new iam.AccountPrincipal(CONFIG[RESTATE_CLOUD_REGION_US].accountId).withConditions({
StringEquals: {
"sts:ExternalId": props.environmentId,
"aws:PrincipalArn": CONFIG[RESTATE_CLOUD_REGION_US].principalArn,
},
}),
});
invokerRole.assumeRolePolicy!.addStatements(
new iam.PolicyStatement({
principals: [new iam.AccountPrincipal("654654156625")],
actions: ["sts:TagSession"],
}),
);
return invokerRole;
}
}

function adminEndpoint(region: RestateCloudRegion, environmentId: EnvironmentId): string {
const bareEnvId = environmentId.replace(/^env_/, "");
return `https://${bareEnvId}.env.${region}.restate.cloud:9070`;
}

function ingressEndpoint(region: RestateCloudRegion, environmentId: EnvironmentId): string {
const bareEnvId = environmentId.replace(/^env_/, "");
return `https://${bareEnvId}.env.${region}.restate.cloud`;
}

export type EnvironmentId = `env_${string}`;
type RestateCloudRegion = "us";

interface RegionConfig {
accountId: string;
principalArn: string;
}

const RESTATE_CLOUD_REGION_US = "us";

const CONFIG = {
us: {
accountId: "654654156625",
principalArn: "arn:aws:iam::654654156625:role/RestateCloud",
},
} as Record<RestateCloudRegion, RegionConfig>;
17 changes: 11 additions & 6 deletions lib/restate-constructs/restate-environment.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as iam from "aws-cdk-lib/aws-iam";
import { IRole } from "aws-cdk-lib/aws-iam";
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
import { ISecret } from "aws-cdk-lib/aws-secretsmanager";
import * as secrets from "aws-cdk-lib/aws-secretsmanager";
import { FunctionOptions } from "aws-cdk-lib/aws-lambda";
import { ServiceDeployer } from "./service-deployer";
import { SingleNodeRestateDeployment } from "./single-node-restate-deployment";
import { RestateCloudEnvironment } from "./restate-cloud-environment";

/**
* A Restate environment is a unique deployment of the Restate service. Implementations of this interface may refer to
Expand All @@ -25,13 +25,18 @@ export interface IRestateEnvironment extends Pick<FunctionOptions, "vpc" | "vpcS
/**
* Authentication token to include as a bearer token in requests to the admin endpoint.
*/
readonly authToken?: secretsmanager.ISecret;
readonly authToken?: secrets.ISecret;
}

/**
* A reference to a Restate Environment that can be used as a target for deploying services. Use {@link fromAttributes}
* to instantiate an arbitrary pointer to an existing environment, or one of the {@link SingleNodeRestateDeployment} or
* {@link RestateCloudEnvironment} convenience classes.
*/
export class RestateEnvironment implements IRestateEnvironment {
readonly adminUrl: string;
readonly authToken?: ISecret;
readonly invokerRole?: IRole;
readonly authToken?: secrets.ISecret;
readonly invokerRole?: iam.IRole;
readonly serviceDeployer: ServiceDeployer;

private constructor(props: IRestateEnvironment) {
Expand Down
22 changes: 14 additions & 8 deletions lib/restate-constructs/service-deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ const DEFAULT_TIMEOUT = cdk.Duration.seconds(180);
* to be communicated to the registrar. Without this dependency, CloudFormation might perform an update deployment that
* triggered by a Lambda handler code or configuration change, and the Restate environment would be unaware of it.
*
* You can share the same instance across multiple service registries provided the configuration options are compatible
* You can share the same deployer across multiple service registries provided the configuration options are compatible
* (e.g. the Restate environments it needs to communicate with for deployment are all accessible via the same VPC and
* Security Groups).
* Security Groups, accept the same authentication token, and so on).
*
* Deployment logs are retained for 30 days by default.
*/
export class ServiceDeployer extends Construct {
/** The custom resource provider for handling "deployment" resources. */
Expand All @@ -61,12 +63,7 @@ export class ServiceDeployer extends Construct {

const eventHandler = new lambda_node.NodejsFunction(this, "EventHandler", {
functionName: props?.functionName,
logGroup:
props?.logGroup ??
new logs.LogGroup(this, "Logs", {
retention: logs.RetentionDays.ONE_MONTH,
removalPolicy: props?.removalPolicy ?? cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
}),
logGroup: props?.logGroup,
description: "Restate custom registration handler",
entry: props?.entry ?? path.join(__dirname, "register-service-handler/index.js"),
architecture: lambda.Architecture.ARM_64,
Expand All @@ -89,6 +86,15 @@ export class ServiceDeployer extends Construct {
: {}),
});

if (!props?.logGroup) {
// By default, Lambda Functions have a log group with never-expiring retention policy.
new logs.LogGroup(this, "DeploymentLogs", {
logGroupName: `/aws/lambda/${eventHandler.functionName}`,
retention: logs.RetentionDays.ONE_MONTH,
removalPolicy: cdk.RemovalPolicy.RETAIN_ON_UPDATE_OR_DELETE,
});
}

this.deploymentResourceProvider = new cr.Provider(this, "CustomResourceProvider", { onEventHandler: eventHandler });
}

Expand Down
Loading

0 comments on commit 1ab611b

Please sign in to comment.