·
16. October 2023·
rust , typescript , wasm , nextjs , leptos , aws , cloud , infrastructure , cdk , ci
In the last post we set up our deployment, fully automated on merge to our main
branch. In this post we will be building our UI (Frontend) applications. See the full overview of posts here.
At the end of this post we will have:
We are essentially hedging our bets by building both a JavaScript-based Frontend, which is the safe bet, and a Rust/WASM-based Frontend, which is the future bet. We will be using the same GraphQL API for both, so we can easily switch between them.
There is quite a lot to cover. My recommendation is to clone down the Part 3 branch in the GitHub repository and use this post as an explanation of what is set up.
While SSR (Server Side Rendering) is seeing a renaissance these days, we are intentionally avoiding this functionality. If you remember the architecture outlined in our introduction to the series, we want to be able to serve our Frontend entirely using static file hosting.
There are multiple reasons for this, going back to our core design goals:
We do sacrifice the ability to do SSR and the various User Experiences that can potentially bring, but the benefits are far outweighed by the downsides in this tradeoff.
Next.js is one of the most popular React frameworks at the moment, and supports a lot of niceties as well as sets us on a path of a good structure from the get-go. It has a lot of momentum behind it, a strong focus on the Developer Experience, and uses React which makes it a very familiar option for a large majority of Frontend Engineers out there.
We'll once again use Bun, which you can install via:
|
While bun unfortunately doesn't fully support the Next.js App Router yet we will still rely on it for installing dependencies and being our general go-to tool for running anything JS related.
Let's get our Next.js app set up, which we will call ui-app
:
)
)
This gets us quite far, we now have an App we can run, Tailwind CSS is already set up, and we got a lot of the structure set up for us:
Voila, we've got a little Hello World Next.js app!
We need to do just one small change to our Next.js setup to make it output static files for us. We'll do this by adding output: "export"
to our next.config.js
file at the root of ui-app/
:
1 // @ts-check
2
3 /** @type {import('next').NextConfig} */
4 5 6 7 8 9 10 11 ;
12
13 module.exports = nextConfig;
We also enabled trailingSlash
(docs here) to make Next.js work nicely with CloudFront.
This tells Next.js that we want to to Statically Export our files, which will generate an HTML file per route, which allows each route to serve the minimal content it needs, enabling faster page loads, instead of the traditional SPA approach of serving one large file upfront.
We get the best of both worlds here, as we still get the reduced bundle sizes typical of SSR, but can retain the static file advantage of SPAs.
As our customer-base grows, we will inevitably run into the need to localization. To do this, we will restructure our App with basic support for this, as well as bring in a dependency to help us with this, namely next-intl. Vercel also has some good documentation on how to Get Started here.
Let's start by adding the dependency:
We'll also create a folder that will contain our localization files, which we'll call messages/
in the root of the ui-app/
project:
This allows us to set up some text for our first languages. Create an English locale, messages/en.json
, with the following:
1 2 3 4 5
And also a French locale, in messages/fr.json
:
1 2 3 4 5
To make this a bit nicer to work with, we'll also add typesafety by letting TypeScript know what keys we support in our localization function. Create a ui-app/global.d.ts
file with the following:
1 // Use type safe message keys with `next-intl`, based on the contents/keys of
2 // our default english locale.
3 ;
4
This ensures that if we misspell a key, or even remove one later on, we will be highlighted of the incorrect usage by TypeScript.
We can now set up a route using the App Router to pick up our locale. We'll want the locale to be part of our URL as a prefix on all routes, so that we can pick it up as a dynamic segment and use it to load the correct localization file.
First we will create a folder where our localized pages will live in, and also clean up the default files that Next.js created for us:
Let's create a simply page in here at src/app/[locale]/page.tsx
, and get our welcome text from the localization file:
1 "use client";
2
3 ;
4
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
We'll need to mark the component as 'use client'
for now, while next-intl is working on server-side support.
Since we removed existing layout file, we need to define a new one that also handles setting up our localization at the root of our components. We'll create a src/app/[locale]/layout.tsx
file with the following:
1 ;
2 ;
3 ;
4 ;
5 ;
6 ;
7 ;
8
9 10 11 12
13
14 ;
15
16 /**
17 * Set up all supported locales as static parameters, which ensures we generate
18 * static pages for each possible value of `locale`.
19 */
20 21 22 23 24
25
26 /**
27 * Load the contents of a given locale's messages file.
28 */
29 30 31 32 33 34 35 36
37
38 39 40 41 42 43 ;
44
45 46 47 48 49 50 51 52 53 54 55 56 57
To avoid needing to maintain a hardcoded list of locales, we dynamically find all the locales defined in our messages/
folder during build time, and construct a list of supported locales from this. We then pass the contents of this into NextIntlClientProvider
.
We could imagine later on, once our translation file becomes massive, that we split up en.json
into smaller segments such as en/home.json
, and load these parts specifically in a app/[locale]/home/layout.tsx
file. For now though, we'll keep it simple.
As the final piece of this puzzle, we need a way to let Next.js know where it should route to by default, since we removed the default root pages.
We unfortunately cannot use middlewares when statically exporting our site, so we will instead redirect the user upon loading the page. Create a src/app/page.tsx
file with the following:
1 ;
2
3 4 5
Along with a root layout file at src/app/layout.tsx
:
;
;
You should now have a structure that looks like this:
And that's it! We're now able to run our app and check it out in the browser:
It may not look like much, but we've implemented a lot of the core functionality we need to get started, such as static builds and localization.
As the final step we will add our commands to just, extending our existing justfile:
1 :
2 3 4 5
We'll also set up a new command for running our development server:
1 # Run <project> development server, e.g. `just dev ui-app`.
2 :
3 4 5 :
6 7 8 9
Leptos is one of the newer entries on the Rust/WASM scene. It has a radical focus on performance and scalability of your codebase, so that as your codebase grows your App doesn't just start to become slower which is a typical issue in React and VDOM-based frameworks.
Leptos should feel somewhat familiar, although it is more closely related to something like Solid.js which is based on Signals and not using a VDOM. Leptos has a good quick overview of features here and a nice FAQ here.
We will be using Trunk for developing and building our Leptos App. Trunk is a great tool for developing Rust/WASM Apps, and is very similar to Bun (in a sense) in that it is a wrapper around the underlying tools. Let's install it first:
# Install dependencies
We can then set up our project, which we'll call ui-internal
:
We'll immediately adjust our Cargo.toml
file with the dependencies we'll need, as well as a few common WASM optimizations for our release builds:
1 []
2 = "ui-internal"
3 = "0.1.0"
4 = "2021"
5
6 # Define our supported locales.
7 []
8 = "en"
9 = ["en"]
10 = "./messages"
11
12 # Optimize for WASM binary size.
13 []
14 = 'z'
15 = true
16 = 1
17
18 []
19 # Core leptos library.
20 = { = "0.5.1", = ["csr", "nightly"] }
21 # Leptos Meta adds support for adjusting <head> from within components.
22 = { = "0.5.1", = ["csr", "nightly"] }
23 # Router and Route state management.
24 = { = "0.5.1", = ["csr", "nightly"] }
25 # Leptos support for i18n localization.
26 = { = "0.2", = ["csr", "nightly"] }
27 # Lightweight logging support.
28 = "0.4"
29
30 # Common WASM libraries.
31 = { = "0.2" }
32 = { = "1" }
33 = { = "0.1" }
And finally, we'll use Rust Nightly to develop our App, which gives us a few better ergonomics:
Let's create a quick index.html
file in the root of the ui-internal/
folder, just to get started:
1
2
3
4
5
And replace the contents of our src/main.rs
:
1 use *;
2
3
4
5 6 7 8 9 10 11 12 13
We'll also create a src/app.rs
file with the following (we'll update this file later):
1 use *;
2
3
4 5 6
We can now run our App using Trunk:
Voila, we've got a little Hello World Leptos app!
Let's configure Tailwind CSS for our Leptos App. First, we need to tell Tailwind where to look for files that might contain our CSS classes. Create a ui-internal/tailwind.config.ts
file with the following:
1 /** @type {import('tailwindcss').Config} */
2 module.exports = 3 4 5 6 7 8 9 10 ;
We also need to tell trunk
to build Tailwind CSS as part of its build process. We can do this by creating a ui-internal/Trunk.toml
file:
1 [[]]
2 = "pre_build"
3 = "sh"
4 = [
5 "-c",
6 "bunx tailwindcss --input resources/input.css --output public/output.css",
7 ]
This let's trunk
know that before it builds our WASM App, it should run the bunx tailwindcss ...
command, which will generate our Tailwind CSS file, which it puts into public/output.css
.
Now, you might have noticed we also have an input file. Let's get that set up, along with a resources/
folder:
We'll then create our base Tailwind CSS file at ui-internal/resources/input.css
, mimicing our Next.js setup:
1 @ 2 @ 3 @ 4 5 6 7 8 9 }
10
11 {
12 13 14 15 16 }
17 }
18
19 20 21 22 23 24 25 26 27 }
28
Final step, we need to pull in our Tailwind CSS file in our index.html
. Update the contents to the following:
1
2
3
4
5
6
7
8 Hello, World!
9
10
11
12
And that's it! We've now integrated Tailwind CSS into our Leptos App.
We're using leptos_i18n for localization in Leptos, which supports an API that's very close to the one we used in Next.js. We already pulled in the dependency when we updated our Cargo.toml
file earlier, so let's get the rest of it set up.
We'll create a ui-internal/messages/
folder where our locales will live:
We'll define our first locale, English, in a messages/en.json
file:
1 2 3 4 5
And also a French locale, in a messages/fr.json
file:
1 2 3 4 5
leptos_i18n exposes a macro load_locales!()
that looks for our configuration and generates code specific for our project that we can load in our App.
Let's update src/main.rs
, and also pull in a new module home
in anticipation of creating splitting our code out from the current app.rs
file:
1 use *;
2
3
4
5
6 // Load our locales from the files defined in `Cargo.toml`.
7 !;
load_locales 8
9 10 11 12 13 14 15 16 17
Let's create a src/home.rs
in which will use our locales:
1 use crate*;
2 use *;
3 use *;
4
5
6 7 8 9 10 11 12 13 14 15 16 17 18
The magic here comes from the crate::i18n::*
which got generated by leptos_i18n::load_locales!()
, and the use_i18n
hook that we now got access to. Very similar to our Next.js App, we then call the macro t!
to get the correct translation for the current locale, given a JSON key.
We're not entirely done yet, we need to tell our Leptos App about the I18nContext
at the root of our application. We also still need to add support for routing between locales.
Let's update src/app.rs
to do this:
1 use crate*;
2 use *;
3 use *;
4 use *;
5
6 use crate home;
7
8 const DEFAULT_LOCALE: &str = "en";
9
10
11 12 13
14
15
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
49
50
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
There's a lot to unpack here, so let's go through it step by step.
pub fn Layout() -> impl IntoView
component we set up a normal Router
component, which will handle routing for us.LocalizedRoute
, which handles detecting our locale and switching the active locale if the path changes.fn LocalizedRoute
function we wrap the normal Route
component, and inject a bit of logic before returning the view
that was otherwise passed into our LocalizedRoute
.The last part is the most interesting, so let's break down what we are doing inside the Route
we are setting up for LocalizedRoute
.
First we get the current parameters, which we know will contain a locale
key:
1 // Extract the locale from the path.
2 let i18n = use_i18n;
3 let params = ;
4 let chosen_locale = move || params.map.unwrap_or;
We then create an effect that will run every time the parameters change, which will be every time the path changes:
1 create_effect 2 3 4 5 6 7 8 9 10 11 12 13 ;
The thing that makes our effect rerun is our usage of i18n()
which subscribes us to the signal, and thus reruns the effect every time the locale changes.
You should now have a structure that looks like this:
And that's it! We're now able to run our app and check it out in the browser:
Again, it may not look like much, but we've implemented a lot of the core functionality we need to get started!
As the final step we will add our commands to just, extending our existing justfile:
1 :
2 3 4 5 6 7 8 9 :
10 11 12 13
End-to-End tests are a great way to ensure that our App is working as expected, and that we don't accidentally break something when we make changes. We'll use Playwright for this, which is a great tool for writing End-to-End tests.
We want three different test suites to cover:
Let's start by setting up our folder structure. Many of our configuration files will be the same across all three test suites, let's create an end2end
folder for our projects:
We intentionally make a distinction between end2end/
and unit/integration tests which will live in tests/
. These have very different requirements for how to run them, and we often want to run them at different times.
Before we can run anything, we will need a couple of other files to set up Playwright as well as support for TypeScript.
Let's create a tsconfig.json
for all for all three projects (ui-app
, ui-internal
, and deployment
). We'll place it at <project>/end2end/tsconfig.json
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Now, let's configure Playwright for all for all three projects (ui-app
, ui-internal
, and deployment
). We'll place it at <project>/end2end/playwright.config.ts
:
1 ;
2 ;
3
4 ;
5
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 ;
55
56 ;
There are some minor adjustments we want to do in the above configuration for each project:
SERVER
variable to http://localhost:3000
and command to just dev ui-app
.SERVER
variable to http://localhost:8080
and command to just dev ui-internal
.webServer
block, and set the SERVER
variable to http://${process.env.DOMAIN}
.And a package.json
in <project>/end2end/package.json
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
We are now ready to add our first test! Since we have just added localization, let's make sure that it works and doesn't regress.
For all three projects ui-app
, deployment
, and ui-internal
we'll create a test in <project>/end2end/tests/localization.spec.ts
:
1 ;
2
3 "localization translates text when changing language", 4 5 6 7 8 9 10 11 12 13 ;
14
15 "localization loads correct text from URL", 16 17 18 19 20 ;
This same test works for both apps since we've set them up with the same functionality.
Let's try and run them:
And for deployment
we can test it locally by starting up just dev ui-app
in another terminal, and then running:
NOTE: You might want to add the following to your .gitignore
:
1 playwright-report
2 test-results
And that's it! We've now got an easy way to run End-to-End tests. Let's do our final step and add this to our justfile
:
1 # Run End-to-End tests for <project>, e.g. `just e2e ui-internal`.
2 :
3 4 5 :
6 7 8 :
9 10 11 :
12
And we'll also update our _setup-project
commands to setup the Playwright dependencies:
1 :
2 3 4 5 6 7 8 9 10 :
11 12 13 14 15 16 17 18 19 :
20 21 22 23 24 25 26 27 28
There are a few Editor improvements that are recommended for working with Leptos and Tailwind CSS if you are using VS Code.
Add to your settings:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
Another nice tool is leptosfmt, which helps keep our Leptos View macro code nicely formatted.
You can install it via:
And then add this to your settings:
1 2 3
Since we are now generating artifacts that we want to deploy, from multiple projects, we need to restructure our existing deployment pipeline slightly.
We still want to retain staggered deployments, but we need a bit of coordination to make sure we have all relevant artifacts before we deploy.
Our new flow will look like this:
In this part we will be doing the following:
cd-deploy.yml
workflow to build artifacts for ui-app
and ui-internal
via a reuseable workflow.wf-deploy.yml
workflow to download artifacts so it can use it during deployments.wf-build.yml
, that will build our artifacts.ui-app
and ui-internal
that will do the actual building.Let's start with our wf-build-ui-app.yml
workflow:
1 name: "Build: ui-app"
2
3 on:
4 workflow_call:
5 inputs:
6 release:
7 type: boolean
8 default: false
9 upload-artifact:
10 type: boolean
11 default: false
12
13 jobs:
14 ui-app:
15 runs-on: ubuntu-latest
16 steps:
17 - uses: actions/checkout@v3
18 - uses: actions/setup-node@v3
19 with:
20 node-version: 20
21 - uses: oven-sh/setup-bun@v1
22 - uses: extractions/setup-just@v1
23
24 - name: Install dependencies
25 working-directory: ./ui-app
26 run: bun install
27
28 - name: Build Next.js project
29 run: just build ui-app
30
31 - uses: actions/upload-artifact@v3
32 if: ${{ inputs.upload-artifact }}
33 with:
34 name: ui-app
35 path: ui-app/out
36 if-no-files-found: error
37 retention-days: 1
And our wf-build-ui-internal.yml
workflow:
1 name: "Build: ui-internal"
2
3 on:
4 workflow_call:
5 inputs:
6 release:
7 type: boolean
8 default: false
9 upload-artifact:
10 type: boolean
11 default: false
12
13 jobs:
14 ui-internal:
15 runs-on: ubuntu-latest
16 steps:
17 - uses: actions/checkout@v3
18 - uses: oven-sh/setup-bun@v1
19 - uses: extractions/setup-just@v1
20 - name: Install Rust toolchain
21 uses: dtolnay/rust-toolchain@nightly
22 with:
23 toolchain: nightly-2023-10-11
24 targets: wasm32-unknown-unknown
25
26 - name: Install trunk
27 uses: jaxxstorm/[email protected]
28 with:
29 repo: thedodd/trunk
30 platform: linux # Other valid options: 'windows' or 'darwin'.
31 arch: x86_64
32
33 - uses: actions/cache@v3
34 continue-on-error: false
35 with:
36 path: |
37 ~/.cargo/registry
38 ~/.cargo/git
39 ui-internal/target
40 key: cargo-${{ hashFiles('Cargo.lock') }}-${{ hashFiles('ui-internal/Cargo.lock') }}-ui-internal
41 restore-keys: cargo-
42
43 - name: Build release build
44 if: ${{ inputs.release }}
45 run: just build ui-internal
46
47 - name: Build debug build
48 if: ${{ !inputs.release }}
49 run: just build ui-internal true
50
51 - uses: actions/upload-artifact@v3
52 if: ${{ inputs.upload-artifact }}
53 with:
54 name: ui-internal
55 path: ui-internal/dist
56 if-no-files-found: error
57 retention-days: 1
Both of these workflows take two optional arguments:
release
: Whether or not to build a release build.upload-artifact
: Whether or not to upload the artifact to GitHub.This means we can easily reuse these builds from our CI workflows. Once our jobs are building, we'll see these artifacts in the Summary view of our workflow, which will look something like this:
With these in place we can now stitch them together in a wf-build.yml
:
1 name: "Build Artifacts"
2
3 on:
4 workflow_call:
5
6 jobs:
7 ui-app:
8 uses: ./.github/workflows/wf-build-ui-app.yml
9 with:
10 release: true
11 upload-artifact: true
12
13 ui-internal:
14 uses: ./.github/workflows/wf-build-ui-internal.yml
15 with:
16 release: true
17 upload-artifact: true
Not much going on here, we are simply calling our previously defined reuseable workflows.
We can now update our cd-deploy.yml
workflow to call our new wf-build.yml
workflow. To do this, we extend the existing file by adding a build-artifacts
job as well as mark our stage-1
job as needs: [build-artifacts]
:
1 # ...
2 jobs:
3 # Build deployment artifacts
4 build-artifacts:
5 name: "Build Artifacts"
6 uses: ./.github/workflows/wf-build.yml
7
8 # Stage 1 tests that the deployment is working correctly.
9 stage-1:
10 name: "Stage 1"
11 needs:
12 uses: ./.github/workflows/wf-deploy.yml
13 # ...the rest is the same
The final change we need to make is to make our wf-deploy.yml
workflow download the artifacts we just built:
1 # ...
2 jobs:
3 deploy:
4 name: "Deploy"
5 runs-on: ubuntu-latest
6 environment: ${{ inputs.environment }}
7 # Limit to only one concurrent deployment per environment.
8 concurrency:
9 group: ${{ inputs.environment }}
10 cancel-in-progress: false
11 steps:
12 - uses: actions/checkout@v3
13 - uses: oven-sh/setup-bun@v1
14 - uses: extractions/setup-just@v1
15
16 - name: Setup artifact directory
17 run: mkdir -p ./deployment/artifacts
18
19 # Download all artifacts from previous jobs.
20 - uses: actions/download-artifact@v3
21 with:
22 path: ./deployment/artifacts/
23
24 - name: Display structure of downloaded files
25 working-directory: ./deployment/artifacts/
26 run: ls -R
27
28 - name: Validate artifacts
29 working-directory: ./deployment/artifacts
30 run: just deploy-validate-artifacts
31
32 - name: Install dependencies
33 working-directory: ./deployment
34 run: bun install
35 # ...the rest is the same
The new additions here are the steps:
extractions/setup-just@v1
: Make just
available in our workflow.Setup artifact directory
: Creating our artifacts folder.actions/download-artifact@v3y
: Download all uploaded assets into this folder.Display structure of downloaded files
: Very helpful for any debugging.Validate artifacts
: Make sure we have all the artifacts we need.The deploy-validate-artifacts
command is defined in our justfile
:
1 # Validate that all deployment artifacts are present.
2 :
3 4
If we remember our three groupings, we see that ui-app
and ui-internal
would fit into Services
:
Global
: "Global" (often us-east-1
) specific things such as ACM Certificates for CloudFront, and we'll also put Hosted Zones hereCloud
: Region specific infrequently changing things such as VPC, Region-specific Certificates, etcPlatform
: DynamoDB, Cache, SQSServices
: Lambdas, API Gateway, etcWe'd like our CloudFront distribution to use its own domain name and HTTPS, and to do this requires a bit of extra work on our end, since CloudFront needs our ACM Certificate to live in us-east-1
specifically.
This means that it will fit into our Global
stack. Cross-stack references in CDK/CloudFormation is a bit finicky, and we generally want to avoid relying on Exports
which are incredibly frustrating to work with and often gets you into sticky situations where you have to carefully destroy or update your stacks in a certain order. Once an export is used, it cannot change, leading to very tight coupling between stacks.
Instead, we will rely on the approach outlined in this nice article from AWS on how to Read parameters across AWS Regions with AWS CloudFormation custom resources.
We will essentially:
us-east-1
.AwsCustomResource
and AwsSdkCall
that can read parameters from a specific region.Services
stack to read the Certificate ARN from us-east-1
.Let's set up our new Certificate first. We'll adjust the existing GlobalStack
slightly in bin/deployment.ts
:
1 /**
2 * SSM Parameter name for the global certificate ARN used by CloudFront.
3 */
4 ;
5
6 /**
7 * Define our 'Global' stack that provisions the infrastructure for our application, such
8 * as domain names, certificates, and other resources that are shared across all regions.
9 *
10 * ```bash
11 * bun run cdk deploy --concurrency 6 'Global/**'
12 * ```
13 */
14 ;
15 if app, globalStackName 16 17 18 19 20 21 22 23 24 25 26
We've introduced GLOBAL_CERTIFICATE_SSM
which will be how we share the name of the parameter across stacks, and certificateArnSsm
as a property to our GlobalStack
.
Let's set up the certificate before we stitch it into our GlobalStack
. We'll create a new file lib/global/certificate.ts
:
1 ;
2 ;
3 ;
4 ;
5 ;
6
7 8 9 10 11 12 13 14 15 16 17
18
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
The last step in the stack stores the certificateArn
in the SSM Parameter Store.
Finally, we adjust lib/global/stack.ts
to now look like:
1 ;
2 ;
3 ;
4 ;
5
6
7
8 9 10 11 12 13 14 15 16 17 18 19
Instead of passing the Hosted Zone into the certificate stack, we explicitly mark the certificate as dependent on the domain stack to ensure the hosted zone exists before we try to access it. Again, avoiding exports.
Normally SSM doesn't take the region as a parameter, so to access the parameter from us-east-1
we'll set up a new construct in lib/services/ssm-global.ts
:
1 ;
2 ;
3 ;
4
5 6 7 8 9 10 11 12 13 14 15
16
17 /**
18 * Remove any leading slashes from the resource `parameterName`.
19 */
20 21 ;
22
23 /**
24 * Custom resource to retrieve a global SSM parameter. See https://aws.amazon.com/blogs/infrastructure-and-automation/read-parameters-across-aws-regions-with-aws-cloudformation-custom-resources/ for more information.
25 *
26 * You store your SSM Parameter as normal in any region:
27 * ```ts
28 * import { StringParameter } from 'aws-cdk-lib/aws-ssm';
29 *
30 * const cert = ...
31 *
32 * new StringParameter(this, 'CertificateARN', {
33 * parameterName: 'CloudFrontCertificateArn',
34 * description: 'Certificate ARN to be used with Cloudfront',
35 * stringValue: cert.certificateArn,
36 * });
37 * ```
38 *
39 * Example of retrieving it from another region:
40 * ```ts
41 * import { SsmGlobal } from './ssm-global';
42 *
43 * const certificateArnReader = new SsmGlobal(this, 'SsmCertificateArn', {
44 * parameterName: "CloudFrontCertificateArn",
45 * region: 'us-east-1'
46 * });
47 *
48 * // Get the value itself.
49 * certificateArnReader.value();
50 * ```
51 */
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
We now have everything we need to create our services.
Now we are ready to get our Services
stack set up!
All files will live in the deployment/
folder. We'll first adjust our bin/deployment.ts
, adding our Services
stack. Append the following at the end:
1 // ...
2 ;
3 // ...
4
5 /**
6 * Define our 'Services' stack that provisions our applications and services, such as our
7 * UI applications and APIs.
8 *
9 * ```bash
10 * bun run cdk deploy --concurrency 6 'Services/**'
11 * ```
12 */
13 ;
14 if app, servicesStackName 15 16 17 18 19 20 21 22 23 24 25 26 27
And our ServicesStack
is defined in lib/services/stack.ts
:
1 ;
2 ;
3
4 ;
5 ;
6
7 8 9 10 11 12 13 14 15 16 17
18
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
In here we deploy both ui-app
and ui-internal
the same way, but do some minor adjustments to the props we pass on to the stack to ensure it gets the right assets and also the right domain.
This brings us to our final part, which is the most lengthy, our lib/services/s3-website.ts
:
1 ;
2 ;
3 ;
4 ;
5 ;
6 ;
7 ;
8 ;
9 ;
10 ;
11 ;
12
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
54
55 /**
56 * Set up an S3 bucket, hosting our assets, with CloudFront in front of it.
57 */
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
And our Lambda@Edge function to rewrite urls is defined in edge-functions/rewrite-urls.js
:
1 exports.handler = 2 3 4 5 6 7 8 9 10 11 12 13 ;
There is quite a bit going on here. A rough overview of what is happening:
rewriteUrls
, we will also set up a Lambda@Edge function that will rewrite URLs to /folder/
-> /folder/index.html
. This is necessary to support the way Next.js generates its files.And that's it! Your ui-app
will now live at the root of your domain, e.g. app.example.com
, and ui-internal
will live at the subdomain internal
e.g. internal.app.example.com
.
Next up is to set up our Federated GraphQL API! Follow along in Part 4 of the series when that arrives!.