Fullstack observability with AWS CDK V2 Canary's and Synthetics

Version 2 of the AWS Cloud Development Kit (CDK) shipped a couple weeks ago.

As part of that release AWS have separated stable and experimental cdk libraries. In this blog I am going to build out with some of te new tools and features to learn a little about how to use experimental constructs, and also to share some observability tooling setups for monitoring your websites.

In this blog

TL; DR

The canary stack can be found here: https://github.com/simonireilly/canary-stack

Once deployed you will have a Dashboard and Alarm for an S3 bucket website.

The architecture that is built once complete is:

AWS Architecture diagram showing links canary and s3 hosted website

Starting a cdk v2 project

Lets begin.

I do the following to get the project configured:

mkdir canary

cd canary

npx [email protected] init app --language typescript

This creates the basic structure.

Previous modules would ship separately, this is still the case for experimental modules.

Adding Experimental modules

To add an experimental module you need to install it with yarn or npm.

yarn add @aws-cdk/aws-synthetics-alpha

You know, I could not recommend cdk-dia enough, with will give you a nice diagram of your stack:

yarn add --dev cdk-dia

Building a Website with canaries

Get started by adding a website to your stack:

mkdir -p lib/website

touch lib/website/index.html

In there you need some HTML, this will do:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Your website</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<main style="display: flex; flex-direction: column; align-items: center;">
<h1>Welcome to the canary stack</h1>

<p>This is a simple single page website that show cases the aws-cdk.</p>
</main>
</body>
</html>

Now you can deploy your website from the stack using 2 AWS buckets, one to hold the zipped files, and another to deploy as a public website.

import { Bucket } from "aws-cdk-lib/aws-s3";
import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment";
import { Construct } from "constructs";
import { Stack, StackProps, CfnOutput } from "aws-cdk-lib";
import * as path from "path";

export class CanaryStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const websiteBucket = new Bucket(this, "WebsiteBucket", {
websiteIndexDocument: "index.html",
publicReadAccess: true,
});

new BucketDeployment(this, "DeployWebsite", {
sources: [Source.asset(path.join(__dirname, "website"))],
destinationBucket: websiteBucket,
});

new CfnOutput(this, "WebsiteUrl", {
value: websiteBucket.bucketWebsiteUrl,
});
}
}

The output from this will reveal your website URL:

 ✅  CanaryStack

Outputs:
CanaryStack.WebsiteUrl = http://canarystack-websitebucket75c24d94-z8brk94c8nvu.s3-website-eu-west-1.amazonaws.com

Adding a Canary

We want to be able to monitor our websites up time, to do that we can add a canary.

The canary code looks like this:

const canary = new Canary(this, "MyCanary", {
schedule: Schedule.rate(Duration.minutes(5)),
test: Test.custom({
code: Code.fromAsset(path.join(__dirname, "canary")),
handler: "index.handler",
}),
runtime: Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_3,
environmentVariables: {
SITE_URL: websiteBucket.bucketWebsiteUrl,
},
});

You need to be specific about where you put the canary javascript. From what I can tell it follows the same patter as when building AWS layers.

mkdir -p lib/canary/nodejs/node_modules

touch lib/canary/nodejs/node_modules/index.js

Now you want to put the canary javascript into that file like so:

const synthetics = require("Synthetics");
const log = require("SyntheticsLogger");

const pageLoadBlueprint = async function () {
// Configure the stage of the API using environment variables
const url = String(process.env.SITE_URL);

const page = await synthetics.getPage();
const response = await page.goto(url, {
waitUntil: "domcontentloaded",
timeout: 30000,
});
// Wait for page to render. Increase or decrease wait time based on endpoint being monitored.
await page.waitFor(15000);
// This will take a screenshot that will be included in test output artifacts.
await synthetics.takeScreenshot("loaded", "loaded");
const pageTitle = await page.title();
log.info("Page title: " + pageTitle);
if (response.status() !== 200) {
throw "Failed to load page!";
}
};

exports.handler = async () => {
return await pageLoadBlueprint();
};

Now we can deploy again:

yarn cdk deploy

This time we will have a canary to go and see in the dashboard which looks like this:

AWS Admin panel screenshot showing canary polled every 5 minutes

Adding Alerts

Monitoring is hard, but it doesn't need to be hard to implement.

The CDK can make creating a dashboard and an alarm for the canary very easy.

Here is the code you need:

const alarm = new Alarm(this, "CanaryAlarm", {
metric: canary.metricSuccessPercent(),
evaluationPeriods: 2,
threshold: 90,
comparisonOperator: ComparisonOperator.LESS_THAN_THRESHOLD,
});

const alarmWidget = new AlarmWidget({
alarm,
title: "Canary Alarm",
});

const dashboard = new Dashboard(this, "MainDashbaord", {
dashboardName: "Main-Dashboard",
periodOverride: PeriodOverride.AUTO,
widgets: [[alarmWidget]],
});

Thats all it takes to get setup with a dashboard for your site.

Canary alarm on a cloudwatch dashboard

Adding Real User Monitoring

AWS just launched Real User Monitoring (RUM). This is a client side tool that tracks the real user journey and page load analytics.

You can read about RUM in Jeff Barr's announcement blog post: https://aws.amazon.com/blogs/aws/cloudwatch-rum/

You should follow the wizard to set this up and chose the option to get a new cognito user pool.

Then you can copy the snippet produced into the <head> tag within the lib/website/index.html.

Now we deploy again:

yarn deploy

When we go to the rum dashboard we will see all the stats for a users session:

Real user monitoring dashboard

Closing

With a canary in place you have: