<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Peasey</title>
    <description>The latest articles on DEV Community by Peasey (@peasey).</description>
    <link>https://dev.to/peasey</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F513708%2F9634d2fc-7ad8-47d2-8b93-4baeb62f85e2.png</url>
      <title>DEV Community: Peasey</title>
      <link>https://dev.to/peasey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/peasey"/>
    <language>en</language>
    <item>
      <title>Using AWS Lambda and Slack to find Xbox Series X stock, so you don't have to</title>
      <dc:creator>Peasey</dc:creator>
      <pubDate>Mon, 28 Dec 2020 08:56:23 +0000</pubDate>
      <link>https://dev.to/peasey/using-aws-lambda-and-slack-to-find-xbox-series-x-stock-so-you-don-t-have-to-1d1g</link>
      <guid>https://dev.to/peasey/using-aws-lambda-and-slack-to-find-xbox-series-x-stock-so-you-don-t-have-to-1d1g</guid>
      <description>&lt;p&gt;Creating an event-driven serverless web browsing and notification tool to automate web-based tasks with AWS Lambda, Chrome, Puppeteer and Slack.&lt;/p&gt;

&lt;h1&gt;
  
  
  TL;DR
&lt;/h1&gt;

&lt;p&gt;Some fun examples including stock availability checks for the Xbox Series X are used to demonstrate the automation of web browsing tasks and notifications using AWS Lambda, headless Chrome, &lt;br&gt;
Puppeteer and Slack. The design decisions are explained, the code repo and implementation notes are shared, and video demos show the tool in action.&lt;/p&gt;
&lt;h1&gt;
  
  
  The idea
&lt;/h1&gt;

&lt;p&gt;During lockdown earlier this year, I wanted to buy a specific outdoor storage solution for the garden. However, this particular product was only available from one retailer and seemingly always out of stock. The retailer didn’t have a stock alerting feature, and I got tired of periodically checking the website to see it was still out of stock. I decided it would be cool to have a little tool that did it for me and notify me when it’s back in stock. I've been meaning to write this post for a while, then just recently, stock availability for the Xbox Series X became a thing, so a good topical reason to do it.&lt;/p&gt;
&lt;h1&gt;
  
  
  Design goals
&lt;/h1&gt;

&lt;p&gt;These are the design goals I had for the tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I’d like to be able to quickly script the automation of basic web browsing tasks (script/test/deploy in around 30 mins)&lt;/li&gt;
&lt;li&gt;I’d like to run multiple tasks&lt;/li&gt;
&lt;li&gt;I’d like to run the tasks on a schedule, such as daily or hourly, with each task having a different schedule&lt;/li&gt;
&lt;li&gt;I’d like to receive a notification on my phone when the task has something worth telling me, i.e. something is in stock or there was an unexpected error while running the task (so I can investigate/fix it)&lt;/li&gt;
&lt;li&gt;I don’t want to spend much (any) money to do this&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;
  
  
  Conceptual design
&lt;/h1&gt;

&lt;p&gt;This is the conceptual design of the tool I want to create:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ky7uB3LZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/b0tdvt2luicn9ldw8os1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ky7uB3LZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/b0tdvt2luicn9ldw8os1.png" alt="Illustration of the conceptual architecture for the web automation tool"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Technology selection
&lt;/h1&gt;

&lt;p&gt;Since we were in lockdown, I had some spare time on my hands and decided to invest some time researching how to build a tool/framework that would allow me to easily automate web browsing tasks.&lt;/p&gt;
&lt;h2&gt;
  
  
  Programming environment
&lt;/h2&gt;

&lt;p&gt;JavaScript/Node.js and its package ecosystem and community is my goto to get up and running quickly, so I’d be using that to build the tool and task framework.&lt;/p&gt;
&lt;h2&gt;
  
  
  Web browser automation
&lt;/h2&gt;

&lt;p&gt;There are several tools in the JavaScript/Node.js ecosystem you can use to do this, &lt;a href="https://www.npmtrends.com/nightmare-vs-puppeteer-vs-selenium-vs-slimerjs-vs-webdriverio"&gt;Puppeteer&lt;/a&gt; seems to be the most popular, and I’ve used it successfully for other automation tasks recently. Puppeteer is headless by default so ideal for automation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Zero-cost infrastructure
&lt;/h2&gt;

&lt;p&gt;The cost goal might seem a bit unreasonable, but due to the scheduling requirement, I knew this was a perfect fit for an event-driven serverless architecture. I’ve worked with AWS Lambda quite a lot for work and personal projects, and the free tier is quite generous, for personal projects I don’t think I’ve paid anything for it yet - if I have, it’s been pennies. However, I needed to validate if I could run web browsing tasks within the constraints of a Lambda function.&lt;/p&gt;
&lt;h2&gt;
  
  
  Headless browser
&lt;/h2&gt;

&lt;p&gt;Puppeteer automates Chromium browsers (headless and non-headless), but can Chromium run in a Lambda function? Not without some great work from the community to create a &lt;a href="https://github.com/alixaxel/chrome-aws-lambda"&gt;Chrome build for the AWS Lambda runtime&lt;/a&gt;. There’s also a &lt;a href="https://github.com/shelfio/chrome-aws-lambda-layer"&gt;Lambda layer&lt;/a&gt; solution for this too, although I haven’t tried this approach yet. Another great feature of this package is that it runs headless when running in Lambda and non-headless when running locally - so it’s frictionless to develop, test and run your scripts.&lt;/p&gt;
&lt;h2&gt;
  
  
  Notifications
&lt;/h2&gt;

