Kensio Software Blog »

Video: Simulating AWS locally for website hosting with Yulin

Hugh Grigg | Kensio Software | Monday 6 Jul 2026

How to use the Yulin local AWS simulator for Node.js to simulate website hosting, including Route53, S3, CloudFront and CloudFront Functions, provisioned via simulated CloudFormation.

YouTube: https://www.youtube.com/watch?v=hkhi5uzc03w

Transcript

In this video, I’m going to show you how to simulate AWS for static website hosting entirely on local host, including Route 53, ACM, S3, Cloudfront, and CloudFront Functions.

So this is the demo website project. It’s an Astro website built with the Astro framework. Let’s see what the website looks like.

Here we have the astro dev command, which starts a local development server. I’ll run that and we get the Astro dev server on local host port 4321. Let’s open it.

Here’s the demo website running on local host 4321. It’s just the official Astro blog theme with a few small modifications. So that’s our starting point.

Now, that’s fine if you just want to view the website locally. But what if you’re deploying it to AWS. A production deployment usually involves several AWS services. For example, you might be using CDK, and your stack could look something like this.

This is a real CDK stack for deploying a static website. Lets walk through it. First, we have a domain name. This isn’t a real registered domain. It’s just for the demo, yulin-demo-website.com.

We have a Route 53 hosted zone for that domain, an ACM certificate so that we can use HTTPS instead of HTTP and an S3 bucket containing the static website files.

We also have two CloudFront Functions, which we’ll come back to shortly, and then we have a CloudFront distribution, which actually serves the website over the internet using our domain name and ACM certificate. It also associates those two CloudFront Functions.

Next is an S3 bucket deployment. This is a CDK feature that automatically copies files from a local directory into the S3 buckets during deployment, which is very useful for static websites.

Finally, we have a Route 53 alias record. Alias records are an AWS specific feature that let you point the root of a domain directly at an AWS service, such as a CloudFront Distribution. We also have the IPv6 equivalent.

The only other things in the stack are three outputs which expose useful values, like the website URL, and we’ll use those later.

If we look back at the bucket deployment, it’s expecting to copy files from a local dist directory. Right now, that directory doesn’t exist because it’s produced by the Astro build step. So let’s run the build.

Cool. That builds the website instead of just serving it. We can now see the generated HTML files in the dist directory, which means the bucket deployment now has something to upload.

The other thing we need is a CloudFormation template. CDK generates this for us and it’s what we use to deploy to either real AWS or our local simulation. I’ll run pnpm exec cdk synth that prints the template to the terminal, but it also creates a new cdk.out directory containing the deployment artefacts.

Here’s the JSON template. It’s the CloudFormation version of our CDK stack and describes all of the resources and their configuration.

Now that we have that, we’re ready to simulate the entire stack locally with Yulin. Below the Astro scripts, we have another script called local-dev.mts. Let’s open that.

This creates a simulated AWS environment and starts a local host server that serves it. It then uses simulated CloudFormation to deploy the JSON template that we just generated. We point it at the template inside cdk.out and it deploys all of those resources into the simulated AWS environment. Finally, it reads the website URL output from the stack, asks the local server for the local host equivalent and logs that URL for convenience. So let’s run it.

Cool. That’s deployed the entire stack into simulated AWS. The first thing you’ll notice is that we have our real domain name with the Route 53 hosted zone attached as a prefix to the sim-aws.localhost domain. This works not only for Route 53 hosted zones, but also for CloudFront distribution domain names, and S3 website host names.

Let’s open the simulated website. The browser is now visiting our simulated Route 53 domain, and the website looks identical to the one served by Astro’s development server.

If we open the developer tools and reload the page, let’s have a look at the response headers, you’ll notice a few extra security headers, such as Referrer-Policy and X-Content-Type-Options.

Now let’s compare that with the Astro development server. If we reload the same page there, those headers aren’t present, so where are they coming from?

Back in our project, they’re being added by the viewer response CloudFront Function that we saw earlier. A viewer response function intercepts responses on their way back to the client. In our CDK stack, this resource brings the CloudFront Function source code into the deployment and then associates it with the CloudFront distribution as a viewer response function.

Let’s look at the function itself. The source code lives in src/cff/viewer-response. As you can see, this is the code that’s adding those security headers before the response is sent back to the browser. So that’s one example of functionality. We’re able to simulate and test locally.

We also have another CloudFront Function, the viewer request function. It’s associated with the distribution as a viewer request function and its source code lives in viewer-request. Let’s open that one. This function does a bit more.

One thing it handles is trailing slash or pretty URL routing. A URL ending with a slash is actually served by an index.html object in S3 behind the scenes. For example, blog/first-post/ is really serving blog/first-post/index.html. That means visitors only need to browse to blog/first-post/ and this CloudFront Function transparently maps that to the correct file. That behaviour is the same in both real AWS and our local simulation.

The function also handles legacy redirects. For example, maybe this site originally served blog posts under /posts but later we decided /blog was a better URL structure. We can keep those old links working by redirecting /posts to /blog inside the viewer request function.

Let’s try that. If I visit /post, we’re automatically redirected to /blog. If we look in dev tools, the request to /posts returned a 301 Moved Permanently and the Location header points to /blog. That redirect is coming directly from the viewer request CloudFront Function we just looked at.

Now compare that with the Astro development server. If I visit /post there, I just get a 404 because the development server doesn’t know about the production redirect rules. With the simulation, we can test exactly the same redirect behaviour that we’ll have in production on AWS without deploying anything.

So far, we’ve simulated the production behaviour locally. But what if there’s a bug in one of our CloudFront Functions and we want to debug it using our IDE. Without Yulin, that’s usually quite difficult and on many projects, it isn’t really practical. With Yulin, it’s quite straightforward.

Let’s go back to our local dev script. At the moment, all it’s doing is telling the simulator which CloudFormation template to deploy. If we look back at the CDK stack, you’ll notice that each CloudFront Function has its source code embedded directly into the generated JSON template. That’s how both real AWS and the simulator normally execute the function.

But in the simulator, we don’t have to use the embedded source code. Instead, we can use a feature called bindings. Bindings let us replace an embedded CloudFront Function with a real JavaScript function running in the same Node.js process as the simulator.

Let’s add one. We’ll create a bindings array where each entry replaces one resource in the template. First, we specify the logical ID of the resource we want to replace. In this case, it’s the viewer request function. Next, we need the handler. We’ll import that directly from our source code. Now we can pass it into the binding.

So what’s happening here is we’re telling Yulin to find the embedded handler for this CloudFront Function and replace it at runtime with the real handler function we’ve imported from our project. Everything now runs in a single process, which means our IDE debugger can see it.

Let’s stop the simulator and start it again, this time in debug mode. Everything looks much the same so far. The website still behaves exactly as before.

Now let’s go back to the viewer request function and set a breakpoint. I’ll put one here. Now when I refresh the page, the debugger stops on our breakpoint. If we switch back to the browser, it’s waiting for a response because execution is currently paused inside the CloudFront Function.

I’ll remove that breakpoint and continue, the page loads normally again. Earlier, we looked at the legacy redirect, so let’s put a breakpoint there instead. If I visit /post we’re taken straight back into the debugger. From here, we can inspect everything that’s available to the function. We can look at the event, the context, the request, and any other values we need while debugging.

Once we’re done, we simply continue execution, the redirect is returned, the browser follows it, and we end up on the correct /blog page, and all of that is enabled by this small local dev script, only about 28 lines of code.