<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Chris Dobson on Medium]]></title>
        <description><![CDATA[Stories by Chris Dobson on Medium]]></description>
        <link>https://medium.com/@chrd?source=rss-b0df41a007b6------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*p4ITji0Yl2CBqCM6</url>
            <title>Stories by Chris Dobson on Medium</title>
            <link>https://medium.com/@chrd?source=rss-b0df41a007b6------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 08 Apr 2026 16:57:56 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@chrd/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Workflow orchestration with Lambda Durable Functions — part 2]]></title>
            <link>https://awstip.com/workflow-orchestration-with-lambda-durable-functions-part-2-6b70f9a9e6ba?source=rss-b0df41a007b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/6b70f9a9e6ba</guid>
            <category><![CDATA[workflow]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[serverless]]></category>
            <dc:creator><![CDATA[Chris Dobson]]></dc:creator>
            <pubDate>Sun, 14 Dec 2025 18:32:18 GMT</pubDate>
            <atom:updated>2025-12-15T08:37:50.325Z</atom:updated>
            <content:encoded><![CDATA[<h3>Workflow orchestration with Lambda Durable Functions — part 2</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*URb_OOgRbYKMhH6BTFdUJA.png" /></figure><p><em>Having previously </em><a href="https://awstip.com/workflow-orchestration-with-lambda-durable-functions-part-1-3892a5a5a7aa"><em>created a Durable Function workflow</em></a><em> now it’s time to add some resiliency and have a look at testing.</em></p><h3>Replay</h3><p>While the workflow I created in the first article worked nicely it did have one issue.</p><p>I noticed when testing it out that the functions passed to the parallel step appeared to be consistently executing twice, once when I would have expected and then once more when a callback was received. This could be seen by something that had been confusing me when I looked at the execution log:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dUPdq1_pY_NAZ9wiGwBGpg.png" /></figure><p>The log above shows that the Callback has Succeeded but the Step had started which made no sense to me, prior to the callback being received the Step showed as Succeeded and the Callback as started which did make sense.</p><p>Looking in the CloudWatch logs prior to the function being executed a second time this platform.start line was in the log:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/920/1*SLpYGqVVqDc-TP1WvrLkzg.png" /></figure><p>After a bit of reading I realised this was due to the way that durable functions are executed, specifically the checkpoint and relay mechanism that is used. It looks like this is what was happening:</p><ul><li>The function hits a point where it needs to wait — waiting for callback in this case — the execution gets suspended.</li><li>The callback is received so the function needs to resume.</li><li>When resuming the function starts from the beginning and skips the first four steps as they have been completed.</li><li>As the ‘execute commands’ step hasn’t been completed it is executed again thereby executing all of the functions again.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/121/1*p4ihl-n5ibJJ5cosEx2jTg.png" /></figure><p>The way around this is to add some idempotency into the execution of those functions — this workflow is simulating a distributed system so hopefully there’d be some idempotency downstream but it seems a good idea to add some to the workflow as well.</p><p>The first step creates a unique processId and as this won’t be executed again this id can be used as an idempotency key. Using that I’ve added a check to each function using a very basic idempotency implementation in the executeOnce higher order function:</p><pre>const commandResults = await context.parallel(&quot;run commands&quot;, [<br>  async (parallelContext) =&gt; {<br>    await parallelContext.waitForCallback(<br>      &quot;command one&quot;,<br>      async (callbackId) =&gt; {<br>        await executeOnce(processId, &quot;commandOne&quot;, async () =&gt; commandOne(callbackId));<br>      },<br>      {<br>        timeout: { hours: 1 },<br>      }<br>    );<br>  },<br>  async (parallelContext) =&gt; {<br>    await parallelContext.waitForCallback(<br>      &quot;command two&quot;,<br>      async (callbackId) =&gt; {<br>        await executeOnce(processId, &quot;commandTwo&quot;, async () =&gt; commandTwo(callbackId));<br>      },<br>      {<br>        timeout: { hours: 1 },<br>      }<br>    );<br>  },<br>]);</pre><p>Once this was added the functions were no longer executed twice.</p><h3>Retries</h3><p>One thing I didn’t implement in the first article which I would usually want when orchestrating a workflow are retries. In a durable execution the step and waitForCallback methods allow a retry strategy to be declared which will be used whenever an exception is thrown.</p><p>A retry strategy is a function that is passed two parameters — the error that was thrown and the attempt number — and returns an object containing a shouldRetry property indicating whether or not to retry and duration which indicates how long to wait before retrying.</p><p>All but one of the steps in this workflow are using AWS services and I’ve given them a very simple strategy — try 3 times regardless of error and wait for 2 minutes longer every retry for a simple backoff. The strategy function looks like this:</p><pre>const strategy = (_: unknown, attempt: number) =&gt; ({<br>  shouldRetry: attempt &lt;= 3,<br>  delay: { minutes: attempt * 2 },<br>});</pre><p>The commandTwo function makes an HTTP request so I’ve made the retry strategy for that a little bit more complex by declaring a list of status codes that may be transient errors and only retrying if the status is one of those and also giving the service a bit more time to recover. So, for instance, if I receive a 400 or 404 there’s no point retrying as I will get exactly the same response. The code for that strategy function is here:</p><pre>const strategy = (error, attempt) =&gt; {<br>  const httpError = error as HttpError;<br>  return {<br>    shouldRetry: httpError.status<br>      ? transientErrorStatuses.includes(httpError.status) &amp;&amp; attempt &lt;= 5<br>      : attempt &lt;= 5,<br>    delay: { minutes: 5 * attempt },<br>  };<br>};</pre><p>A note about retries is that they do not apply to a failure being returned in a callback — I’m not sure whether I expected them to or not to be honest. So if a waitForCallback receives a failure notification then an exception is thrown regardless of any retry strategy.</p><h3>Parallel failures</h3><p>Turns out I missed a couple of things when first using the parallel method. I was testing for success by checking the status of each result like this:</p><pre>const failedCommands = <br>  commandResults.all.filter((result) =&gt; result.status === &quot;FAILED&quot;);<br>if (failedCommands.length) {<br>  throw new Error(`${processId} failed ${JSON.stringify(failedCommands, null, 2)}`);<br>}</pre><p>Which worked fine however there are better ways of doing this. The return from parallel includes a completionReason property which could be tested instead:</p><pre>if (commandResults.completionReason !== &quot;ALL_COMPLETED&quot;) {  <br>  throw new Error(`${processId} failed ${JSON.stringify(commandResults.failed(), null, 2)}`);<br>}</pre><p>Also there are failed() and succeeded() methods which return lists of the failed/successful items.</p><p>The completionReason property isn’t just a success or fail it can be one of ALL_COMPLETED , MIN_SUCCESSFUL_REACHED or FAILURE_TOLERANCE_EXCEEDED and this is because the parallel method allows a failure tolerance to be configured. The completionConfig property can be defined something like this:</p><pre>context.parallel(&quot;name&quot;, [.....], {<br>  minSuccessful: 2,<br>  toleratedFailureCount: 1,<br>  toleratedFailurePercentage: 20,<br>});</pre><p>Where minSuccessful is the minimum number of executions that can succeed for the step to be considered a success and the toleratedFailureCount and toleratedFailurePercentage are the number and percentage of failures tolerated respectively.</p><p>In this workflow I haven’t defined a completionConfig as I would like all of the executions to be successful.</p><h3>Testing</h3><p>One thing that can be difficult to do with orchestrations such as this is to test it locally but in the case of durable functions that has been made much easier. The <a href="https://www.npmjs.com/package/@aws/durable-execution-sdk-js-testing">@aws/durable-execution-sdk-js-testing</a> library is available and can be used to test the function both locally, and therefore in a CI environment, and test the deployed function in the AWS environment. I will be concentrating on the local testing in this article.</p><p>While I was expecting to be able to use it in <a href="https://www.npmjs.com/package/vitest">vitest</a> I had a problem with running it — I suspect there is some config I’ve missed and the fact that I gave up quickly is due to impatience on my part. So I installed <a href="https://www.npmjs.com/package/jest">jest</a> instead and things ran well — although as I needed support for ESM I had to use the --experimental-vm-modules node flag.</p><p>Firstly I wanted to test a happy path so therefore needed to create a test that could send callback notifications both from a single waitForCallback and two running in parallel. This turned out to be pretty simple.</p><p>Firstly I needed to add code to setup and teardown a test environment and create a local runner to execute the test:</p><pre>import { handler } from &quot;../index&quot;;<br><br>beforeAll(() =&gt; LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }));<br>afterAll(LocalDurableTestRunner.teardownTestEnvironment);<br><br>const runner = new LocalDurableTestRunner({ handlerFunction: handler });</pre><p>To execute the function I could now use runner.run() which returns a promise however I didn’t want to await it at that point as I needed to setup the various callbacks I needed first:</p><pre>const executionPromise = runner.run();<br><br>const approvalCallback = runner.getOperation(&quot;ask for approval&quot;);<br>await approvalCallback.waitForData(WaitingOperationStatus.STARTED);<br>approvalCallback.sendCallbackSuccess(JSON.stringify({ approved: true }));<br><br>const commandOneCallback = runner.getOperation(&quot;command one&quot;);<br>await commandOneCallback.waitForData(WaitingOperationStatus.STARTED);<br>commandOneCallback.sendCallbackSuccess();<br><br>const commandTwoCallback = runner.getOperation(&quot;command two&quot;);<br>await commandTwoCallback.waitForData(WaitingOperationStatus.STARTED);<br>commandTwoCallback.sendCallbackSuccess();<br><br>const execution = await executionPromise;</pre><p>Once the run() method has been called — but not awaited — I could then access the different operations that required callbacks, wait for them to start and then send a success notification. Once that was all setup I could await the promise and do some assertions.</p><p>As every operation can be accessed through the runner I could then check that each operation had done what I expected — for instance this code checks the create process operation returned the expected processId :</p><pre>const createProcessOperation = runner.getOperation(&quot;create process&quot;);<br>expect(createProcessOperation.getStepDetails()?.result).toBe(processId);</pre><p>Once the happy path was tested I wanted to test that any callbacks receiving a failure notification would fail the workflow. For instance to test a failure when asking for an approval:</p><pre>const approvalCallback = runner.getOperation(&quot;ask for approval&quot;);<br>await approvalCallback.waitForData(WaitingOperationStatus.STARTED);<br>approvalCallback.sendCallbackFailure();</pre><p>As well as testing for failure notifications I wanted to test that if any running processes didn’t end in time then also fail the workflow. The waitForCondition that checks for running processes makes 10 attempts with 10 minutes between each attempt — unsurprisingly I didn’t really want a test that lasted for 100 minutes! Because I setup the test environment using the skipTime option I didn’t have to — the read from the Dynamo table was mocked so I changed the mock to always return an item in the list and the test executes each attempt without having to wait. So the test looked like this:</p><pre>it(&quot;should fail if running processes do not complete&quot;, async () =&gt; {<br>  mockCreateProcess.mockResolvedValue(processId);<br>  mockFindRunningProcesses.mockResolvedValue([{ processId: &quot;1&quot; }]);<br><br>  const executionPromise = runner.run();<br><br>  const approvalCallback = runner.getOperation(&quot;ask for approval&quot;);<br>  await approvalCallback.waitForData(WaitingOperationStatus.STARTED);<br>  approvalCallback.sendCallbackSuccess(JSON.stringify({ approved: true }));<br><br>  const execution = await executionPromise;<br><br>  expect(execution.getStatus()).toBe(&quot;FAILED&quot;);<br>});</pre><p>Finally I wanted to test a retry strategy. This time using a mock to reject the promise first time and then in subsequent attempts resolve it meant that I could ensure that the step was re-tried correctly. This tests the retry strategy for the create process step:</p><pre>it(&quot;should complete if create process fails once&quot;, async () =&gt; {<br>  mockCreateProcess.mockRejectedValueOnce(new Error()).mockResolvedValue(processId);<br>  ...<br>  const executionPromise = runner.run();<br>  ...<br>  const execution = await executionPromise;<br><br>  expect(execution.getStatus()).toBe(&quot;SUCCEEDED&quot;);<br>});</pre><p><a href="https://github.com/ChrisDobby/durable-function-demo/blob/main/functions/workflow/src/__tests__/index.test.ts">This is the test suite I ended up with</a> — while it doesn’t test every single step thoroughly I was able to prove to myself that I would be able to if I had the time and inclination!</p><p>So I’ve ended up with a fairly resilient orchestration using Lambda Durable Functions that uses callbacks, runs processes in parallel and waits for a condition, which has a test suite running locally. Mission accomplished ✅</p><p>The final code for the workflow Lambda is <a href="https://github.com/ChrisDobby/durable-function-demo/blob/main/functions/workflow/src/index.ts">here</a> and is deployed using this <a href="https://github.com/ChrisDobby/durable-function-demo/blob/main/lib/durable-function-demo-stack.ts">CDK</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6b70f9a9e6ba" width="1" height="1" alt=""><hr><p><a href="https://awstip.com/workflow-orchestration-with-lambda-durable-functions-part-2-6b70f9a9e6ba">Workflow orchestration with Lambda Durable Functions — part 2</a> was originally published in <a href="https://awstip.com">AWS Tip</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Workflow orchestration with Lambda Durable Functions — part 1]]></title>
            <link>https://awstip.com/workflow-orchestration-with-lambda-durable-functions-part-1-3892a5a5a7aa?source=rss-b0df41a007b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/3892a5a5a7aa</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[workflow]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <dc:creator><![CDATA[Chris Dobson]]></dc:creator>
            <pubDate>Wed, 10 Dec 2025 09:35:35 GMT</pubDate>
            <atom:updated>2025-12-16T15:39:45.345Z</atom:updated>
            <content:encoded><![CDATA[<h3>Workflow orchestration with Lambda Durable Functions — part 1</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oyhFsCw35CprKgYTTMDzhQ.png" /></figure><p><em>AWS have recently released </em><a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html"><em>Lambda Durable Functions</em></a><em> which allow us to build multi-step workflows so I thought I’d give them a go and try and build one of these multi-step workflows. This is how I got on…</em></p><h3>The workflow</h3><p>The workflow I built was based on something I was planning on implementing in my job using Step Functions.</p><p>It’s a fairly simple flow for the time being — I need to ask a user for an approval and issue commands to other domains to execute long running processes and once they are completed the workflow is complete.</p><p>In addition as the processes that will be executing cause significant pressure downstream I only want to run one of the workflows at once.</p><p>Finally I want some record of the status of the process.</p><p>So the different steps are:</p><figure><img alt="Diagram of the steps described in the text above" src="https://cdn-images-1.medium.com/max/121/1*CqfkaSkhAYQISP6kSkiOeQ.png" /></figure><h3>Creating the function</h3><p>I created the function using node and Typescript so the first thing to do is install the <a href="https://www.npmjs.com/package/@aws/durable-execution-sdk-js">@aws/durable-execution-sdk-js</a> package — this package is available in the Lambda execution environment so there’s no need to bundle it with your deployment if you don’t wish.</p><p>Creating a durable function is a case of wrapping a handler with the withDurableExecutionhigher order function:</p><pre>export const handler = withDurableExecution(async (event, context) =&gt; {</pre><p>The event parameter is the same as for a regular Lambda — the event that triggered the function — while the context is a DurableContext which gives us access to the step-based workflow. In this article I will use the following methods of the context object:</p><ul><li>step</li><li>waitForCallback</li><li>waitForCondition</li><li>parallel</li></ul><p>So the first part of the workflow is to create a record in a Dynamo table for the process. This can be done inside a step:</p><pre>const processId = await context.step(&quot;create process&quot;, async (stepContext) =&gt; {<br>  stepContext.logger.info(&quot;creating new process&quot;);<br>  return await createProcess();<br>});</pre><p>The first parameter is the name of the step — all context methods I’ve used have this parameter — and the second is the function used to execute the step. This function is provided with a StepContext object which provides a logger as I’ve used in the code. The createProcess helper adds a record to the Dynamo process table and returns a process id and that becomes the return value of the step.</p><p>So I’ve now started the workflow by adding a process record to the table…</p><h3>Asking for user approval</h3><p>Next I need to seek approval from a user.</p><p>I’ve created a helper function — sendApproval — that will construct a URL including the detail needed to callback into the function and publishes it to an SNS topic. For the purpose of testing I can then subscribe my email address to the topic, click on the URL and approve the request.</p><p>I need to send that message and wait for the user to approve — to do this I use the waitForCallback method. This method accepts a function which gets passed a callback id which can then be used to make a callback to the function:</p><pre>const userApproval = await context.waitForCallback(<br>  &quot;ask for approval&quot;,<br>  async (callbackId) =&gt; {<br>    await sendApproval(callbackId);<br>  },<br>  {<br>    timeout: { hours: 1 },<br>  }<br>);</pre><p>In this case I’ve added a timeout of one hour meaning that my user has an hour to approve the request.</p><p>To callback to the function the Lambda sdk has two new commands added — SendDurableExecutionCallbackSuccessCommand and SendDurableExecutionCallbackFailureCommand — which take two parameters the callback id and an optional result. So to send a success notification back with a result indicating that a user approved the request would look something like this:</p><pre>lambdaClient.send(new SendDurableExecutionCallbackSuccessCommand({<br>  CallbackId: callbackId,<br>  Result: JSON.stringify({ success: true }),<br>})</pre><p>The result is then returned from waitForCallback - if a failure notification is sent or the request times out then an exception is thrown, which if not caught will cause the function to fail.</p><h3>Waiting for other workflows to complete</h3><p>Next I need to ensure that there are no processes currently running before I can start this one. To do this I need to scan the process table to check for anything with a status of in-progress, if there are none then the process can start, if there are then wait for a length of time and scan again until the process can start.</p><p>This can be done using the waitForCondition method — it takes parameters of the step name, a function to check the table and return a boolean indicating whether there are any currently running processes along with an object specifying the initial state and a wait strategy.</p><p>For this case I’ve set the initialState to false and a waitStrategy that will wait for 10 minutes between attempts and will try for 10 attempts before giving up:</p><pre>const haveAllProcessesFinished = await context.waitForCondition(<br>  &quot;check for running processes&quot;,<br>  async () =&gt; {<br>    const runningProcesses = await findRunningProcesses();<br>    return runningProcesses.length === 0;<br>  },<br>  {<br>  initialState: false,<br>    waitStrategy: (state, attempt) =&gt; ({<br>      shouldContinue: !state &amp;&amp; attempt &lt;= 10,<br>      delay: { minutes: 10 },<br>    }),<br>  }<br>);</pre><p>Unlike waitForCallback once waitStrategy.shouldContinue gets set to false no exception is thrown so I test the return value and throw an exception if the result is false (there are running processes):</p><pre>if (!haveAllProcessesFinished) {<br>  throw new Error(&quot;Processes not finished in a timely manner&quot;);<br>}</pre><p>Once there are no processes running I can use another step to update the process record to have a status of in-progress :</p><pre>await context.step(&quot;start process&quot;, async () =&gt; {<br>  await setProcessStatus(processId, &quot;in-progress&quot;);<br>});</pre><h3>Executing commands</h3><p>The main part of this workflow is the execution of two commands — I need to wait until both of these commands have completed before moving on. As previously I’ll be using the waitForCallback method to execute the commands and wait for a notification so the code for both would look like this:</p><pre>await context.waitForCallback(<br>  &quot;command one&quot;,<br>  async (callbackId) =&gt; {<br>    await commandOne(callbackId);<br>  },<br>  {<br>    timeout: { hours: 1 },<br>  }<br>);<br><br>await context.waitForCallback(<br>  &quot;command two&quot;,<br>  async (callbackId) =&gt; {<br>    await commandTwo(callbackId);<br>  },<br>  {<br>    timeout: { hours: 1 },<br>  }<br>);</pre><p>However I don’t want to have to execute these sequentially, ideally I’d like to execute them at the same time and move on once both have completed. Fortunately there is a parallel method that can be used for just that. As well as a name parameter I also pass an array of functions to be executed in parallel:</p><pre>const commandResults = await context.parallel(&quot;run commands&quot;, [<br>  async (parallelContext) =&gt; {<br>    await parallelContext.waitForCallback(<br>      &quot;command one&quot;,<br>      async (callbackId) =&gt; {<br>        await commandOne(callbackId);<br>      },<br>      {<br>        timeout: { hours: 1 },<br>      }<br>    );<br>  },<br>  async (parallelContext) =&gt; {<br>    await parallelContext.waitForCallback(<br>      &quot;command two&quot;,<br>      async (callbackId) =&gt; {<br>        await commandTwo(callbackId);<br>      },<br>      {<br>        timeout: { hours: 1 },<br>      }<br>    );<br>  },<br>]);</pre><p>Rather than use the function context parameter when inside a parallel method the functions get passed a separate DurableContext object.</p><p>When a waitForCallback either times out or receives a failure notification it will throw an exception, however when inside the parallel method the exception will be caught and returned along with other executions. So I’m checking the return value from the parallel call and looking for any with a status of FAILED — if there are any then throw an exception so that the function execution fails:</p><pre>const failedCommands = commandResults.all.filter((result) =&gt; result.status === &quot;FAILED&quot;);<br>if (failedCommands.length) {<br>  throw new Error(`${processId} failed ${JSON.stringify(failedCommands, null, 2)}`);<br>}</pre><p>Once the commands have been successfully executed the workflow is complete.</p><p>I’ve mentioned a few times that throwing an exception will fail the execution of the function which is what I want, however before completing the execution I need to change the status of the process in the table so that any processes waiting to start will be able to. To do this, as you would expect, I catch the exception, then set the status in the process table to failed and rethrow the exception:</p><pre>catch (error) {<br>  context.logger.error(error);<br>  await setProcessStatus(processId, &quot;failed&quot;);<br>  throw error;<br>}</pre><h3>Deployment</h3><p>I’ve deployed this demo using CDK — a new durableConfig property has been added to the Lambda types. In this case I’ve set the execution timeout to 2 days which, given that everything times out after an hour, will be far too long and the retention period for the execution logs to 14 days:</p><pre>durableConfig: {<br>  executionTimeout: cdk.Duration.days(2),<br>  retentionPeriod: cdk.Duration.days(14),<br>}</pre><h3>Executions</h3><p>When a Lambda is configured as a durable function you get a new tab called Durable Executions which shows all executions and when an execution is selected all steps inside that execution:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/514/1*xUU2eUlx8q20ZBYrWfMbLg.png" /></figure><p>This shows a list of executions for the function:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KGoGdpo7o_mZYaplRzHwEA.png" /></figure><p>Clicking into an execution gives a list of steps that were executed:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ZumU3aWxTE4BI7-KmdmPaQ.png" /></figure><p>Mostly I’ve found this pretty good sometimes, especially when executing in parallel it seems to be incorrect while the execution is going on but once it’s complete then is correct.</p><p>The code for this function can be found <a href="https://github.com/ChrisDobby/durable-function-demo/blob/part-1/functions/workflow/src/index.ts">here</a> with the CDK <a href="https://github.com/ChrisDobby/durable-function-demo/blob/part-1/lib/durable-function-demo-stack.ts">here</a> — the deployment also includes components that mock the callbacks and allow it to be executed.</p><p>I have now built a nice workflow — in <a href="https://awstip.com/workflow-orchestration-with-lambda-durable-functions-part-2-6b70f9a9e6ba">part 2</a> I will look at the real power of durable functions and add in some resilience with retries and replies as well as looking at testing the function.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3892a5a5a7aa" width="1" height="1" alt=""><hr><p><a href="https://awstip.com/workflow-orchestration-with-lambda-durable-functions-part-1-3892a5a5a7aa">Workflow orchestration with Lambda Durable Functions — part 1</a> was originally published in <a href="https://awstip.com">AWS Tip</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Localstack in VSCode]]></title>
            <link>https://medium.com/@chrd/localstack-in-vscode-a8a9e8d53d10?source=rss-b0df41a007b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/a8a9e8d53d10</guid>
            <category><![CDATA[localstack]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[aws-step-functions]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <dc:creator><![CDATA[Chris Dobson]]></dc:creator>
            <pubDate>Sun, 14 Sep 2025 14:38:54 GMT</pubDate>
            <atom:updated>2025-09-14T14:38:54.072Z</atom:updated>
            <content:encoded><![CDATA[<h3>LocalStack in VSCode</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/400/1*A0QBlVRnfGG-yMgvMRsHMQ.png" /></figure><p><em>Developing using LocalStack from within VSCode just got a lot easier with the latest update to the AWS Toolkit adding support for LocalStack profiles.</em></p><p>In a <a href="https://medium.com/aws-tip/a-local-lambda-development-environment-b34929eddecb">previous post</a> I detailed an environment I’ve been using for local development using <a href="https://www.localstack.cloud/">LocalStack</a>.</p><p>One of the tools I mentioned was the <a href="https://aws.amazon.com/visualstudiocode/">AWS Toolkit</a> which at the time was tricky to configure to use <a href="https://www.localstack.cloud/">LocalStack</a>. To use the toolkit with <a href="https://www.localstack.cloud/">LocalStack</a> meant editing the settings file to add a line for every service (some of which didn’t work) and, of course, to go back to AWS environment remove those lines. I also couldn’t get Lambda remote debugging to work (although this could easily have been me!)</p><p>Well the toolkit has had an update introducing direct support for <a href="https://www.localstack.cloud/">LocalStack</a> meaning that a single operation can switch between AWS and local environments.</p><h3>Setting up LocalStack in the toolkit</h3><p>When the latest version of the toolkit extension has been installed select ‘Walkthrough of Application Builder’</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/652/1*OoRhkaKqHDNBGKqlo1wBmA.png" /></figure><p>there will be an option to install <a href="https://www.localstack.cloud/">LocalStack</a></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xmwzZ6qVTI2yIRywdNuGiQ.png" /></figure><p>This will open a dialogue</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/906/1*3lRHoJk3q2yqT90FdhXfEg.png" /></figure><p>Setup will authenticate with <a href="https://www.localstack.cloud/">LocalStack</a> and install the <a href="https://aws.amazon.com/visualstudiocode/">AWS Toolkit</a> integration.</p><p>Once that’s installed <a href="https://www.localstack.cloud/">LocalStack</a> will appear in the VSCode status bar.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/330/1*uH-Sn0I65IJeB4SMOnJOUA.png" /></figure><p>Clicking on that will give the option to start <a href="https://www.localstack.cloud/">LocalStack</a> from within VSCode. When starting <a href="https://www.localstack.cloud/">LocalStack</a> your license key is retrieved from your account so that you are running the correct version.</p><p>One thing I did notice is when using <a href="https://rancherdesktop.io/">Rancher</a> instead of <a href="https://www.docker.com/">Docker</a> the extension didn’t update the status of <a href="https://www.localstack.cloud/">LocalStack</a> to started — I had to restart VSCode to be able to use <a href="https://www.localstack.cloud/">LocalStack</a> in the toolkit. I believe this has something to do with <a href="https://rancherdesktop.io/">Rancher</a> implementing notifications differently so on the odd occasion I can’t use <a href="https://www.docker.com/">Docker</a> then I simply start LocalStack from the cli before starting VSCode.</p><p>Once <a href="https://www.localstack.cloud/">LocalStack</a> is running the status bar updates</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/616/1*_sqOASAEwMCkwQIZPwrvlw.png" /></figure><p>And there is a <a href="https://www.localstack.cloud/">LocalStack</a> profile available to use with the toolkit — click on the current profile in the status bar to get a list of available profiles</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5isp-zCvRAK22A1QbNRXag.png" /></figure><p>Select the profile and the toolkit will be pointing to your <a href="https://www.localstack.cloud/">LocalStack</a> instance.</p><h3>Lambda invocation and debugging</h3><p>There are a few ways to add a Lambda function into your <a href="https://www.localstack.cloud/">LocalStack</a> instance and have it available for debugging</p><ul><li><strong>Create a new SAM Application</strong></li></ul><p>A new SAM application can be created from within the toolkit. A right-click on ‘Lambda’ in the toolkit explorer opens a menu containing ‘Create Lambda SAM Application’</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aAttr-bL8gaOPvZtd4PWCw.png" /></figure><p>Selecting this will go through a number of options eventually creating a new SAM application containing a Lambda function.</p><p>Once this has been created it can be deployed to <a href="https://www.localstack.cloud/">LocalStack</a> from the toolkit ‘Application Builder’. Find the application in the list and right-click to get a menu</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/832/1*QlAQvyrnu4RhD9VDfzhV0A.png" /></figure><p>Select ‘Deploy SAM Application’ to deploy your Lambda to <a href="https://www.localstack.cloud/">LocalStack</a></p><ul><li><strong>Copy a function from your AWS environment</strong></li></ul><p>Something I’ve found useful is the ability to copy a Lambda function from my AWS environment into <a href="https://www.localstack.cloud/">LocalStack</a> relatively quickly.</p><p>To do this ensure the toolkit is using your AWS profile, find the Lambda you want to copy in the explorer and right-click you will see this menu</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/832/1*aBkzMAfDXahZgE5ggeYpPg.png" /></figure><p>Select ‘Convert to SAM Application’ to create a new Lambda SAM Application from the function.</p><p>Once complete switch the toolkit to use your <a href="https://www.localstack.cloud/">LocalStack</a> profile and the application can be deployed in the same way as a newly created one.</p><ul><li><strong>Create a Lambda from CDK</strong></li></ul><p><a href="https://medium.com/aws-tip/a-local-lambda-development-environment-b34929eddecb">This blog post</a> looked at a setup I’ve been using to develop locally — using CDK in that way (or any other way) to deploy a Lambda to <a href="https://www.localstack.cloud/">LocalStack</a> will make it available for remote debugging.</p><p>Once a Lambda function has been added to <a href="https://www.localstack.cloud/">LocalStack</a> it can be invoked using the option in the toolkit.</p><p>Find the Lambda in the toolkit and select the ‘Invoke Remotely’ option</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/786/1*-0bvGTSkGH3bZR3kE-2BQA.png" /></figure><p>Opens the ‘Remote invoke configuration’ window</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6mJ1WkEQpUtlQm_TshWt-g.png" /></figure><p>Use that to invoke and debug the function as required.</p><h3>Step functions</h3><p>This update has also made developing and testing Step Functions locally significantly easier.</p><p>The Step Functions visual designer has been available in the toolkit for a while and is excellent for creating and editing from within VSCode and now it can be used to deploy and test in <a href="https://www.localstack.cloud/">LocalStack</a>.</p><p>Simply select the <a href="https://www.localstack.cloud/">LocalStack</a> profile for the <a href="https://aws.amazon.com/visualstudiocode/">AWS toolkit</a>, go to ‘Step Functions’ in the explorer and either right-click to create a new state machine or find the state machine you want to work with in the list, right-click and select ‘Open with Workflow Studio` to open up the designer.</p><p>Once finished designing the ‘Save &amp; Deploy’ option can be used to either create a new state machine or update and existing one in <a href="https://www.localstack.cloud/">LocalStack</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Mer9XapVOTSMDBK7qVB4cw.png" /></figure><p>One thing I’ve found really useful is the ability to easily copy an existing state machine from my AWS environment, deploy it to <a href="https://www.localstack.cloud/">LocalStack</a> to test and edit locally.</p><p>To do that ensure that the toolkit is using the AWS environment, find the state machine in the explorer, right-click and select ‘Download Definition’.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/902/1*7QAUaqCucDMuHjHoZCP1Fg.png" /></figure><p>This will download the definition of the state machine into VSCode, select the definition to open it in the designer, switch profiles so that the toolkit is using <a href="https://www.localstack.cloud/">LocalStack</a> and deploy it. The state machine can then be edited and deployed through the designer at will and, if required, it can be deployed back to the AWS environment by switching profiles back.</p><p>All in all so far I’ve found the new <a href="https://www.localstack.cloud/">LocalStack</a> integration a useful addition to my toolkit, especially the ability to quickly switch between environments to copy resources to <a href="https://www.localstack.cloud/">LocalStack</a> from AWS.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a8a9e8d53d10" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A local Lambda development environment]]></title>
            <link>https://awstip.com/a-local-lambda-development-environment-b34929eddecb?source=rss-b0df41a007b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/b34929eddecb</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[localstack]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <dc:creator><![CDATA[Chris Dobson]]></dc:creator>
            <pubDate>Sun, 20 Jul 2025 10:02:37 GMT</pubDate>
            <atom:updated>2025-07-28T15:54:02.924Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AcH_eBqwmL-s5VQUOIMc9g.png" /></figure><p><em>Sharing some ways in which I streamline my workflow and speed up the feedback loop when developing for AWS Lambda</em></p><p>Two tools I use when developing for AWS serverless in general are <a href="https://www.localstack.cloud/">LocalStack</a> and the <a href="https://aws.amazon.com/visualstudiocode/">AWS Toolkit for VSCode</a> which has recently been updated to include new features making it more compelling to use with Lambda.</p><p>The main way I speed up my feedback loop when developing in AWS Lambda is by using <a href="https://www.localstack.cloud/">LocalStack</a>. LocalStack is essentially an AWS environment running locally on your machine and I find it really useful for quickly testing many things not just Lambda functions.</p><p>It has a free tier which gives access to everything I’ve used in this article so go and sign up!</p><h3>Setup LocalStack</h3><p>Once <a href="https://app.localstack.cloud/sign-up">signed up with LocalStack</a> getting going is pretty simple.</p><p>LocalStack runs in a docker container and once you’ve followed the steps in the <a href="https://app.localstack.cloud/getting-started">Getting Started guide</a> you should be up and running. Once everything’s running the <a href="https://app.localstack.cloud/inst/default/resources">LocalStack console</a> should look something like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9HjWDjmsyQBBTIuGgg13ww.png" /></figure><p>To demonstrate a typical setup I use when developing I’m going to go through how I’d create a simple setup containing an SQS queue, a Lambda function and an S3 bucket. Messages on the SQS trigger the Lambda which writes the message that was received into the bucket.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/559/1*BTWJxqMkkJSWabINgRRBqA.png" /></figure><h3>Automate deployment to LocalStack</h3><p>Generally I would be deploying code using <a href="https://www.npmjs.com/package/aws-cdk">CDK</a> so, given that <a href="https://www.npmjs.com/package/aws-cdk">CDK</a> is installed, I would <a href="https://www.npmjs.com/package/aws-cdk#cdk-init">initialise a new CDK project</a> — in my case I would usually be using Typescript as the language:</p><pre>cdk init --language typescript</pre><p>and then create the three components and necessary permissions in the CDK stack:</p><pre>const bucket = new Bucket(this, &quot;automated-bucket&quot;, {<br>  bucketName: &quot;automated-bucket&quot;,<br>  versioned: true,<br>  removalPolicy: cdk.RemovalPolicy.DESTROY,<br>})<br><br>const sqs = new Queue(this, &quot;automated-queue&quot;, {<br>  queueName: &quot;automated-queue&quot;,<br>  visibilityTimeout: cdk.Duration.seconds(300),<br>  retentionPeriod: cdk.Duration.days(4),<br>  removalPolicy: cdk.RemovalPolicy.DESTROY,<br>})<br><br>const lambda = new NodejsFunction(this, &quot;automated-lambda&quot;, {<br>  entry: &quot;functions/test/src/index.ts&quot;,<br>  handler: &quot;handler&quot;,<br>  runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,<br>  environment: {<br>    BUCKET_NAME: bucket.bucketName,<br>  },<br>})<br><br>lambda.addEventSource(<br>  new SqsEventSource(sqs, {<br>    batchSize: 10,<br>  })<br>)<br><br>sqs.grantConsumeMessages(lambda)</pre><p>If I wanted to deploy this to an AWS environment I would use the following CDK commands:</p><pre>cdk bootstrap<br>cdk deploy</pre><p>To deploy this to the LocalStack environment I install <a href="https://www.npmjs.com/package/aws-cdk-local">CDK Local</a> which is a wrapper around CDK that executes the commands on LocalStack. So to bootstrap and deploy:</p><pre>cdklocal bootstrap<br>cdklocal deploy</pre><p>Now everything is deployed to LocalStack which is great, however I’d rather not run a cdklocal command manually every time I’ve updated anything. To automate this deployment I use <a href="https://www.npmjs.com/package/nodemon">nodemon</a> to watch for any changes in the stack and run the deploy command. To do that I use the following nodemon config file:</p><pre>{<br>  &quot;exec&quot;: &quot;cdklocal deploy --require-approval never&quot;,<br>  &quot;watch&quot;: [<br>    &quot;lib&quot;,<br>    &quot;functions&quot;<br>  ],<br>  &quot;ext&quot;: &quot;ts&quot;<br>}</pre><p>This configuration watches for changes in the lib directory which contains the CDK stack and the functions directory which contains the Lambda function and if any changes are made to ts files then runs cdklocal deploy --require-approval never . Approvals are turned off as I don’t want to have to approve any security related changes while I’m developing — I just want to save the file and it gets deployed. This is a local development environment so there shouldn’t be any problems 🤞</p><p>Now if I execute nodemon in the root of the project whenever I make any changes to either the stack or the Lambda function they will be deployed to LocalStack for me to test.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2t6p2pRd6c5P6VHmZ_FZdA.png" /></figure><p>This repository setup is <a href="https://github.com/ChrisDobby/local-lambda-dev-setup/tree/cdk-deployment">here</a>.</p><p>This is great and all changes get deployed ready for test however, as you can see from the screenshot, there is a delay of a few seconds especially when changing the Lambda. Given that that’s the component that would be changed most often when developing it would be good to get rid of the delay if possible…</p><h3>Lambda hot reload in LocalStack</h3><p>Well it is possible, using LocalStack’s Lambda <a href="https://docs.localstack.cloud/aws/tooling/lambda-tools/hot-reloading/">hot reload</a> 🔥</p><p>To use hot reloading create a Lambda to retrieve it’s code from an S3 bucket called hot-reload with a key of the path to the code on your local machine, LocalStack will then update the function every time there’s a change to files in the specified path.</p><p>Note — there’s no need to actually create the hot-reload bucket.</p><p>To use this I would usually add the Lambda into the CDK stack using hot reload:</p><pre>   new Function(this, &quot;hot-reloading-function&quot;, {<br>      runtime: cdk.aws_lambda.Runtime.NODEJS_22_X,<br>      functionName: &quot;hot-reloading-function&quot;,<br>      handler: &quot;index.handler&quot;,<br>      code: Code.fromBucket(<br>        Bucket.fromBucketName(this, &quot;hot-reload&quot;, &quot;hot-reload&quot;),<br>        &quot;path-to-repository/functions/test/dist&quot;<br>      ),<br>    });ty</pre><p>Note the name of the bucket and the key in the call to Code.fromBucket .</p><p>Then remove the functions directory from the nodemon config so that it only deploys when the stack is changed:</p><pre>{<br>  &quot;exec&quot;: &quot;cdklocal deploy --require-approval never&quot;,<br>  &quot;watch&quot;: [<br>    &quot;lib&quot;<br>  ],<br>  &quot;ext&quot;: &quot;ts&quot;<br>}</pre><p>Add another nodemon config into the Lambda directory that builds it whenever it’s changed:</p><pre>{<br>  &quot;exec&quot;: &quot;npm run build&quot;,<br>  &quot;ext&quot;: &quot;ts&quot;<br>}</pre><p>Then run nodemon into the Lambda directory as well as in the root of the project so that if the function code is changed the Lambda is hot reloaded while if the stack is changed then a full deploy happens.</p><p>This repository setup is <a href="https://github.com/ChrisDobby/local-lambda-dev-setup/tree/hot-reload-deployment">here</a>.</p><h3>AWS Toolkit for VSCode</h3><p>As a VSCode user I also often use the <a href="https://aws.amazon.com/visualstudiocode/">AWS Toolkit for VSCode</a> for a number of things — especially the local Workflow Studio for editing Step Functions.</p><p>Recently (17th July 2025) AWS released an <a href="https://aws.amazon.com/blogs/aws/simplify-serverless-development-with-console-to-ide-and-remote-debugging-for-aws-lambda/">update</a> that adds two new features that I would expect to become very useful when developing for Lambda:</p><ul><li>Console to IDE integration which allows you to open a Lambda function in VSCode directly from the AWS console and then deploy any changes directly from the IDE.</li><li>Remote debugging which allows you to debug a Lambda function that is running in an AWS environment from within VSCode</li></ul><p>Both features are pretty simple to use…</p><h3>Console to IDE integration</h3><p>There is now a new button on the Lambda console — Open in Visual Studio Code</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*vsdsqhbq6pZZbqsKZ2VlHQ.png" /></figure><p>which does exactly what it says and allows you to open and edit the function in the IDE and gives the option to deploy when changes have been made.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/870/1*o0zitTNcTfghOBTSSSIpzA.png" /></figure><p>This can also be initiated from within the IDE — find the function you want to edit in the Explorer and select the ‘Download…’ option, the function is then downloaded and can be edited and deployed.</p><h3>Remote debugging</h3><p>Once the Lambda is available in the IDE which can be done using either of the two techniques above it can be debugged remotely. To do this right-click on the Lambda in the explorer and select ‘Invoke Remotely’, or click the arrow button, the ‘Remote invoke configuration’ dialog is opened.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RNKVjdyRU8bIqc9x7jj2vg.png" /></figure><p>This dialog includes a checkbox for ‘Remote debugging’, tick that box, add a breakpoint into the function code, hit the ‘Remote invoke’ button and the Lambda will be invoked in the AWS environment but the breakpoint you’ve added will be hit in the IDE 🎉</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1016/1*ojZi84SXBRyEahHmCjirLQ.png" /></figure><h3>Use AWS Toolkit with LocalStack</h3><p>Wouldn’t it be cool if these two new Lambda features could be used within LocalStack?</p><p>Well, there’s good news 😃 and bad news 😦!</p><p>The good news is that the IDE integration seems to work, however I’ve not been able to get the remote debugging to work as yet (if I do I’ll update this post!)</p><p>It’s a little bit fiddly to setup as the toolkit doesn’t expose any configuration to use different endpoints so you need to edit the VSCode settings.json and add the following:</p><pre>&quot;aws.dev.endpoints&quot;: {<br>  &quot;lambda&quot;: &quot;http://localhost.localstack.cloud:4566&quot;<br>}</pre><p>this tells the toolkit to use the LocalStack endpoint for Lambda meaning that the IDE integration can be used on LocalStack as well.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b34929eddecb" width="1" height="1" alt=""><hr><p><a href="https://awstip.com/a-local-lambda-development-environment-b34929eddecb">A local Lambda development environment</a> was originally published in <a href="https://awstip.com">AWS Tip</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Keeping supporters up to date using AWS Events and Web Push (part 4)]]></title>
            <link>https://medium.com/@chrd/keeping-supporters-up-to-date-using-aws-events-and-web-push-part-4-bac616cfaf35?source=rss-b0df41a007b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/bac616cfaf35</guid>
            <category><![CDATA[event-driven-architecture]]></category>
            <category><![CDATA[serverless-architecture]]></category>
            <category><![CDATA[web-push-notifications]]></category>
            <category><![CDATA[cricket]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Chris Dobson]]></dc:creator>
            <pubDate>Sun, 23 Feb 2025 14:54:35 GMT</pubDate>
            <atom:updated>2025-02-23T14:54:35.503Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JA0ejw7JW_WfC9OaK7RHEw.png" /></figure><p><em>Adding Web Push notifications</em></p><p>This is the final of a series of articles showing how I built a solution to keep supporters of a sports club up to date using an event-driven architecture in AWS and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API"><strong>Web Push</strong></a> notifications.</p><p>Previous articles:</p><ul><li><a href="https://chrisdobby.dev/keeping-supporters-up-to-date-using-aws-events-and-web-push-part-1-98bdb8b1a29e"><strong>Part 1</strong></a></li><li><a href="https://chrisdobby.dev/keeping-supporters-up-to-date-using-aws-events-and-web-push-part-2-f8d93c356c41"><strong>Part 2</strong></a></li><li><a href="https://chrisdobby.dev/keeping-supporters-up-to-date-using-aws-events-and-web-push-part-3-0846e9abfebf"><strong>Part 3</strong></a></li></ul><h3>Goals</h3><ul><li>Keep supporters up to date as much as possible during the games</li><li>Use an event-driven architecture in AWS</li><li>Require no manual intervention (I’ve better things to do at the weekend)</li><li>Minimal cost as it’s running in my AWS account</li></ul><h3>Part 4</h3><p>Now that the latest score updates are being published to an SNS topic I want to push notifications to any devices that have subscribed to receive them.</p><p>No one wants to receive push notifications twice a minute so I decided to only send notifications for the following:</p><ul><li>Every 10 overs</li><li>Every wicket</li><li>Every player milestone</li><li>The result</li></ul><p>I also needed a way for users to register/unregister to receive these notifications.</p><h3>Subscription API</h3><p>To create and delete subscriptions I use an API Gateway <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html">HTTP API</a> with a single route with POST, PUT and DELETE methods which expect a body which is validated using this <a href="https://github.com/colinhacks/zod">Zod</a> schema:</p><pre>import { z } from &#39;zod&#39;;<br><br>const SubscriptionSchema = z.object({<br>  endpoint: z.string(),<br>  keys: z.object({<br>    p256dh: z.string(),<br>    auth: z.string(),<br>  }),<br>});</pre><p>I created a new Dynamo table to store the subscriptions with a partition key of the endpoint URL and the POST and PUT methods make a PUT request to the table passing the validated body.</p><p>The DELETE method makes a request to the table to delete the endpoint URL passed as a parameter.</p><p>The methods are all integrated with a single Lambda which uses the httpMethod property of the event:</p><pre>export const handler = async ({ body, httpMethod }) =&gt; {<br>  const validateResult = validateSubscription(JSON.parse(body));<br>  if (!validateResult.success) {<br>    return { statusCode: 400, body: JSON.stringify(validateResult.error) };<br>  }<br><br>  const { data: subscription } = validateResult;<br>  switch (httpMethod) {<br>    case &#39;POST&#39;:<br>      await subscribe(subscription);<br>      break;<br>    case &#39;DELETE&#39;:<br>      await unsubscribe(subscription.endpoint);<br>      break;<br>    case &#39;PUT&#39;:<br>      await update(subscription);<br>      break;<br>  }<br>  return { statusCode: 200 };<br>};</pre><p>Finally, I wanted some security on this API and decided a simple API key would suffice. Each route is secured using the same simple <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html">authoriser Lambda</a> which tests the header against the expected key:</p><pre>export const handler = async ({ headers: { authorization } }) =&gt;<br>    ({ isAuthorized: authorization === process.env.API_KEY });</pre><h3>Creating notifications</h3><p>As previously mentioned I didn’t want to send every single update so I needed to create a buffer that received the updates from the SNS and only sent a web push notification when needed.</p><p>I created a Lambda that subscribes to the SNS topic, compares the data with what was sent for the previous notification and, if a new notification is due, creates that notification and adds it to an SQS. The code that checks whether a notification is required is <a href="https://github.com/ChrisDobby/live-scores/blob/2a468c1cffd0f419130c6fc67ff260f4b6b292e0/packages/functions/push-notify/src/updates.ts#L112">here</a>.</p><p>Once I had created the SQS for sending notifications I decided it would be useful to send a confirmation notification whenever a new subscription is received so I updated the Subscription API to put a notification onto the queue. This way a user can confirm everything is working as it should when they subscribe, rather than realising no updates are coming through during a game.</p><h3>Sending notifications</h3><p>To use web push I need to create a Vapid key-pair. To do this I used this <a href="https://vapidkeys.com/">tool</a>.</p><p>To send the notifications I used the <a href="https://www.npmjs.com/package/web-push">web-push library</a> and get the Vapid details from the environment:</p><pre>const vapidSubject = `${process.env.VAPID_SUBJECT}`;<br>const vapidPublicKey = `${process.env.VAPID_PUBLIC_KEY}`;<br>const vapidPrivateKey = `${process.env.VAPID_PRIVATE_KEY}`;<br><br>webpush.setVapidDetails(vapidSubject, vapidPublicKey, vapidPrivateKey);</pre><p>Then I needed to get the subscriptions from the Dynamo table, I did this using a Scan which should be fine for the foreseeable future but for a large amount of subscriptions may not scale very well — will cross this bridge if I get there.</p><p>Once I have all of the subscriptions I can send them using sendNotification:</p><pre>await webpush.sendNotification(<br>    subscription,<br>    JSON.stringify({ title: &#39;Hello&#39;, body: &#39;Hello from live-scores&#39; })<br>);</pre><h3>Expired subscriptions</h3><p>Although there is an API call that can be made to delete a subscription sometimes that might not be called when a subscription is disabled or the subscription may expire.</p><p>When the sendNotification is called for an expired subscription an exception is thrown. As these subscriptions cannot become active again (as far as I know) then it makes sense to delete them from the table so as not to waste resources trying to send to them every time.</p><p>As I already had the code written to delete a subscription from the API it made sense to try and re-use it. Rather than call the API for each expired subscription I decided to create an SQS queue with a new Lambda to read from it that deletes the subscription from the table. Both the API DELETE method and the code that handles expired subscriptions then put a message on the queue:</p><pre>import webpush, { PushSubscription, WebPushError } from &#39;web-push&#39;;<br><br>const send = (removeSubscription: RemoveSubscription, notification: string) =&gt; async (subscription: PushSubscription) =&gt; {<br>  try {<br>    await webpush.sendNotification(subscription, notification);<br>  } catch (e: unknown) {<br>    if (e instanceof WebPushError &amp;&amp;<br>        (e.body.includes(&#39;unsubscribed&#39;) || e.body.includes(&#39;expired&#39;))) {<br>      removeSubscription(subscription.endpoint);<br>    } else {<br>      console.error(e);<br>    }<br>  }<br>};</pre><p>The architecture looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/604/1*AUC3-FuoyMBBzpX7qrK-Gg.png" /></figure><h3>Subscribing/unsubscribing from a web client</h3><p>To use web push from the client I first needed to check that it is supported in the browser which I did using this expression &#39;serviceWorker&#39; in navigator &amp;&amp; &#39;PushManager&#39; in window &amp;&amp; &#39;showNotification&#39; in ServiceWorkerRegistration.prototype</p><p>Once I’ve established that it is supported I use PushManager.subscribe passing in the public Vapid key which will create a new subscription object for the website. To retrieve an existing subscription I use PushManager.getSubscription. This object matches the <a href="https://github.com/colinhacks/zod">Zod</a> schema that it used to validate the subscription API calls.</p><p>Then it’s a question of either making a POST or DELETE request to the API depending on whether the subscription is being created or deleted. The code I use to subscribe from a Remix website is <a href="https://github.com/ChrisDobby/cleckheaton-cc/blob/ba9ce60f839ca7b666c101ff064688008acb69a3/web/app/components/subscription.tsx#L93">here</a>.</p><p>This also does some extra checks when running in IOS as if the os version is 16.4 or over and web push is not supported then if the user adds the site to their home screen then it should be available.</p><p>The code for this article can be found on <a href="https://github.com/ChrisDobby/live-scores/tree/blog-part-4"><strong>this branch</strong></a> and the full service is <a href="https://github.com/ChrisDobby/live-scores"><strong>here</strong></a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bac616cfaf35" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Keeping supporters up to date using AWS Events and Web Push (part 3)]]></title>
            <link>https://medium.com/@chrd/keeping-supporters-up-to-date-using-aws-events-and-web-push-part-3-0846e9abfebf?source=rss-b0df41a007b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/0846e9abfebf</guid>
            <category><![CDATA[aws-lambda]]></category>
            <category><![CDATA[cricket]]></category>
            <category><![CDATA[event-driven-architecture]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Chris Dobson]]></dc:creator>
            <pubDate>Sun, 09 Feb 2025 14:10:37 GMT</pubDate>
            <atom:updated>2025-02-09T14:10:37.366Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JA0ejw7JW_WfC9OaK7RHEw.png" /></figure><p><em>Adding WebSocket push notifications</em></p><p>This is part 3 of a series of articles showing how I built a solution to keep supporters of a sports club up to date using an event-driven architecture in AWS and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API">Web Push</a> notifications.</p><p>Previous articles:</p><ul><li><a href="https://chrisdobby.dev/keeping-supporters-up-to-date-using-aws-events-and-web-push-part-1-98bdb8b1a29e">Part 1</a></li><li><a href="https://chrisdobby.dev/keeping-supporters-up-to-date-using-aws-events-and-web-push-part-2-f8d93c356c41">Part 2</a></li></ul><h3>Goals</h3><ul><li>Keep supporters up to date as much as possible during the games</li><li>Use an event-driven architecture in AWS</li><li>Require no manual intervention (I’ve better things to do at the weekend)</li><li>Minimal cost as it’s running in my AWS account</li></ul><h3>Part 3</h3><p>Now that the latest score updates are being published to an SNS topic I needed to update the club website through a <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API">WebSocket</a> to keep the score page up-to-date. Before being able to send the updates though there needed to be a way for the page to connect to the <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API">WebSocket</a>.</p><h3>Creating a WebSocket API</h3><p>First I needed to use API Gateway to create a <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html">WebSocket</a> API. In this project the infrastructure is created using <a href="https://www.terraform.io/">Terraform</a> and the code to create the API is <a href="https://github.com/ChrisDobby/live-scores/blob/13d76de8bb3ba3f12eec9f2a869bc3bf5d2d0fc8/terraform/api-gateway.tf#L2">here</a> but, as always, a <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api.html">WebSocket API</a> can also be created through the <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-create-empty-api.html">AWS console or CLI</a>.</p><p>In this instance, the socket connection was only going to be used to send messages, not receive anything, so I only needed the $connect and $disconnect routes.</p><h3>Connecting</h3><p>I added the $connect route and integration with a Lambda, socket-connect, when the $connect route is selected in the console the detail looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aFhqGA3jF-lMD-ToCT2HAQ.png" /></figure><p>The socket-connect Lambda receives an event as a parameter that includes a requestContext.connectionId value which is the unique id that can be used to send a message on the <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API">WebSocket</a>. I created a new Dynamo table to store the connection ids as its partition key along with a TTL of 24 hours to keep the table clean.</p><p>The code for this Lambda is fairly straightforward, it gets the connectionId and performs a PUT on the Dynamo table of the value along with the expiry field which is configured as the TTL:</p><pre>import { DynamoDBClient } from &#39;@aws-sdk/client-dynamodb&#39;;<br>import { DynamoDBDocumentClient, PutCommand } from &#39;@aws-sdk/lib-dynamodb&#39;;<br><br>const client = new DynamoDBClient({});<br>const documentClient = DynamoDBDocumentClient.from(client);<br>const TableName = &#39;cleckheaton-cc-live-score-connections&#39;;<br>export const handler = async event =&gt; {<br>  const { connectionId } = event.requestContext;<br>  await documentClient.send(<br>    new PutCommand({<br>      TableName,<br>      Item: {<br>        connectionId,<br>        expiry: Math.floor(Date.now() / 1000) + 24 * 60 * 60,<br>      },<br>    }),<br>  );<br>  return { statusCode: 200 };<br>};</pre><h3>Disconnecting</h3><p>Similarly, I added a $disconnect route and integration with a Lambda called socket-disconnect:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yr8fmUlDDi3IcW8rANBJkw.png" /></figure><p>The socket-disconnect Lambda also receives an event with requestContext.connectionId as a parameter. This function needs to delete the record from the Dynamo table:</p><pre>import { DynamoDBClient } from &#39;@aws-sdk/client-dynamodb&#39;;<br>import { DynamoDBDocumentClient, DeleteCommand } from &#39;@aws-sdk/lib-dynamodb&#39;;<br><br>const client = new DynamoDBClient({});<br>const documentClient = DynamoDBDocumentClient.from(client);<br>const TableName = &#39;cleckheaton-cc-live-score-connections&#39;;<br>export const handler = async event =&gt; {<br>  const { connectionId } = event.requestContext;<br>  await documentClient.send(new DeleteCommand({ TableName, Key: { connectionId } }));<br>  return { statusCode: 200 };<br>};</pre><h3>Sending</h3><p>When the updated scorecard JSON is received from the SNS topic it needs to be sent to every connection stored in the Dynamo table. For the foreseeable future, a single Scan should be sufficient to get all the data although if traffic increases significantly then this may not scale too well and will have to be looked at again.</p><p>Once all the connections have been retrieved then the JSON is sent to each of them:</p><pre>import { ApiGatewayManagementApiClient, PostToConnectionCommand } from &#39;@aws-sdk/client-apigatewaymanagementapi&#39;;<br>import { Scorecard } from &#39;@cleckheaton-ccc-live-scores/schema&#39;;<br><br>const apiGatewayClient = new ApiGatewayManagementApiClient({ region: &#39;eu-west-2&#39;, endpoint: `${process.env.SOCKET_ENDPOINT}` });<br>const sendScorecard = (scorecard: Scorecard) =&gt; async (connectionId: string) =&gt; {<br>  const command = new PostToConnectionCommand({<br>    ConnectionId: connectionId,<br>    Data: Buffer.from(JSON.stringify(scorecard)),<br>  });<br>  return apiGatewayClient.send(command);<br>};</pre><p>The architecture looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/579/1*XnDZm8lHY83ncr8H33G63A.png" /></figure><h3>Connecting from the web client</h3><p>To connect to the socket from the web client I create a new instance of the <a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket">WebSocket class</a> passing the URL of the API to the constructor.</p><p>To receive events from the socket use the addEventListener method passing the name of the event and a listener function. The message event is the important one for receiving the latest scorecard and in the handler for that event I <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent">dispatch</a> a <a href="https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent">custom event</a> - this event has the name of the team the update refers to and the detail property is the JSON received in the message. These events can then be handled on any page that needs an up-to-date score:</p><pre>const teamEventName = {<br>  firstTeam: &#39;firstTeamScoreUpdate&#39;,<br>  secondTeam: &#39;secondTeamScoreUpdate&#39;,<br>};<br><br>const registerForScorecardUpdates = () =&gt; {<br>  const socket = new WebSocket(`${window.ENV.UPDATES_WEB_SOCKET_URL}`);<br>  socket.addEventListener(&#39;open&#39;, () =&gt; {<br>    console.log(&#39;Connected to updates web socket&#39;);<br>  });<br>  socket.addEventListener(&#39;message&#39;, event =&gt; {<br>    console.log(&#39;received&#39;, event.data);<br>    const scorecard = JSON.parse(event.data);<br>    window.dispatchEvent(new CustomEvent(teamEventName[scorecard.teamName as &#39;firstTeam&#39; | &#39;secondTeam&#39;], { detail: scorecard }));<br>  });<br>};<br>export { registerForScorecardUpdates };</pre><p>The code for this article can be found on <a href="https://github.com/ChrisDobby/live-scores/tree/blog-part-3"><strong>this branch</strong></a> and the full service is <a href="https://github.com/ChrisDobby/live-scores"><strong>here</strong></a>.</p><h3>Coming next</h3><p>Part 4 will show how the updates are pushed to <a href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API"><strong>Web Push</strong></a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0846e9abfebf" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Keeping supporters up to date using AWS Events and Web Push (part 2)]]></title>
            <link>https://medium.com/@chrd/keeping-supporters-up-to-date-using-aws-events-and-web-push-part-2-f8d93c356c41?source=rss-b0df41a007b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/f8d93c356c41</guid>
            <category><![CDATA[event-driven-architecture]]></category>
            <category><![CDATA[cricket]]></category>
            <category><![CDATA[lambda-function]]></category>
            <category><![CDATA[web-push-notifications]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Chris Dobson]]></dc:creator>
            <pubDate>Sun, 26 Jan 2025 19:27:50 GMT</pubDate>
            <atom:updated>2025-01-26T19:27:50.034Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JA0ejw7JW_WfC9OaK7RHEw.png" /></figure><p><em>Parsing the HTML and using an AWS fan-out architecture to publish the results</em></p><p><em>This is part 2 of a series of articles showing how I built a solution to keep supporters of a sports club up to date using an event-driven architecture in AWS and </em><a href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API"><em>Web Push</em></a><em> notifications.</em></p><p><em>The first part can be found </em><a href="https://chrisdobby.dev/keeping-supporters-up-to-date-using-aws-events-and-web-push-part-1-98bdb8b1a29e"><em>here</em></a><em>.</em></p><h3>Goals</h3><ul><li>Keep supporters up to date as much as possible during the games</li><li>Use an event-driven architecture in AWS</li><li>Require no manual intervention (I’ve better things to do at the weekend)</li><li>Minimal cost as it’s running in my AWS account</li></ul><h3>Part 2</h3><p>Now I have the raw HTML for the current score on an SQS queue it needs parsing to create a JSON representation of the scorecard which will be used to make several updates:</p><ul><li>JSON object in an S3 bucket</li><li>WebSockets</li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API">Web Push</a> notifications</li><li>Update the result in CMS when the game is over</li><li>Teardown EC2 instance when the game is over</li><li>In the near future create a <a href="https://openai.com/blog/chatgpt">ChatGPT</a> match report when the game is over</li></ul><p>This article will deal with updating the S3 bucket and the game over updates — WebSockets and <a href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API">Web Push</a> will be covered in parts 3 &amp; 4.</p><h3>Creating the scorecard JSON</h3><p>To create the scorecard I created a Lambda, create-scorecard, that reads the HTML from the SQS and uses Cheerio to parse it and create an object. Once the object has been created it is validated using <a href="https://github.com/colinhacks/zod">Zod</a> against this schema:</p><pre>import { z } from &#39;zod&#39;;<br><br>const BowlingFiguresSchema = z.object({<br>  name: z.string(),<br>  overs: z.string(),<br>  maidens: z.string(),<br>  runs: z.string(),<br>  wickets: z.string(),<br>  wides: z.string(),<br>  noBalls: z.string(),<br>  economyRate: z.string(),<br>});<br>const PlayerInningsSchema = z.object({<br>  name: z.string(),<br>  runs: z.string(),<br>  balls: z.string(),<br>  minutes: z.string(),<br>  fours: z.string(),<br>  sixes: z.string(),<br>  strikeRate: z.string(),<br>  howout: z.array(z.string()),<br>});<br>const InningsSchema = z.object({<br>  batting: z.object({<br>    innings: z.array(PlayerInningsSchema),<br>    extras: z.string(),<br>    total: z.string(),<br>    team: z.string(),<br>  }),<br>  fallOfWickets: z.string(),<br>  bowling: z.array(BowlingFiguresSchema),<br>});<br>const ScorecardSchema = z.object({<br>  url: z.string(),<br>  teamName: z.string(),<br>  result: z.string().nullable(),<br>  innings: z.array(InningsSchema),<br>});</pre><p>This schema can then be used by any downstream service to validate that the message received is a valid scorecard.</p><h3>Making the updates</h3><p>Once the JSON had been created the various updates needed to be made. This could easily be done from the same Lambda that creates it however I like my Lambda functions to adhere to the <a href="https://en.wikipedia.org/wiki/Single-responsibility_principle">Single Responsibility Principle</a> as much as I can and only perform one function. This means creating a single Lambda to perform each update (update S3, teardown the EC2 instance, update the CMS) and pass the JSON to each one.</p><p><a href="https://aws.amazon.com/sns/">AWS SNS</a> allows the JSON to be published to a topic and multiple Lambdas (amongst other services) to subscribe to the topic. So I created a scorecard-updated topic and added a subscription for each of the Lambda functions and the create-scorecard Lambda publishes the JSON to the topic.</p><p>Each Lambda validates the incoming message using the <a href="https://github.com/colinhacks/zod">Zod</a> schema defined in the previous section in case rogue messages have been published to the topic.</p><p><strong><em>Update S3</em></strong></p><p>The update-bucket Lambda updates the S3 Object, it simply creates a key for the object based on the team and the match date and PUTs the object using that key. The PUT operation will either create a new object or overwrite an existing one.</p><p><strong><em>Update CMS</em></strong></p><p>The update-sanity Lambda checks if the match has a result and, if it does, makes an update to <a href="https://www.sanity.io/">Sanity</a>. The fixtures should already exist in <a href="https://www.sanity.io/">Sanity</a> so firstly it makes a query to find the fixture based on team and date and, if one is found, makes a PATCH request for the fixture setting the result.</p><p><strong><em>Teardown EC2 Instance</em></strong></p><p>The update-processors Lambda also checks if the match has a result and, if it does, checks if the EC2 instance needs terminating. When the EC2 instance is created two tags are added, firstly the Owner which is set to cleckheaton-cc and secondly InProgress which is a count of the number of matches currently being processed by the instance.</p><p>This Lambda finds the instance with and Owner of cleckheaton-cc and checks the InProgress count, if the count is 1 then, as the match has now been completed the instance can be terminated. If the count is &gt; 1 then the count is decreased by 1 and the tag is updated.</p><p>There is still a Lambda that will run at 9 pm to terminate any instances that are running that will catch anything that is missed by this process but it may save the odd cent here and there!</p><p>The components added so far look like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/580/1*w2j5p8jCXtU_DYSNlY7A3g.png" /></figure><h3>Refactor</h3><p>Of the three Lambda functions I just added two of them only do any updates if the match is complete (has a result). This made me think that it might be worth adding a new SNS topic that publishes only matches that are complete which the update-processors and update-sanity Lambdas subscribe to instead.</p><p>So I created a new Lambda game-over that subscribes to the scorecard-updated SNS and checks whether the match has a result or not and if there is a result publishes to the game-over topic. This check can then be removed from update-processors and update-sanity which now subscribe to the game-over topic.</p><p>The architecture now looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/632/1*D1qJn8n_kntOsxcOwTynmw.png" /></figure><p>The code for this article can be found on <a href="https://github.com/ChrisDobby/live-scores/tree/blog-part-2">this branch</a> and the full service is <a href="https://github.com/ChrisDobby/live-scores">here</a>.</p><h3>Coming next</h3><p>Part 3 will show how the updates are pushed to WebSockets.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f8d93c356c41" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Keeping supporters up to date using AWS Events and Web Push (part 1)]]></title>
            <link>https://medium.com/@chrd/keeping-supporters-up-to-date-using-aws-events-and-web-push-part-1-98bdb8b1a29e?source=rss-b0df41a007b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/98bdb8b1a29e</guid>
            <category><![CDATA[event-driven-architecture]]></category>
            <category><![CDATA[lambda-function]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[cricket]]></category>
            <dc:creator><![CDATA[Chris Dobson]]></dc:creator>
            <pubDate>Wed, 22 Jan 2025 14:13:28 GMT</pubDate>
            <atom:updated>2025-01-22T14:13:28.932Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*JA0ejw7JW_WfC9OaK7RHEw.png" /></figure><p><em>How I built a solution to keep supporters of a sports club up to date using an event-driven architecture in AWS and Web Push</em></p><p>When I was asked to create a new website for the cricket club I played for I figured it would be something I could use to experiment with. Given that my UI skills leave a lot to be desired I didn’t want to do much on the front end so I decided to include up-to-date live scores for both our teams every weekend and allow users to subscribe to receive updates via <a href="https://developer.mozilla.org/en-US/docs/Web/API/Push_API">Web Push</a> to their device during the games.</p><h3>Goals</h3><ul><li>Keep supporters up to date as much as possible during the games</li><li>Use an event-driven architecture in AWS</li><li>Require no manual intervention (I’ve better things to do at the weekend)</li><li>Minimal cost as it’s running in my AWS account</li></ul><h3>Part 1</h3><p>The first step was to find the raw data and make it available. The live scorecards for each game in the league are available on separate web pages so I needed to find the pages for games involving my club and scrape the latest HTML from them.</p><h3>Finding the scorecards</h3><p>Some page interaction was needed with the page before the links were available so <a href="https://github.com/puppeteer/puppeteer/tree/main#readme">Puppeteer</a> seemed like the best choice to open up the page.</p><p>It made sense to run this process as a lambda as it should run in under a minute and only once. This sounded fine until trying to deploy to AWS — to cut a long story short <a href="https://github.com/puppeteer/puppeteer/tree/main#readme">Puppeteer</a> needs Chrome binaries and once bundled into a zip file the whole thing is far larger than the 50Mb limit for Lambda. So I ended up creating a lambda layer from <a href="https://www.npmjs.com/package/chrome-aws-lambda">chrome-aws-lambda</a> (which is just under 50Mb), removing it from the bundle and referencing it through the layer.</p><p>The URLs are stored in Dynamo and the Lambda is invoked by an EventBridge rule every 30 minutes between 12.00 and 14.00 — no games will start before midday so there’s no point running it earlier and it runs more than once in case there’s been a problem with the page. With the URLs being stored in Dynamo it’s easy to check if they already exist at the beginning of the function and return early if they do. Also, these URLs will not be required for any more than around 9 hours so a TTL is added for 24 hours to keep the table clean.</p><h3>Reading the raw data</h3><p>Once I have a URL for a game I then need to navigate to the correct tab for the scorecard, extract the relevant HTML, and do something with it.</p><p>Again <a href="https://github.com/puppeteer/puppeteer/tree/main#readme">Puppeteer</a> turned out to be my friend except this time it made no sense running as a Lambda. I wanted to get the scores every 5 minutes at the very least, ideally every 20 seconds, to be as up-to-date as possible so it was far more sensible to run the process inside an EC2 instance.</p><p>Because the page updates itself, presumably via a WebSocket, I can open up the webpage, navigate to the scorecard tab, every 20 seconds scrape the HTML, and if it’s changed put it on an SQS for processing downstream.</p><h3>Putting it together</h3><p>Once the URLs have been added to Dynamo I needed to create an EC2 instance that, when started, would clone the git repo, install dependencies and invoke a process for each of the games that are in progress.</p><p>Enabling streams on the Dynamo table and invoking a Lambda function is probably the best choice for this.</p><p>Once the lambda receives an insert event from the stream it creates an EC2 instance using the AWS SDK and sets the userdata of the instance to install git, install node, clone the repo, run npm ci and invoke a process for each game that is being played.</p><pre>const USER_DATA = `#!/bin/bash<br>yum update -y<br>yum install -y git<br>yum install -y pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64 libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1 xorg-x11-fonts-misc<br>curl -sL &lt;https://rpm.nodesource.com/setup_16.x&gt; | sudo bash -<br>yum install -y nodejs<br>git clone &lt;https://github.com/ChrisDobby/cleckheaton-cc.git&gt;<br>cd cleckheaton-cc/live-scores/scorecard-processor<br>npm ci<br>`;<br><br>const getStartCommand = ({ teamName, scorecardUrl }: ScorecardUrl) =&gt; `npm start ${scorecardUrl} ${process.env.PROCESSOR_QUEUE_URL} ${teamName}`;<br>const createInstance = (scorecardUrls: ScorecardUrl[]) =&gt; {<br>  const userData = `${USER_DATA} ${scorecardUrls.map(getStartCommand).join(&#39; &amp; &#39;)}`;<br>  const command = new RunInstancesCommand({<br>    ImageId: &#39;ami-0d729d2846a86a9e7&#39;,<br>    InstanceType: &#39;t2.micro&#39;,<br>    MaxCount: 1,<br>    MinCount: 1,<br>    KeyName: &#39;test-processor&#39;,<br>    SecurityGroupIds: [process.env.PROCESSOR_SG_ID as string],<br>    IamInstanceProfile: { Arn: process.env.PROCESSOR_PROFILE_ARN },<br>    UserData: Buffer.from(userData).toString(&#39;base64&#39;),<br>    TagSpecifications: [<br>      {<br>        ResourceType: &#39;instance&#39;,<br>        Tags: [<br>          { Key: &#39;Owner&#39;, Value: &#39;cleckheaton-cc&#39; },<br>        ],<br>      },<br>    ],<br>  });<br>  return client.send(command);<br>};</pre><p>Originally I had a single EC2 instance per game but, obviously, a single instance will halve the cost!</p><p>On the subject of cost, I now have an EC2 instance running and, currently, the only way of terminating it is manually. Instead of relying on myself to both remember to terminate it and to have access to the AWS console to do it every weekend, I added another Lambda function that would search for any instances with the Ownertag set to cleckheaton-cc and terminate them. I can happily invoke this from an EventBridge rule scheduled at 9 pm as it’s very unlikely a game will be going on after that time.</p><pre>export const handler = async () =&gt; {<br>  const command = new DescribeInstancesCommand({<br>    Filters: [{ Name: &#39;tag:Owner&#39;, Values: [&#39;cleckheaton-cc&#39;] }],<br>  });<br><br>  const instances = await ec2Client.send(command);<br>  const instanceIds = instances.Reservations.flatMap(({ Instances }) =&gt;<br>    Instances?.map(({ InstanceId }) =&gt; InstanceId)<br>  ).filter(Boolean) as string[];<br>  const terminateCommand = new TerminateInstancesCommand({<br>    InstanceIds: instanceIds,<br>  });<br>  await ec2Client.send(terminateCommand);<br>};</pre><p>So at this point, I’m finding the matches that are being played on a particular day, reading the scorecard html for each game every 20 seconds and putting it onto an SQS queue for processing downstream. Also, everything is being cleaned up — the scorecard URLs will be deleted from the table and the EC2 instance will be terminated.</p><p>The architecture looks like this:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/530/1*VHmSbtGRySX2Mn4lkMvu9w.png" /></figure><p>The code for this article can be found on <a href="https://github.com/ChrisDobby/live-scores/tree/blog-part-1">this branch</a> and the full service is <a href="https://github.com/ChrisDobby/live-scores">here</a>.</p><h3>Coming next</h3><p>Part 2 will show how the HTML is processed downstream and the fan-out pattern is used to make several updates.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=98bdb8b1a29e" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[An adventure in testing Step Functions part 2]]></title>
            <link>https://medium.com/@chrd/an-adventure-in-testing-step-functions-part-2-8f521a68a019?source=rss-b0df41a007b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/8f521a68a019</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[aws-step-functions]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[step-functions]]></category>
            <category><![CDATA[testing]]></category>
            <dc:creator><![CDATA[Chris Dobson]]></dc:creator>
            <pubDate>Sun, 08 Dec 2024 20:11:13 GMT</pubDate>
            <atom:updated>2024-12-08T20:11:13.678Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/694/1*mlMZwUSDmxcqMGgq2_WhnA.png" /></figure><p><em>Time travel and mocking.</em></p><p>In <a href="https://medium.com/@chrd/an-adventure-in-testing-step-functions-f26e1246d03d">part 1</a> I looked at testing a Step Function through the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html">TestState API</a> without having to deploy anything. I ended up with a fairly simple library that could test a full state machine that doesn’t include parallel and map states. Next I looked at how I could add features that make testing Step Functions easier…</p><h3><strong>Wait states</strong></h3><p>One of the challenges with testing Step Functions is if the state machine includes a <a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-wait.html">Wait state</a>. Many workflows include a requirement to wait for minutes, hours, days, weeks and in order to test the deployed state machine it is often necessary to pass in a flag to indicate this is a test and that the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-wait.html">Wait state</a> should not actually wait. What I really need is a flux capacitor!</p><p>In the absence of <a href="https://en.wikipedia.org/wiki/Emmett_Brown">Doc Brown</a> testing though the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html">TestState API</a> allows me to transform any of the states relatively easily. So a tiny bit of code to check if the state is a <a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-wait.html">Wait state</a> and if it is set the number of seconds to 0 proves that time travel is actually possible:</p><pre>  switch (stateDefinition.Type) {<br>    case &quot;Wait&quot;:<br>      return { ...stateDefinition, Seconds: 0 }</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/173/1*2alFaFHx6bnYPpSM01hmOw.png" /><figcaption>Who needs a flux capacitor when I’ve got a switch statement?</figcaption></figure><h3><strong>Mocking a full state</strong></h3><p>A challenge in testing any code that interacts with another service is that I may not be able to control the other service and, therefore, cannot cover all cases that I may want to test. Also many workflows will involve reading or writing records to a database — often in this case I can add test records into the database and clean up afterwards but it’s not unheard of for inserting a record to start off a new workflow. For these and other instances I will often want to mock a result without making calls to the service.</p><p>Using the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html">TestState API</a> to mock single states that are not deployed makes mocking a full state relatively easy. I have implemented a similar interface to the one used by <a href="https://jestjs.io/">jest</a> and <a href="https://vitest.dev/">vitest</a> where the test code can set up a mock for a state. For instance this sets up a mock output for the test-state state:</p><pre>mockState(&quot;test-state&quot;, { output: { description: &quot;This output is mocked!&quot; } })</pre><p>The test runner will then check for any registered mocks for a state and if there are rather then calling the API to execute the state will return the result supplied when the mock is setup:</p><pre>export const mockedResult = (stateName: string, nextState?: string) =&gt; {<br>  const mocks = stateMocks.get(stateName)<br><br>  if (!mocks) {<br>    return null<br>  }<br><br>  const [mock] = mocks<br><br>  return {<br>    error: mock.error,<br>    status: mock.error ? (&quot;FAILED&quot; as const) : (&quot;SUCCEEDED&quot; as const),<br>    nextState: mock.nextState || nextState,<br>    output: mock.output,<br>  }<br>}</pre><p>In this way I am able to bypass calling any services that there could be a problem using.</p><h3><strong>Mock service responses</strong></h3><p>Bypassing the execution of a whole state is all well and good but a state will often include steps to transform responses that are returned from a service. If the full state is being mocked then these transforms cannot be tested. What I needed to do was to execute the state but return a specified response from the task that is called. Neither the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html">TestState API</a> or the Step Function service can do this.</p><p>In the absence of a native way of doing this some creative thinking was required — some may say hacking but I’m sticking with my description.</p><p>As with the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-wait.html">Wait state</a> I have taken advantage of the fact that the state isn’t deployed anywhere and can be transformed in a test. In this case I deployed a <a href="https://docs.aws.amazon.com/lambda/latest/dg/welcome.html">Lambda Function</a> that simply returns whatever is passed in:</p><pre>export const handler = async ({ mockResult }) =&gt; {<br>  return mockResult;<br>};</pre><p>In the same way as when mocking a full state the test sets up the service responses for specific states.</p><p>This sets up a response for an HTTP task:</p><pre>mockResponse(&quot;test-state&quot;, { response: {<br>    ResponseBody: {<br>      name: &quot;Chris Dobson&quot;,<br>    },<br>    StatusCode: 200,<br>    StatusText: &quot;OK&quot;,<br>  }<br>})</pre><p>This sets up a response for a <a href="https://docs.aws.amazon.com/step-functions/latest/dg/connect-ddb.html">DynamoDB GetItem</a> task:</p><pre>mockResponse(&quot;test-state&quot;, { response: {<br>    Item: {<br>      id: {<br>        S: &quot;user-1&quot;,<br>      },<br>      name: {<br>        S: &quot;Chris Dobson&quot;,<br>      },<br>    },<br>  }<br>})</pre><p>When the test runner is required to mock a response the state is transformed into a Lambda task with a target of the function created above:</p><pre>export const transformState = (stateName: string, stateDefinition: TestSingleStateInput[&quot;stateDefinition&quot;]) =&gt; {<br>  const mocks = responseMocks.get(stateName)<br>  if (!mocks || !mocks.length) {<br>    return stateDefinition<br>  }<br><br>  const [mock] = mocks<br><br>  return {<br>    ...stateDefinition,<br>    ...updateOutputs(stateDefinition, mock.response),<br>    Resource: &quot;arn:aws:states:::lambda:invoke&quot;,<br>    Parameters: {<br>      FunctionName: mockFunctionName,<br>      Payload: { mockResult: mock.response },<br>    },<br>  }<br>}</pre><p>This then will return the required response when the state is executed through the API and tests the rest of the state.</p><p>The library includes functions that will create and delete the step function that can be called before and after testing.</p><h3><strong>The library</strong></h3><p>The library I’ve created is called step-by-step (any suggestions for a better name gratefully received 😀) and can be found on <a href="https://github.com/chrisDobby/step-by-step">Github</a> and <a href="https://www.npmjs.com/package/@chrisdobby/step-by-step">npm</a>.</p><h3><strong>Next</strong></h3><p>In the next post in this series I will look at supporting <a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-map.html">Map</a> and <a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-parallel.html">Parallel</a> states and testing failure.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8f521a68a019" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[An adventure in testing Step Functions]]></title>
            <link>https://awstip.com/an-adventure-in-testing-step-functions-f26e1246d03d?source=rss-b0df41a007b6------2</link>
            <guid isPermaLink="false">https://medium.com/p/f26e1246d03d</guid>
            <category><![CDATA[testing]]></category>
            <category><![CDATA[step-functions]]></category>
            <category><![CDATA[serverless]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[aws-step-functions]]></category>
            <dc:creator><![CDATA[Chris Dobson]]></dc:creator>
            <pubDate>Sat, 12 Oct 2024 15:02:17 GMT</pubDate>
            <atom:updated>2024-12-09T13:35:57.545Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="A Step Functions state machine" src="https://cdn-images-1.medium.com/max/1004/1*WjqN5QVWTRsAx2sLQbGxzw.png" /></figure><p><em>Is it possible to get good test coverage of a Step Function? I am attempting to create a library using the TestState API that will allow me to test my Step Functions more thoroughly.</em></p><p>Recently I needed to debug a Step Function and found myself:</p><ul><li>editing the state in the AWS console</li><li>hitting the Test state button to open the test window</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1004/1*HrxMvNkhiSaExB6sp-2PSg.png" /></figure><ul><li>setting the inputs, executing the state</li><li>copying the output and next state that was returned</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1Fph0PDP4MWWcaMTsaqimg.png" /></figure><ul><li>going back to the function and finding the next state</li><li>hitting the Test state button</li><li>pasting the output as the input and so on….</li></ul><p>Obviously as a developer I figured why would I spend 15 minutes manually doing this when I could spend a much more interesting 90 minutes writing some code to automate it 😀</p><p>So I set about using the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html">TestState API</a> to automate the process and noticed something I didn’t expect — I expected I’d need to pass the state machine ARN and the name of state to test but the API accepts the state as JSON instead meaning that the function doesn’t need to be deployed in order to test a single state.</p><p>💭 Given that the API returns both the output and the next state to be executed I figured it ought to be possible to write some code that accepts the full function definition and an initial input and executes each state. Occasionally it can also be desirable to execute a subset of the states in a function and by writing code using the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html">TestState API</a> this also ought to be possible.</p><p>Currently testing a full state machine can be tricky and I find the approach tends to depend on what the function is doing which isn’t ideal. The main two options seem to be:</p><h3><strong>Deploy to AWS</strong></h3><p>Deploying to AWS ensures that the test is running in the actual environment that it will be running on in production and is generally good for testing the ‘happy path’. Depending on the AWS environment deployed to you may need to clean up after yourself if some of the side effects of the function create data, if you are able to test using an <a href="https://www.youtube.com/watch?v=JO0arpbkh5w">ephemeral environment</a> then this shouldn’t be a problem.</p><p>Testing some paths, especially errors, can be difficult — for instance influencing the response of a third party API is impossible and making an AWS service with say five 9s uptime fail relies on running the test in the 6 minutes it may be down. If I’m unit testing some code these responses can be mocked but, obviously, that can’t be done in a real AWS environment.</p><h3>Deploy locally</h3><p>I find <a href="https://app.localstack.cloud">Localstack</a> an invaluable tool when developing and some of the features I find invaluable can also be applied to testing Step Functions. For instance unlike the real AWS environment service responses can be mocked to help get to those hard to reach states. A disadvantage though is that however good it is (and it is very good) it isn’t an actual AWS environment so could potentially contain bugs (different bugs to the real AWS environment) and may not keep completely up to date with AWS — as we know the cadence of AWS introducing new features can be very high and event though they do a fantastic expecting <a href="https://app.localstack.cloud">Localstack</a> to keep up is a bit much!</p><p>Both of these options have advantages and disadvantages but a couple of things can’t be done using either.</p><ul><li>Firstly neither have an option to ‘time travel’ — by which I mean that if a Wait state is used there is no option for skipping the time so if I have a state that waits for a month then I have to wait for a month. This can be mitigated by passing the time to wait as a parameter to the function but this means including code specifically for testing which while sometimes can’t be avoided I prefer to avoid if I can.</li><li>Secondly they both involve deploying somewhere which may make it difficult or at the very least time consuming to include in a CI pipeline.</li></ul><h3>Could an approach using the TestState API improve things?</h3><p>I think the approach I stumbled across when automating my debugging process could potentially improve a few things:</p><ul><li>Using the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html">TestState API</a> doesn’t required the state machine to be deployed</li><li>Wait states can be handled by the code executing the test setting the delay rather than specifically including test code</li><li>Full states and service responses can be mocked in the code executing the test</li></ul><p>It seemed to me that it might be worth creating a library that uses these ideas and see if it is, in fact, of any use. So I’m creating a library (Typescript/Javascript as that is what I use most) and writing about it as I go.</p><p>Initially I implemented execution of a single state and full state machine without any of the bells and whistles discussed above — they are coming soon!</p><h3>Executing a single state</h3><p>Execution of a single state was just a question of creating a wrapper around the <a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/sfn/">AWS SDK</a>:</p><pre>const result = await client.send(<br>  new TestStateCommand({<br>    definition: JSON.stringify(stateDefinition),<br>    roleArn: process.env.AWS_ROLE_ARN!,<br>    input: JSON.stringify(input),<br>  })<br>)<br><br>return {<br>  error: result.error ? { message: result.error!, cause: result.cause! } : undefined,<br>  status: result.status,<br>  nextState: result.nextState,<br>  output: result.output ? JSON.parse(result.output) : undefined,<br>}</pre><h3>Executing a full state machine</h3><p>Each state machine definition includes a StartAt property which, as the name suggests, is the name of the first state to be executed and states that complete the state machine will have a property of End: true. So executing a full state machine involves finding the first state to execute and recursively testing each state until hitting a state that completes the function. I also keep a record of the ‘call stack’ of states:</p><pre>const execute = async ({<br>  functionDefinition,<br>  input,<br>  state,<br>  stack = [],<br>}: TestFunctionInput &amp; {<br>  state: string<br>  stack?: TestFunctionOutput[&quot;stack&quot;]<br>}): Promise&lt;TestFunctionOutput&gt; =&gt; {<br>  const stateDefinition = functionDefinition.States[state]<br>  const result = await testSingleState({ stateDefinition, input })<br>  const updatedStack = [...stack, { ...result, stateName: state }]<br><br>  return stateDefinition.End<br>    ? { ...result, stack: updatedStack }<br>    : execute({ functionDefinition, input: result.output, state: result.nextState!, stack: updatedStack })<br>}</pre><h3>The library</h3><p>The library I’ve created is called step-by-step (any suggestions for a better name gratefully received 😀) and can be found on <a href="https://github.com/chrisDobby/step-by-step">Github</a> and <a href="https://www.npmjs.com/package/@chrisdobby/step-by-step">npm</a></p><h3>Next</h3><p><a href="https://medium.com/@chrd/an-adventure-in-testing-step-functions-part-2-8f521a68a019">Next</a> I will be adding time travel support for the Wait state and investigating how I might handle mocking…</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f26e1246d03d" width="1" height="1" alt=""><hr><p><a href="https://awstip.com/an-adventure-in-testing-step-functions-f26e1246d03d">An adventure in testing Step Functions</a> was originally published in <a href="https://awstip.com">AWS Tip</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>