&lt;p&gt;Getting push notifications on your phone usually requires you have an app you can publish the notification to via the vendor’s push notification service. There’s no chance I’m developing an app just to get notifications. I could use Twilio/SNS to send SMS messages instead of push notifications, but SMS isn’t a very flexible messaging format, plus it wouldn’t be completely free (although arguably a negligible cost for my usage). I already use Slack to get notifications for AWS billing alerts etc via SNS, and I know its Webhook API provides a simple but powerful way to deliver fairly rich messages that can appear as notifications on your devices. Plus it would be a cost-free solution (for my usage).&lt;/p&gt;
&lt;h1&gt;
  
  
  Validation
&lt;/h1&gt;

&lt;p&gt;Feeling comfortable I had all the components to build this tool, I created a quick proof of concept to validate the technology choices and the approach. I used the &lt;a href="https://www.serverless.com/"&gt;serverless framework&lt;/a&gt; to get up and running quickly with a single function that ran a basic web scraping task using &lt;a href="https://github.com/alixaxel/chrome-aws-lambda"&gt;chrome-aws-lambda&lt;/a&gt; and &lt;a href="https://github.com/puppeteer/puppeteer#readme"&gt;puppeteer-core&lt;/a&gt;. The serverless framework enables you to add AWS CloudWatch event rules as schedules to your Lambda functions with a &lt;a href="https://www.serverless.com/framework/docs/providers/aws/events/schedule/"&gt;few of lines of YAML&lt;/a&gt;. Sure enough, the solution was packaged in under 50MB and once deployed it ran on schedule and did exactly what I expected.&lt;/p&gt;
&lt;h1&gt;
  
  
  Design
&lt;/h1&gt;

&lt;p&gt;After the technology selection and validation, the conceptual design evolved into something more concrete:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---HZRR4vA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/tx8xcoc9vdyuxtihxefl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---HZRR4vA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/tx8xcoc9vdyuxtihxefl.png" alt="Illustration of the logical architecture for the web automation tool"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;
  
  
  Implementation
&lt;/h1&gt;

&lt;p&gt;I’ve published the code for the tool on &lt;a href="https://github.com/peasey/lambda-surf"&gt;Github&lt;/a&gt; with the examples from the demos further on in the post, feel free to use it and adapt it. Below are some notes on the implementation:&lt;/p&gt;
&lt;h2&gt;
  
  
  Plugins
&lt;/h2&gt;

