Jul 14 2024 - Based on the Basic version of AstroAspire, let's dive into how to orchestrate resources, environment variables, and telemetry in NodeJS web apps with .NET Aspire.
We’ll use AstroAspire (basic) to show how to share resources together within a .NET Aspire solution including Astro as our Nodejs application.
Using the Basic version of AstroAspire, we’re going to see how a Nodejs application like Astro can harness the benefits of .NET Aspire.
In a previous post, I explained why I started all of this and walked through the finished goods. Now, we’re going to breakdown how it all works and why it was developed in the way that it was.
A later post in this series will explain how to start this same kind of project from the beginning. For now, we’ll breakdown the AstroAspire basic project.
There is a GitHub repository where each variation of AstroAspire will be stored. The first one is called basic
, which is in the folder named, “Basic” and can be found
here: GitHub: conradagramont/AstroAspire
NOTE: While the project will open and work with Visual Studio 2022 (make sure to have the latest version), I did all of the project work using command line tools (running on Windows 11 and using PowerShell 7).
The .NET Aspire project is the orchestrator of all the resources. It is the main project that will be used to manage the environment variables, telemetry, and resources that will be
shared with the other projects.
There are some intrinsic connections between the .NET Aspire resources that are .NET based and even more that are .NET Aspire providers (e.g. Redis for caching).
Later, we’ll show how to connect many of those same services within the Nodejs side of our project with Astro.
If this is your first time using .NET Aspire with a .NET based project, I’d going through the Quickstart: Build your first .NET Aspire project which is close to everything in the AstroAspire API. The only difference is I used the MVC template and moved the weatherforecast.cs code and made it a controller. I’m just more familiar with it.
The Apphost file (Basic\AstroAspire.AppHost\Program.cs
) is where we describe the resources for which .NET Aspire will orchestrate the project for us. AppHost is where a developer can define what resources should be included which includes our code and resources like a MySQL database (not in this project sample at the moment).
During a deployment to Azure, the AppHost is inspected to define what resources may need to be provisioned or updated at that time. We’ll cover this more later.
Within the AppHost for this project, we define the .NET based API and our Nodejs project which is the Server-Side Rendering (SSR) of Astro. The AppHost will also handle sharing endpoints and other environment variables across all the resources that are defined to have them during run time. This includes using them in local development and adherence to them when deployed into Azure.
Below, you’ll see where we first define our API and tell .NET Aspire that we want to use external HTTP endpoints. This is so that one of the client side pages in Astro can call the API. This will eventually need some configuration for CORS, but we’ll get into that later.
You can also see below where we define the frontend project. We use the AddNpmApp
method to add the frontend project. This is because the frontend project is a Nodejs
project and we want to run the aspirerun
script that is defined in the package.json
file. This is ONLY good for local development. When deploying to Azure, the
PublishAsDockerFile
method will create a Docker image of the frontend project. This is because Azure App Services doesn’t support running Nodejs projects directly.
Furthermore, we pass the frontend reference to the API which will be accessible to the frontend project as an environment variable.
Within the Basic\AstroFrontend\src\pages\weatherssr.astro
page, we can see this in use with the following code:
This value is passed to the frontend project from the AppHost. This is how we can share environment variables across all the resources that are defined in the AppHost.
.NET Aspire Service Defaults (Basic\AstroAspire.ServiceDefaults\Extensions.cs
) will handle common functions that will be injected to all of our .NET projects (in this case, we only have one) for shared implementations for health checks and telemetry.
In the case of our frontend running in Nodejs, we don’t get those injected for us. However, you’ll see later how it will participate.
Let’s first take a look at what each pages does within the web UI.
When the Astro project is built, it generates static HTML files that are served by a static web server. The Astro project also generates Nodejs files that will be used by the SSR server (Nodejs/Express) to render the pages. The SSR server will use the Nodejs files to render the pages and serve them to the client.
Mermaid:
For pages that will be pre-rendered during the build process, Astro will generate the HTML files which will be served up by Nodejs/Express server.
For pages that will be rendered on the server (aka Server Side Rendering), Astro will generate the Nodejs files that will be used by the SSR server to render the pages.
For the purposes of this project, the following pages were created to demonstrate different methods of rendering and fetching data.
src/pages/weatherstatic.astro
src/pages/weatherapidirect.astro
src/pages/weatherastroapi.astro
src/pages/weatherssr.astro
Astro pre-renders the pages at build time. This means that the pages are generated as static HTML files that are served by a static web server.
For pages that are statically built, Astro will generate the HTML files. The HTML files are then served as static files.
Within AstroAspire, there several pages that are statically built.
weatherstatic.astro is a static page and is pre-rendered.
Mermaid:
weatherapidirect.astro is a static page and is pre-rendered. When a user visits the page, the static html page is served up quickly, then the page will call the AstroAspire API directly to get the weather data. The Javascript will then update the page with the data.
Mermaid:
Note: This page required enabling CORS in the AstroAspire.API project and updates in AppHost as well. This is because the AstroAspire API is called directly from the client which is a different port.
weatherastroapi.astro is a static page and is pre-rendered. This is the same as the weatherapidirect.astro page but the page will call the Astro API instead to get the weather data. The Astro API will then call the AstroAspire API to get the weather data. The Astro API will then return the data to the page.
Mermaid:
We have one special page that will be rendered on-demand. This page is rendered on the server and will call the AstroAspire API to get the weather data. The page is still rendered using the benefits of the Astro framework, but the data is fetched on-demand and so is the resulting html page.
weatherssr.astro is a page that is rendered on-demand. When the user vists the page url, the page is rendered on the server by Nodejs/Express and the data is fetched from the AstroAspire API. The page is then served to the client.
Mermaid:
Let’s focus on the configuration of the Astro project. While the focus here will be on Astro, you might find this useful for other Nodejs applications. The implementation we’ll use will be based on Expressjs and guidance from Astro’s documentation for Nodejs SSR with middleware.
In the Basic\AstroFrontend\astro.config.mjs
file, we’ll inform Astro that when we build, it’s to run as a using node as our middleware.
With the hybrid
as the output, we’re instructing it to build everything as a static page unless we tell it not to. If all or most of your pages will be expected to run server side, you can change that to server
.
An example of this is the Basic\AstroFrontend\src\pages\weatherssr.astro
where we set a value to inform Astro NOT to build this as a static page.
We’ll create a new page at the root of the Astro project (Basic\AstroFrontend\app.js
) which will be the main entry point when our application runs locally AND from within the Docker image that is then pushed to Azure.
Here’s the app.js
file for our project.
In the \Basic\AstroFrontend\package.json
, we have a few scripts that will be helpful for local development, including nodemon. Yet, when we go to production, we still use the
default build
script defined by a typical Astro install.
You’ll notice that we import ./instrumentation.js
which is a file we’ll discuss later regarding health checks and telemetry.
For local development, we’ll use Nodemon which is included in the script aaWatch
. We import Nodemon as a dev dependency, thus it will not be in our deployment into Azure.
The ./instrumentation.js
file we use is based on the example file
provided by Microsoft.
The instrumentation file is where we pull in capabilities from the OpenTelemetry project. This gives us the capabilities to add tracing information which the AppHost will orchestrate when a given trace spans beyond our Astro/Nodejs process.
Below is an example from the Basic\AstroFrontend\src\pages\api\get-astroaspireapi.js
file which is Astro API. You see in the Highlighted code where we start a span and set attributes. Make
sure to end the span at the end of the function. This is important for the tracing to work correctly.
Cross-Origin Resource Sharing (CORS) is something we need to address as we have our .NET API that we told .NET Aspire to provide an external endpoint for.
In this AstroAspire Basic project, we have one of our pages calling the .NET API directly. This could be an example where you DO want the frontend giving an HTML that maybe hits an https://api.yourdomain.com as a valid operation.
CORS by default will stop this from happening without some configuration.
In the Basic\AstroAspire.API\Program.cs
file, we added code that said we allow all CORS to come in. Before you say this is bad security, which I agree with at first, be patient.
Doing this will continue to allow the development to continue as needed. When you deploy to CI/CD, production or anything else, this same rule will apply.
However, this is actually good as we don’t want to hardcode any endpoints directly into our code. I tried this by telling the App.Host to send the frontend as a reference which I could add to the policy within the API. This worked great, until I went to deploy to Azure. Since the frontend doesn’t actually exist (keep in mind that our frontend is not .NET but a NodeJS deployment which has yet to be built into an image), the deployment fails.
Here is what I did to add this as a parameter to the API. I kept it in the project so you can see how it’s done, but we don’t use it.
The solution to this, which is actually better for the operations team, is that we leave the configuration of the allowed origins within the api
Container App directly.
While there are many cli and scripts that can do this, I’ll give you the following two:
api
under Settings > CORS>corsPolicy:
and associated settings directly into the bicep file used for deployment.Here is a link to Microsoft documentation that explains this process further.
Generate Bicep from .NET Aspire project model
We’re going to shortcut and get right to it. From within the folder location of the AppHost, run the following commands
Now we’re going to add in the below highlighted section to the file Basic\AstroAspire.AppHost\infra\api.tmpl.yaml
that the azd infra synth
exported to us.
Because this is YAML, the order and spacing of information is vital.
Earlier, I showed where we added the “ file to allow our frontend to participate in the tracing orchestration. I also explained earlier how the ServiceDefaults injects this into all of the .NET projects that are associated with the .NET Aspire AppHost. When running things locally, everything probably works as expected. However, once deployed to Azure, you might not see tracing flowing like it did locally. What we needed to do is tell the Service Defaults of tracing source names that will be used.
Here is what this looks like in the Basic\AstroAspire.ServiceDefaults\Extensions.cs
file:
When you’re doing local development and running .NET Aspire locally, our configuration tells the AppHost to run Nodejs and our service directly.
The script we defined in Basic\AstroFrontend\package.json
that the AppHost is referring to is specific to use Nodemon. We’re doing this to allow changes to our Astro file to be watched and restart the service. It’s not exactly as awesome as the fast reload that Astro does normally with npm run dev
but a little browser refresh isn’t bad.
To run it locally, let’s run the following command from with the Basic\AstroAspire.AppHost
folder:
This should start up our project and show you a URL where you can open in your local browser.
We’re going to do a deployment to Azure both directly from our desktop and via CI/CD with GitHub Actions. In either case, we need to have a docker file created that will work with our Astro Nodejs/Express configuration. While you don’t need the docker file for this sample project just to get it to run, when we deploy to Azure, it’s a must.
When deploying to Azure from a local workstation, or maybe a Windows server for some reason, we need to tell docker how to build our Nodejs application and finish off with the command to start it.
In our project, we already have the Basic\AstroFrontend\Dockerfile
created (yes, with no extension) which is in the root level of our Nodejs application.
Here is what our dockerfile looks like and you can that we still call build
and the last command is to start the node process and import the /app/instrumentation.js
with /app/app.js
as our main entry point to Express.
Azure Developer Cli
First, we need to initialize our deployment. This command is tied to your locally cloned repo (or just your local folder if you don’t have it in a repository just yet).
Let’s run the following command from with the Basic\AstroAspire.AppHost
folder:
Let’s assume that you followed the directions and all went well.
Let’s run the following command from with the Basic\AstroAspire.AppHost
folder:
You’ll notice that during the deployment of the frontend Nodejs app, that you’ll see some docker commands execute. If all goes well, that deployment is now an Azure Container App!
If not, then we have some configuration issues to tend with.
Much to my surprise and delight, deployment from GitHub Actions to Azure was pretty simple. Using the same process that I previously posted,
DevOps intro to deploy .NET Aspire CI/CD to Azure with GitHub Actions,
everything just worked. I didn’t have to do anything special for Docker!
The GitHub Actions workflow files is in the Basic\.github\workflows' folder will need to be at the top of your repo in order for GitHub to find it. Since I plan to have other variations of AstroAspire, I kept the workflow files in the
Basic` folder.
Here is a quick summary of key takeaways that I wish someone gave me before I started down this track:
I’m sure you’ve already checked out the code in GitHub: conradagramont/AstroAspire, and maybe already cloned it. If not, you should give it a go.
Do the following:
I do plan on a few more posts on the Basic
version that I think could use more details. I’ll also see if there are any requests from others found at below or in the various
Discord and other community locations.
Feel free to leave a comment below. Keep in mind that all comments are moderated and will be approved before being published.