Using standard constructs

In cdk you can use constructs for almost anything, it can contain your whole applications or many other constructs. Feel free to read the documentation https://docs.aws.amazon.com/cdk/v2/guide/constructs.html . I used a few standard constructs to easily deploy the websocket api and the rust lambda's. Keep in mind that at the time of writing this V2 of CDK was already out and I would recommend using the v2 version instead (which mainly changes the import at the top of the code).

So the below code is still in V1. I needed a few lambda's as you've seen it in the first chapter. So I wrote a construct to create a lambda in which I can pass all the parameters to create multiple lambda's which makes it easier for me to expand on it.

import {
  Effect,
  PolicyStatement,
  PolicyStatementProps,
} from "@aws-cdk/aws-iam";
import { RetentionDays } from "@aws-cdk/aws-logs";
import { WebSocketApi } from "@aws-cdk/aws-apigatewayv2";
import * as lambda from "@aws-cdk/aws-lambda";
import { Construct, Stack } from "@aws-cdk/core";
import { LambdaWebSocketIntegration } from "@aws-cdk/aws-apigatewayv2-integrations";

export interface RustWebsocketLambdaProps {
  pathToZip: string;
  api: WebSocketApi;
  apiStageName: string;
  environment?: { [key: string]: string };
  apiPath?: string;
}
export class RustWebsocketLambda extends lambda.Function {
  constructor(
    scope: Construct,
    id: string,
    props: RustWebsocketLambdaProps,
    lambdaProps?: lambda.FunctionProps | undefined
  ) {
    const { pathToZip, api, apiStageName, apiPath } = props;
    const _props: lambda.FunctionProps = {
      code: lambda.Code.fromAsset(pathToZip),
      handler: "doesntmatter",
      runtime: lambda.Runtime.PROVIDED_AL2,
      logRetention: RetentionDays.THREE_MONTHS,

      ...lambdaProps,
      environment: {
        ...lambdaProps?.environment,
        ...props.environment,
        CURRENT_WEBSOCKET: api.apiEndpoint,
      },
    };
    super(scope, id, _props);
    this.addToRolePolicy(new InvokeApiDocument(scope, api.apiId, apiStageName));
    if (apiPath) {
      api.addRoute(apiPath, {
        integration: new LambdaWebSocketIntegration({
          handler: this,
        }),
      });
    }
  }
}

export class InvokeApiDocument extends PolicyStatement {
  constructor(
    scope: Construct,
    apiId: string,
    apiStageName: string,
    props?: PolicyStatementProps | undefined
  ) {
    const ourProps: PolicyStatementProps = {
      actions: ["execute-api:Invoke", "execute-api:ManageConnections"],
      effect: Effect.ALLOW,
      resources: [
        `arn:aws:execute-api:${Stack.of(scope).region}:${
          Stack.of(scope).account
        }:${apiId}/${apiStageName}/*`,
      ],
      ...props,
    };
    super(ourProps);
  }
}

That was a lot, let's break it down a little. In the first part we define the interface that the construct needs to create the lambda and give it the proper permissions and information to handle rock paper scissors.

An interesting thing to see is that the line export class RustWebsocketLambda extends lambda.Function makes the new Class RustWebsocketLambda with all the properties of the lambda function construct that is provided by AWS. The construct is extended with some implementation on how the rust lambda's are built.

Let's highlight some interesting parts

      handler: "doesntmatter",
      runtime: lambda.Runtime.PROVIDED_AL2,

handler is usually how the lambda function knows where to enter the assembly or the script. But because this is a provided runtime it wil call the runtime and the handler doesn't matter. The runtime has to know what to call. Luckily there's a rust package that handles this well. I will get to that later.

We give the websocket Api to the lambda so it can subscribe to a path or get permission to call the websocket to send messages back to the connected parties.

So this is an example of a reusable construct in CDK. Maybe not the prettiest example, sorry.

You also have easier examples as the following. Here you see the websocket api being created. It needs 2 lambda's (here secretly the same one 🤫)

const props: WebSocketApiProps = {
      connectRouteOptions: {
        integration: new LambdaWebSocketIntegration({
          handler: connectDisconnectLambda,
        }),
      },
      disconnectRouteOptions: {
        integration: new LambdaWebSocketIntegration({
          handler: connectDisconnectLambda,
        }),
      },
    };

new WebSocketApi(scope, `${id}-websocket-api-v2`, props);

Here you can see how you can leverage a higher order construct where they wrap a lot of details in a construct to make your life easier.

The connect lambda and the disconnect lambda are there to handle the events of people connecting to the websocket and disconnecting from the websocket.

Thanks for reading

I enjoyed trying to make everything and I hope you'll enjoy reading it. If you are excited to have a chat or think I could improve feel free to write me on LinkedIn. I also have portfolio website where I put most of what I make and host trial projects portfolio.rohanengosia.com.