&lt;p&gt;To make it quick and easy to add/remove tasks in the future I decided to create a plugin model where the tasks are dynamically loaded at runtime from a specified directory. The plugin implementation recursively scans the specified directory and requires any JavaScript modules it finds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (!pluginPath.endsWith('.test.js') &amp;amp;&amp;amp; pluginPath.endsWith('.js')) {
  if (!require.cache[pluginPath]) {
    log.info(`loading plugin: ${pluginPath}`)
    // eslint-disable-next-line import/no-dynamic-require
    return require(pluginPath)(container)
  }
  log.info(`plugin already loaded: ${pluginPath}`)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each plugin is passed a plugin container (array) that it should push itself into. I also wanted to develop my tasks using TDD, and my preference is to colocate the tests file with the subject file, so I had to specifically ignore test scripts in the loading sequence (line 1).&lt;/p&gt;

&lt;p&gt;I originally designed this as an ephemeral process and loaded the plugins on each invocation, but it turns out a Lambda process can hang around for a while, which makes sense from an optimisation point of view (especially if it has scheduled events within a relatively short time frame). Anyway, I had to add a check to see if the plugin was already loaded (line 2).&lt;/p&gt;

&lt;h2&gt;
  
  
  Tasks
&lt;/h2&gt;

&lt;p&gt;Now adding a task is as simple as adding a new JavaScript module, but what would a task look like? I decided each task should have the following structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;name&lt;/strong&gt;: used as the display name in notifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;url&lt;/strong&gt;: the entry point for the task and also a link in the notification for quick access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;emoji&lt;/strong&gt;: to easily distinguish the content for each task in a notification I decided to include an emoji as a prefix to the content&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;schedule&lt;/strong&gt;: the event schedule to run the task with, I decided to use the &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html#RateExpressions"&gt;AWS CloudWatch ‘rate’ expression for event schedules&lt;/a&gt; as it covers my needs and is easy to parse (I can always add ‘cron’ support later if I ever need it)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;run&lt;/strong&gt;: a function that performs the task (async of course), it should return a result that can be used in subsequent notifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;shouldNotify&lt;/strong&gt;: a function that is provided with the result of the task and returns true/false to signal whether a notification should be sent, this enables flexibility about what gets notified. For example, I might only want a notification if stock is available or if the task failed, otherwise don’t notify me at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a basic example from the task scheduling test for a task that runs every 5 minutes (demo later on):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const task = () =&amp;gt; ({
  name: 'Every 5 mins',
  url: 'http://localhost/task/minutes/5',
  emoji: ':five:',
  schedule: 'rate(5 minutes)',
  shouldNotify: () =&amp;gt; true,
  run: async function run() {
    return `${this.name} just ran`
  },
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/integration/tasks/plugin-task-provider.js"&gt;plugin task provider&lt;/a&gt; loads the tasks from a specified location and parses the schedule into a more filterable object representation using the &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/integration/aws/cloudwatch/rules/schedule-parser.js"&gt;schedule parser&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const matches = schedule.match(/(.*)\((\d*) (.*)\)/)
if (matches &amp;amp;&amp;amp; matches.length &amp;gt;= 4) {
  if (matches[1] === 'rate') {
    return {
      type: 'rate',
      unit: matches[3],
      value: parseInt(matches[2], 10),
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a chainable &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/integration/tasks/filter.js"&gt;task filter&lt;/a&gt; can easily filter a list of tasks based on their schedules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Task schedules
&lt;/h2&gt;

&lt;p&gt;I want a single Lambda function to run the tasks, which means I'll need multiple event schedules defined on the function. Since one of my design goals is to make it as simple as possible to add a new task, I don't want to have to remember to add new schedules to my function as and when the need for them comes up. I'd prefer the schedule requirements were picked up automatically from the tasks that have been defined.&lt;/p&gt;

&lt;p&gt;One of the reasons I chose the serverless framework is due to its extensibility, I've &lt;a href="https://blog.peasey.co.uk/blog/blue-green-deployments-for-cloudflare-workers/"&gt;previously written about using plugins and lifecycle hooks to add new capabilities&lt;/a&gt;. I created a &lt;a href="https://github.com/peasey/lambda-surf/blob/main/.serverless_plugins/task-schedules/index.js"&gt;serverless framework plugin&lt;/a&gt; that hooks into the &lt;code&gt;before:package:initialize&lt;/code&gt; lifecycle hook to load the tasks and build a unique list of schedules, which it adds to the function definition dynamically before the function is packaged and deployed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Task host
&lt;/h2&gt;

&lt;p&gt;The task host is the execution environment that receives the invocation event and is responsible for resolving the invocation schedule. In this case, the host is a Lambda function, and unfortunately the event payload only contains a reference to the CloudWatch event rule ARN that invoked the Lambda, rather than the rule itself. So, I have to jump through some hoops to split the rule ARN to get the rule name using the &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/integration/aws/lambda/resource-parser.js"&gt;resource parser&lt;/a&gt;, then get the rule with its schedule from the CloudWatch events API before parsing it with the &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/integration/aws/cloudwatch/rules/schedule-parser.js"&gt;schedule parser&lt;/a&gt;. This all comes together in the &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/integration/aws/lambda/host.js"&gt;host&lt;/a&gt; to load the tasks and filter them based on the invocation schedule, and if there are any, runs them via the &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/integration/tasks/runner.js"&gt;task runner&lt;/a&gt; and awaits the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const ruleName = resourceParser.parse({ resource: event.resources[0] })
if (ruleName) {
  const rule = await rules.byName({ name: ruleName })
  if (rule) {
    log.info(
      `invocation schedule is ${rule.schedule.type}(${rule.schedule.value} ${rule.schedule.unit})`,
    )
    log.info('loading tasks')
    const tasks = await taskProvider.tasks()
    if (tasks.length &amp;gt; 0) {
      log.info(`loaded ${tasks.length} tasks`)
      const scheduledTasks = taskFilter(tasks).schedule(rule.schedule).select()
      log.info(`running ${scheduledTasks.length} scheduled tasks`)
      result.tasks = await runner.run({ tasks: scheduledTasks })
      result.tasks.total = tasks.length
      result.completed = true
      log.info('done')
    }
  } else {
    log.info('could not parse the schedule')
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The host augments the result from the task runner with the total tasks provided to the runner and signals that the process completed successfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  Task runner
&lt;/h2&gt;

&lt;p&gt;The first thing the task runner does is map through all the provided tasks and runs them, adding any successfully run tasks and their results to a list of successful runs, and the failed tasks and their results to a list of failed runs, which are returned with a count of the tasks run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const result = {
  run: 0,
  succeeded: [],
  failed: [],
}

const promises = tasks.map(async (task) =&amp;gt; {
  try {
    log.info(`running ${task.name} task`)
    result.run += 1
    const taskResult = await task.run()
    result.succeeded.push({ task, result: taskResult })
  } catch (err) {
    log.error(`error running ${task.name} task`, err)
    result.failed.push({ task, result: err })
  }

  return result
})

await Promise.all(promises)

return result

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the task runs are complete, the task runner determines which tasks should have notifications and sends them via the notifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notifier
&lt;/h2&gt;

&lt;p&gt;In this case, the notifier is sending the notifications via Slack. First, each task result is summarised into a block of text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;text: `&amp;lt;${success.task.url}|${success.task.name}&amp;gt;\n${success.task.emoji} ${success.result}`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failed tasks are summarised similarly, except an ❗ emoji is used.&lt;/p&gt;

&lt;p&gt;The task result summaries (for success and failures) are sent in a single Slack message, with each summary in a separate block and interspersed with dividers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const message = {
  blocks: [],
}

const toBlock = (summary) =&amp;gt; ({
  type: 'section',
  text: {
    type: 'mrkdwn',
    text: summary.text,
  },
})

const blocks = summaries.map(toBlock)

const divider = {
  type: 'divider',
}

message.blocks = intersperse(blocks, divider)

return message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The message is then sent to the Slack Webhook endpoint configured in the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const endpoint = process.env.SLACK_ENDPOINT
...
const response = await fetch(endpoint, {
  method: 'POST',
  body: JSON.stringify(message),
  headers: { 'Content-Type': 'application/json' },
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s the gist of it, time for some demos.&lt;/p&gt;

&lt;h1&gt;
  
  
  Demos
&lt;/h1&gt;

&lt;p&gt;I have 2 demos for this tool. The first demo is of a test I created to ensure scheduled events worked with tasks as expected. The second is a more practical example of some real-world tasks, a daily check for rumours about my football club (Newcastle United) and a topical/seasonal example, checking stock availability for an Xbox Series X.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schedule task runner
&lt;/h2&gt;

&lt;p&gt;I set up this demo to test the scheduled running of tasks, it consists of 4 tasks that are scheduled to run every &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/test-tasks/minutes_5.js"&gt;5 minutes&lt;/a&gt;, &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/test-tasks/minutes_10.js"&gt;10 minutes&lt;/a&gt;, &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/test-tasks/hour_1.js"&gt;once an hour&lt;/a&gt; and &lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/test-tasks/hours_2.js"&gt;every 2 hours&lt;/a&gt;. The tasks don’t do much other than return some text detailing that they ran, but each has a number emoji so I can see if it’s working correctly:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/eYbmezjPU7Q"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Footy gossip and Xbox Series X stock checks
&lt;/h2&gt;

&lt;p&gt;Examples of some tasks I’m using right now are to scrape any rumours about Newcastle United from the &lt;a href="https://www.bbc.co.uk/sport/football/gossip"&gt;BBC football gossip page&lt;/a&gt; which I run on a daily schedule, and checking the &lt;a href="https://www.xbox.com/en-GB/consoles/xbox-series-x#purchase"&gt;Xbox website&lt;/a&gt; for stock availability of the Series X, which I run on an hourly schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Footy gossip
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/tasks/footy-gossip.js"&gt;This task&lt;/a&gt; loads the gossip page, finds all the individual paragraphs and applies a regular expression (rumourMatcher) to filter paragraphs that contain the words Newcastle or Toon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const rumourMatcher = /(Newcastle|Toon)/
...
const page = await browser.newPage()

await page.goto(url)
const allRumours = (await page.$$('article div p')) || []

log.info(`found ${allRumours.length} total rumours...`)

const text = await Promise.all(
  [...allRumours].map((rumour) =&amp;gt; rumour.getProperty('innerText').then((item) =&amp;gt; item.jsonValue()),
),)

const matchedRumours = text.filter((rumour) =&amp;gt; rumour.match(context.rumourMatcher))

log.info(`found ${matchedRumours.length} matching rumours...`)

result = matchedRumours.length &amp;gt; 0 ? matchedRumours.join(`\n\n`) : 'No gossip today.'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any matching rumours are concatenated together with some spacing lines, and if none are matched the text ‘No gossip today.’ is returned. The task is configured with a football emoji.&lt;/p&gt;

&lt;h2&gt;
  
  
  Xbox Series X stock availability
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/peasey/lambda-surf/blob/main/src/tasks/xbox.js"&gt;This task&lt;/a&gt; loads the stock availability page for the standalone Xbox Series X, finds all the retailers, extracts the retailer name (or domain) from the alt text of the logo image and the stock availability text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const page = await browser.newPage()

await page.goto(url)
const retailerElements = (await page.$$('div.hatchretailer')) || []

log.info(`found ${retailerElements.length} retailers...`)

const retailerName = async (retailer) =&amp;gt;
retailer.$eval(
  `span.retlogo img`,
  (element) =&amp;gt; element.getAttribute('alt').slice(0, -' logo'.length), // trim ' logo' off the end of the alt text to get the retailer name
)

const retailerStock = async (retailer) =&amp;gt;
retailer.$eval(`span.retstockbuy span`, (element) =&amp;gt; element.innerHTML)

const hasStock = (retailers) =&amp;gt;
retailers.reduce((acc, curr) =&amp;gt; {
  if (curr.stock.toUpperCase() !== 'OUT OF STOCK') {
    acc.push(curr)
  }

  return acc
}, [])

const retailers = await Promise.all(
  [...retailerElements].map(async (retailer) =&amp;gt; ({
    name: await retailerName(retailer),
    stock: await retailerStock(retailer),
  })),
)

const retailersWithStock = hasStock(retailers)

result =
  retailersWithStock.length &amp;gt; 0
  ? retailersWithStock.map((retailer) =&amp;gt; `${retailer.name} (${retailer.stock})`).join(`\n\n`)
: 'No stock.'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I don’t know what the text is when there is stock, so I’m testing the stock availability text for anything that isn’t ‘OUT OF STOCK’ to determine retailers that &lt;em&gt;might&lt;/em&gt; have stock, and again, concatenating any retailers with potential stock together with some spacing lines, and if none are matched the text ‘No stock.’ is returned. The task is configured with a joystick emoji.&lt;/p&gt;

&lt;p&gt;Here are the tasks in action:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/HmkEvOw8vFk"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Note: I changed the schedules to 1 minute to quickly demo the tasks running.&lt;/p&gt;

&lt;h1&gt;
  
  
  Wrapping up
&lt;/h1&gt;

&lt;p&gt;Well if you didn’t unwrap an Xbox Series X for Xmas, now you can be one of the first to know when they’re available again. I’ve shown you some fun examples of how you can use this technology, it’s especially useful where you want to act on data that isn’t available via other means, such as an alert or API. There's loads of things you can do, for fun or profit, I'll leave it to your imagination - the world wide web is your oyster.&lt;/p&gt;

&lt;p&gt;The original title of this article (Using AWS Lambda and Slack to browse the web, so you don't have to) was published on &lt;a href="https://blog.peasey.co.uk/blog/using-aws-lambda-and-slack-to-browse-the-web-so-you-dont-have-to/"&gt;my blog&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>node</category>
      <category>aws</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Delivering APIs at the edge with Cloudflare Workers</title>
      <dc:creator>Peasey</dc:creator>
      <pubDate>Mon, 16 Nov 2020 14:53:10 +0000</pubDate>
      <link>https://dev.to/peasey/delivering-apis-at-the-edge-with-cloudflare-workers-225i</link>
      <guid>https://dev.to/peasey/delivering-apis-at-the-edge-with-cloudflare-workers-225i</guid>
      <description>&lt;h1&gt;
  
  
  TL;DR
&lt;/h1&gt;

&lt;p&gt;The background is given about why Cloudflare Workers were chosen to deliver an API, there's an exploration phase covering constraints, architecture, development, delivery and operations aspects, followed by an implementation phase with demo videos covering using Node.js and VS Code for local development and debugging, logical Cloudflare environments, blue/green deployments, middleware and routing, and observability.&lt;/p&gt;

&lt;h1&gt;
  
  
  Background
&lt;/h1&gt;

&lt;p&gt;While we were looking at solutions for a new service, we faced uncertainty over some requirements, and if they could be met with a third-party solution we’d found. We also considered if we should build a solution ourselves or wrap the third-party solution to plug any requirement gaps. We decided that the most likely outcomes would require us to build an API of some description. We made good progress on an innovative approach to building APIs using Cloudflare Workers, so we thought we’d share the approach.&lt;/p&gt;

&lt;p&gt;This article is a summary of a series of posts I wrote on &lt;a href="https://blog.peasey.co.uk/blog/delivering-apis-at-the-edge-with-cloudflare-workers/"&gt;my blog&lt;/a&gt; about this, there’s a GitHub repo accompanying most of the posts so I’ll link to the relevant posts for those that want a deeper dive.&lt;/p&gt;

&lt;h1&gt;
  
  
  Our high-level API requirements
&lt;/h1&gt;

&lt;p&gt;At the time, our primary concern was the lack of Open ID Connect integration with the third-party solution. We wanted to ensure only end-users that had been authenticated with our identity provider could use the service.&lt;/p&gt;

&lt;p&gt;We also needed to store a small amount of data and some processing logic for each user that wasn’t currently configurable with the third-party solution.&lt;/p&gt;

&lt;p&gt;We knew that any solution had to be highly available and capable of handling the demand of our global user base.&lt;/p&gt;

&lt;p&gt;In line with our design guidelines, we wanted to keep costs and operational complexity to a minimum and leverage serverless technology where possible.&lt;/p&gt;

&lt;p&gt;Finally, in line with our CI/CD guidelines, we wanted to automate everything and ensure the solution was always up.&lt;/p&gt;

&lt;h1&gt;
  
  
  Why Cloudflare Workers?
&lt;/h1&gt;

&lt;p&gt;Good question. Originally, we looked at a more typical serverless architecture in AWS using API Gateway and Lambda functions. The new HTTP API type had just been introduced to API Gateway and we were weighing up the pros and cons of choosing that over the REST API type. As a team, we’d also recently had a frustrating experience trying to automate the delivery of multi-region zero downtime (blue/green deployments) architectures with the serverless tech in AWS.&lt;/p&gt;

&lt;p&gt;It just felt like there should be a simpler way to deploy highly available and scalable APIs using serverless technology.&lt;/p&gt;

&lt;p&gt;Another team had recently used Cloudflare Workers to process HTTP headers on requests before they hit their API and we thought that was an interesting approach to running code with global availability, scale and performance, and might offer an interesting solution for the API “wrapper” architecture we were considering, without the headache of multi-region architectures and other deployment complexity.&lt;/p&gt;

&lt;p&gt;We decided to commit some time to explore the idea.&lt;/p&gt;

&lt;h1&gt;
  
  
  Exploration
&lt;/h1&gt;

&lt;p&gt;Cloudflare Workers weren’t specifically designed to deliver APIs, so we needed to focus our attention on the following to test the feasibility of the idea:&lt;/p&gt;

&lt;h2&gt;
  
  
  Runtime constraints
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developers.cloudflare.com/workers/platform/limits"&gt;Workers platform limits&lt;/a&gt; are published, we have an enterprise agreement so are subject to the “bundled” limits. For us, the constraints of note are:&lt;/p&gt;

&lt;h3&gt;
  
  
  CPU runtime
&lt;/h3&gt;

&lt;p&gt;At first glance, 50ms seems low, but it's important to note that this is CPU time you use on the edge servers per request, it's not your request duration. So, while your Worker is waiting for asynchronous I/O to complete, it's not counting towards your CPU usage.&lt;/p&gt;

&lt;p&gt;Interestingly, not long after we’d finished looking at this, Cloudflare &lt;a href="https://blog.cloudflare.com/introducing-workers-unbound/"&gt;announced Workers Unbound&lt;/a&gt; with the CPU restriction removed altogether, which I think is confirmation that Workers are being used for increasingly more complex use cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Programming environment
&lt;/h3&gt;

&lt;p&gt;You have two options for programming Workers: JavaScript or a WebAssembly compatible language. A quick look at both approaches showed that the JavaScript approach seemed more mature and benefited from better community engagement and tooling support. &lt;/p&gt;

&lt;p&gt;The Worker JavaScript environment is aligned to Web Workers, so writing JavaScript for Workers is more akin to writing a Worker in a browser than a server-side environment like Node.js. This means care needs to be taken when adding dependencies to ensure they are compatible with the &lt;a href="https://developers.cloudflare.com/workers/runtime-apis"&gt;runtime APIs&lt;/a&gt;. As an example, you can’t use the standard AWS JavaScript SDK as it doesn’t use the Fetch API for HTTP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Worker script size
&lt;/h3&gt;

&lt;p&gt;The maximum size for a Worker script is 1MB. This shouldn’t be an issue when using &lt;a href="https://webpack.js.org/"&gt;webpack&lt;/a&gt; to bundle your JavaScript, and if you use a (smaller) script per Worker rather than sharing a (large) script across all Workers.&lt;/p&gt;

&lt;p&gt;Although we did see an issue with this when we added the &lt;a href="https://momentjs.com/"&gt;moment package&lt;/a&gt; to perform some date processing - the default package size is very large due to the locale files, but &lt;a href="https://github.com/jmblog/how-to-optimize-momentjs-with-webpack"&gt;you can optimise it&lt;/a&gt; (or just replace it with something else).&lt;/p&gt;

&lt;p&gt;Note: the script size limitation is no longer 1MB, recently it got bumped up to 25MB.&lt;/p&gt;

&lt;h2&gt;
  
  
  API architecture and routing
&lt;/h2&gt;

&lt;p&gt;When building APIs, your service/framework typically allows you to define API routes based on properties of the HTTP request. For RESTful APIs, the HTTP method and path are typically used to map requests to resource handlers. Popular API frameworks such as &lt;a href="https://expressjs.com/en/guide/using-middleware.html"&gt;Express&lt;/a&gt; and &lt;a href="https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/"&gt;ASP.NET Core&lt;/a&gt; allow you to define middleware that enables you to factor out common tasks into pipelines that can be applied in sequence to multiple API routes.&lt;/p&gt;

&lt;p&gt;The route matching capabilities in Cloudflare Workers are quite basic. You can use a wildcard (*) in matching patterns but only at the beginning of the hostname and the end of the path, and there's no support for parameter placeholders. So, the following are ok:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*api.somewhere.com/account*
api.somewhere.com/account/action*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But these aren’t:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api.somewhere.com/*/account*
api.somewhere.com/account/:id/action
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last example above is a valid route, it just won't do what you're probably trying to do, i.e. use :id as a placeholder for any value and provide that value in an easily accessible way in the Worker.&lt;/p&gt;

&lt;p&gt;Also, note in the valid examples that the pattern doesn't include the trailing slash of the path before the wildcard, this is so the pattern still matches on requests to the root of said path/resource (with or without the trailing slash).&lt;/p&gt;

&lt;p&gt;This all means we must move the API route handling logic into our Worker, as you would with frameworks like Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/account/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;readAccount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;readAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above code is configuring the express middleware to run the &lt;strong&gt;readAccount&lt;/strong&gt; function on the get method for paths that match &lt;strong&gt;/account/:id&lt;/strong&gt; in the HTTP request (where &lt;strong&gt;:id&lt;/strong&gt; is a placeholder for an arbitrary value).&lt;/p&gt;

&lt;h2&gt;
  
  
  Development experience
&lt;/h2&gt;

&lt;p&gt;When developing applications/services, engineers want fast local feedback cycles to quickly iterate on their work and deliver efficiently. Working with cloud services can significantly slowdown that cycle while you're waiting for code to deploy and execute.&lt;/p&gt;

&lt;p&gt;Cloudflare provides the &lt;a href="https://developers.cloudflare.com/workers/cli-wrangler"&gt;wrangler CLI&lt;/a&gt; to support local development and publishing of Workers, the &lt;strong&gt;dev&lt;/strong&gt; mode aims to enable a faster local feedback cycle by listening to requests on a local server.&lt;/p&gt;

&lt;p&gt;However, the ability to easily debug the code using local development tools such as VS Code is key to effective and efficient development.&lt;/p&gt;

&lt;p&gt;It’s also worth considering the consistency of tooling between local development and CI/CD processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivery experience
&lt;/h2&gt;

&lt;p&gt;Deliverability of the API is crucial. From the outset, we want to know how we're going to provision resources in environments and how we can deploy and roll-back/forward/sideways with zero downtime to ensure high availability.&lt;/p&gt;

&lt;p&gt;We're also going to deploy other services in AWS that we’ll be integrating with, so ideally, we’ll have a consistent tooling experience for our CI/CD processes across different service providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operations experience
&lt;/h2&gt;

&lt;p&gt;Once the API is deployed, we want to keep an eye on it and make sure we can react to any issues.&lt;/p&gt;

&lt;p&gt;Cloudflare offers some basic Worker metrics you can periodically query via their &lt;a href="https://developers.cloudflare.com/analytics/graphql-api/tutorials/querying-workers-metrics"&gt;GraphQL API&lt;/a&gt;, but it won’t give you an API centric view, or the ability to easily trigger alerts, so some custom metrics will be required to monitor the API effectively.&lt;/p&gt;

&lt;p&gt;By default, log messages in Workers are ephemeral and simply sent to the standard output/error streams. This is ok to support local development and debugging in the Cloudflare workers.dev dashboard, but it would be useful to persist these logs from production workloads to support potential troubleshooting scenarios.&lt;/p&gt;

&lt;h1&gt;
  
  
  Implementation
&lt;/h1&gt;

&lt;p&gt;After a phase of exploration, we had an idea how we could implement it that would tie all the above together and enable a global serverless API that was cost-effective to run, highly available, scalable, and easy to deliver. So, we built a proof of concept that incorporated the following elements:&lt;/p&gt;

&lt;h2&gt;
  
  
  Serverless framework
&lt;/h2&gt;

&lt;p&gt;From a delivery point of view, we decided to use the &lt;a href="https://www.serverless.com/"&gt;Serverless framework&lt;/a&gt; to provide a common approach to provisioning and deploying our Cloudflare and AWS resources, both locally and from our CI/CD processes.&lt;/p&gt;

&lt;p&gt;The AWS provider in the Serverless framework is an abstraction over CloudFormation and other AWS service APIs, and the Cloudflare provider is an abstraction over the Cloudflare APIs:&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/h2qjuv93yepw/4ODwSApyCgu74caAw5HEev/7b9aae8b34769e7a0410cdffd9d1c0f3/Serverless_framework.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/h2qjuv93yepw/4ODwSApyCgu74caAw5HEev/7b9aae8b34769e7a0410cdffd9d1c0f3/Serverless_framework.png" alt="Illustration of multi-provider architecture in the Serverless framework"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The plugin model for the Serverless framework allows you to augment/extend the capabilities of each provider where there are gaps in the framework, or if you want to provide custom functionality:&lt;/p&gt;

&lt;p&gt;&lt;a href="//images.ctfassets.net/h2qjuv93yepw/1l2ggaxjfIcu3Na7mHhrQZ/861083b948f723411ada404fa099327f/Serverless_framework_plugins.png" class="article-body-image-wrapper"&gt;&lt;img src="//images.ctfassets.net/h2qjuv93yepw/1l2ggaxjfIcu3Na7mHhrQZ/861083b948f723411ada404fa099327f/Serverless_framework_plugins.png" alt="Illustration of plugin augmentation architecture in the Serverless framework"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For instance, we wrote a plugin that would hydrate KV (Cloudflare’s key/value data store) with data such as signing certificates and reference data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Blue/Green deployments
&lt;/h2&gt;

&lt;p&gt;While exploring Cloudflare Workers, the simplicity of the routing capability struck us as a great way to flexibly and quickly change the code that would run for requests to a given endpoint. The idea was to use this flexibility to enable blue/green deployments for our API by using state embedded in a naming convention of the Workers and dynamically update the Worker route mappings at the point of deployment.&lt;/p&gt;

&lt;p&gt;By creating a Serverless plugin we could hook into the &lt;strong&gt;before:deploy&lt;/strong&gt; hook to inspect the current Worker route mappings and determine the current slot, and then pre-process the template to configure it for deployment to the next slot. We could do the same for the &lt;strong&gt;before:remove&lt;/strong&gt; hook to ensure the correct resources were removed when required.&lt;/p&gt;

&lt;p&gt;In addition to those hooks, we could create plugin commands that are actionable from the Serverless CLI to activate and rotate slots by calling the appropriate Cloudflare APIs.&lt;/p&gt;

&lt;p&gt;Those plugin commands would be available locally and in CI/CD processes, so the rotate slot command could be executed at the end of a Continuous Deployment process, or via an approval trigger after a Continuous Delivery process.&lt;/p&gt;

&lt;p&gt;Watch a demo of blue/green deployments using the Serverless framework:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/s5fzAPlbheQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can read more about blue/green deployments with the Serverless framework and details on accessing the code in the &lt;a href="https://blog.peasey.co.uk/blog/blue-green-deployments-for-cloudflare-workers"&gt;blog post&lt;/a&gt; on the subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node.js and VS Code
&lt;/h2&gt;

&lt;p&gt;The dev command in the wrangler CLI enables you to send HTTP requests to an instance of your Worker running locally, but to be honest we didn't find the mapping of Workers to scripts and routes in the required wrangler.toml file as intuitive, flexible or extensible as it is with the Serverless framework. We also struggled to find a way to easily launch (i.e. hit F5) into a debugging session with VS Code when using wrangler.&lt;/p&gt;

&lt;p&gt;Since we preferred the Serverless framework for provisioning and deploying anyway, we decided to design a development experience that would allow us to use VS Code and Node.js to build and debug our API without using wrangler.&lt;/p&gt;

&lt;p&gt;To do that we embedded the principles of &lt;strong&gt;substitutable dependencies&lt;/strong&gt; and &lt;strong&gt;substitutable execution context&lt;/strong&gt; into our design.&lt;/p&gt;

&lt;p&gt;Substitutable dependencies is an inversion of control technique that requires identification of specific runtime features you will depend on when running in a given execution context (Cloudflare Workers) that may require an alternative implementation in another execution context (Node.js), and making sure you have a mechanism for substituting the dependencies (a form of dependency injection). An example is environment variables, in Node.js you access process.env and in Cloudflare they are accessible in the global scope.&lt;/p&gt;

&lt;p&gt;Substitutable execution context follows on from the principle of substitutable dependencies and is the principle that your code should be appropriately encapsulated so that it is runnable in any execution context, with minimal integration to acquire input and generate output. Practically speaking this involves identifying the entry and exit points of your execution context and ensuring as much of your code as possible is contained within portable abstractions. This allows you to test most of your application code irrespective of the target execution context, and for those thin layers of integration, you can use appropriate mocks and integration tests at appropriate points in your delivery pipeline.&lt;/p&gt;

&lt;p&gt;With appropriate abstractions in place for configuration etc and a substitution mechanism that took advantage of the global scope used in Cloudflare Workers, we were able to easily run and test our API resources locally in Node.js. Since we were able to run in a Node.js process, this meant we could create a debug launch configuration in VS Code that allowed us to easily debug via the debugging tools or by hitting F5.&lt;/p&gt;

&lt;p&gt;Watch a demo of Worker debugging in VS Code:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/sYa_4xVDo6U"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Logical environments
&lt;/h2&gt;

&lt;p&gt;The approach above enabled us to iterate quickly while working locally, but we wanted a way to test the integration of our code into Cloudflare Workers while working locally before committing to the shared repo. When we do commit to the shared repo, we want to have CI/CD processes running on our commits and pull requests (PRs) that can deploy our Workers and run integration tests. Having a separate Cloudflare account per developer and CI/CD process isn't feasible, especially when premium features are required, and we share resources such as DNS records/TLS certs.&lt;/p&gt;

&lt;p&gt;Enter the logical environment. This is a concept that allows multiple deployments of the same resources to exist in the same physical environment. The concept follows the blue/green deployments approach where an environment label forms part of the naming convention for the routes and Worker scripts and is dynamically embedded at the point of deployment. We modified the Serverless plugin to include the concept of an environment.&lt;/p&gt;

&lt;p&gt;Practically speaking this means that each engineer can have a private local environment file (.env) that contains an environment identifier specific to them, which ensures any resources they deploy are uniquely namespaced to them. Likewise, CI/CD processes can set the environment identifier appropriately to create resources for specific purposes, and then remove them at the end of a lifecycle (such as closing/merging a PR).&lt;/p&gt;

&lt;p&gt;Watch a demo of a logical environment being used for local development:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/3rEdbQ64x7w"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Watch a demo of a logical environment being used for a GitHub Pull Request review:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/3UNdBhz7Kqo"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can read more on using Node.js, VS Code and logical environments and accessing the code in the &lt;a href="https://blog.peasey.co.uk/blog/enhancing-the-development-experience-for-cloudflare-workers"&gt;blog post&lt;/a&gt; on the subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing and Middleware
&lt;/h2&gt;

&lt;p&gt;While the simplicity of the Workers routing is great for enabling use cases like zero-downtime deployments, it’s not great for mapping HTTP requests to API endpoints – but Cloudflare Workers wasn’t designed to be an API gateway.&lt;/p&gt;

&lt;p&gt;The solution is not so different from how you might do it in other execution contexts, such as containers if you aren’t using an API gateway - middleware.&lt;/p&gt;

&lt;p&gt;We considered the feasibility of running existing middleware frameworks like Express in a Worker, but they’re too dependent on the Node.js runtime, and/or would require extensive customisation/adaptation and unlikely to fit within the 1MB script size limit.&lt;/p&gt;

&lt;p&gt;Instead, we borrowed concepts such as route matching and found lightweight modules we could integrate and adapt to enable modular asynchronous pipelines to handle different combinations of HTTP methods and paths.&lt;/p&gt;

&lt;p&gt;Watch a demo of middleware with authorisation and validation middleware responding accordingly:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/L5QQS8yVuI0"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can read more on the middleware architecture and accessing the code in the &lt;a href="https://blog.peasey.co.uk/blog/a-middleware-architecture-for-cloudflare-workers"&gt;blog post&lt;/a&gt; on the subject.&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS CloudWatch Logs and Metrics
&lt;/h2&gt;

&lt;p&gt;Since part of our solution was going to be in AWS anyway, we decided that CloudWatch would be a good option for observability. There’s some impedance between the availability of a global solution like Cloudflare Workers and regional solutions in AWS, but the cross-region reporting capabilities of CloudWatch gave us confidence we could have a global solution to observability if we implemented failure detection and multi-region capabilities in our Workers (although we only implemented a single region for the proof of concept).&lt;/p&gt;

&lt;p&gt;There were three options to integrate AWS CloudWatch, which are also relevant for other AWS services, these were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Direct from Cloudflare Workers to AWS Service APIs, but this required implementing the AWS v4 request signing process with CPU intensive crypto functions.&lt;/li&gt;
&lt;li&gt;Via API Gateway, a Lambda function and the AWS SDK, but the cost of running Lambda was orders of magnitude higher than the cost to run the entire API in Cloudflare.&lt;/li&gt;
&lt;li&gt;Via API Gateway but mapped directly to the AWS Service APIs, i.e. no Lambda.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We chose the third option as it offered minimal cost and there was no need for CPU intensive crypto in our Workers, balanced against a little bit of complexity to setup the API Gateway mappings.&lt;/p&gt;

&lt;p&gt;For logs, we wanted the logger to be easily accessible to all code and for log messages to go to standard output regardless of the execution context. When running in Cloudflare, we also wanted the messages to be persisted so they can be flushed to an observability endpoint at the end of the request. We created a logging abstraction that was substitutable to handle those requirements.&lt;/p&gt;

&lt;p&gt;For metrics, we were only interested in creating/seeing them when running in Cloudflare. Most of the metrics could be derived from data in the original request or the response, the exception was duration, for that, we needed to track the start and end time of the request. We created a substitutable observability abstraction that encapsulated the steps to create the stream, log messages and metrics.&lt;/p&gt;

&lt;p&gt;The logs and metrics are asynchronously dispatched to the observability endpoint at the end of each Cloudflare Worker request.&lt;/p&gt;

&lt;p&gt;Watch a demo of observability for Cloudflare Workers using AWS CloudWatch:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/uDSNiT_tYcE"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;You can read more on observability and accessing the code in the &lt;a href="https://blog.peasey.co.uk/blog/api-observability-for-cloudflare-workers"&gt;blog post&lt;/a&gt; on the subject.&lt;/p&gt;

&lt;h1&gt;
  
  
  Conclusion and recommendations
&lt;/h1&gt;

&lt;p&gt;It took a little bit of effort to create an ideal development, delivery and operations experience for using Cloudflare Workers as an API. I think in total we spent 1-2 months exploring and implementing it, and at the end of that, we had a good slice of the API ready to go.&lt;/p&gt;

&lt;p&gt;My recommendation to Cloudflare would be to provide local development tooling that can be decoupled from wrangler and easily integrated into local development and debugging workflows. It would be useful to allow more complex route matching too.&lt;/p&gt;

&lt;p&gt;I love the simplicity of deploying Cloudflare Workers and the use cases they open up, due to their global scale and performance characteristics I think they’re perfect for so-called “wrapper” APIs, or abstraction layers, that enable you to mitigate vendor lock-in, plug feature gaps and allow you to augment the vendor offering, or even provide a short to long term migration strategy from a vendor based solution to a bespoke solution. You could even just use as a filter layer for authentication, authorisation and validation for other APIs, that would remove a lot of duplication and deployment trade-offs you get with some other API technologies.&lt;/p&gt;

&lt;p&gt;Edge network serverless computing could be the next big thing, but a major part of that is having global data persistence solutions. Not long after we’d completed our work on this, Cloudflare announced the &lt;a href="https://developers.cloudflare.com/workers/learning/using-durable-objects"&gt;“Durable Objects”&lt;/a&gt; beta, which is a new way of thinking about persistence, but a step in that direction. There are also services like &lt;a href="https://fauna.com/"&gt;Fauna&lt;/a&gt; emerging to offer solutions in that space. It’s exciting times for the way we think about cloud computing, I think the ultimate experience for cloud computing should be to simply deploy code to a cloud service and have it run performantly at scale and near your end-users without having to concern ourselves with choosing regions and the trade-offs in multi-region architectures. That's the dream, and I don't think we're very far away. &lt;/p&gt;

</description>
      <category>serverless</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
