.NET 8, Aspire, & Aspir8: Deploy Microservices Into Dev Environments Effortlessly with CLI — No Dockerfiles or YAML Needed! Plus, a Little Yarp

Joseph Whiteaker
12 min readJan 10, 2024

--

Background

Kubernetes has emerged as a cornerstone in the future of cloud computing. Its status as a cloud-agnostic container orchestration service, available from every major cloud provider and suitable for on-premise hosting, underlines its versatility. A key aspect of Kubernetes’ appeal is its ability to avoid vendor lock-in, providing unparalleled flexibility and control over deployment environments.

My personal journey with Kubernetes began with a keen interest, fueled by its potential and capabilities. However, I quickly realized that the complexity of reading Kubernetes documentation with no context could be quite daunting, especially for those new to the platform.

The good news is that the landscape is evolving. The development of new tools and frameworks is progressively simplifying complex processes. From open telemetry to OSS APM tools, and comprehensive UIs and ecosystems such as Rancher and Red Hat OpenShift, the Kubernetes ecosystem is becoming more accessible. These advancements imply that developers are no longer required to understand every nuance of Kubernetes to leverage its potential.

Aspire & Aspir8

With solutions like Aspire and Aspir8, the landscape becomes even more accessible. Aspire provides a developer-friendly UI to aid in development, while Aspir8 offers a CLI tool for deploying microservices. These tools harness Kubernetes’ open-source, cloud-agnostic nature, making it easier for developers to deploy their applications across any cloud environment.

In this tutorial, we’ll explore how to create and manage an Aspire project. By the end of this guide, you’ll learn to use Aspir8 for building Docker images and pushing them to a local Docker registry, a crucial skill for container based development.

Step 1: Setting Up Your Project First, visit our GitHub Project Page. Here, you have two options: If you’re interested in building the Aspire project from the beginning, go and create your own empty Aspire project.

Alternatively, if your focus is more on Kubernetes deployment automation, you can simply pull the code from the repository and skip this step entirely. If you wish to skip this step, simply ctrl + f to ‘Time to Automate Kubernetes Deployments with Aspir8!’.

Step 2: Building and Deploying With the project files in hand, we’ll walk through the process of using Aspir8 to build Docker images. You’ll see how to efficiently manage these images in your local Docker registry. This guide aims to provide clear, step-by-step instructions to ensure a smooth learning experience.

Let’s get started and see how these tools can enhance your project’s efficiency and scalability.

In order to effectively follow this tutorial and achieve the intended outcomes, it is necessary to have specific tools and resources prepared in advance. Please ensure you have the following downloaded and set up:

1. Rancher Desktop by SUSE: While Docker Desktop is an alternative, Rancher Desktop is recommended for optimal compatibility with our objectives. Download Rancher Desktop

3. .NET 8: Essential for this project, .NET 8 can be downloaded for Linux, macOS, and Windows. Download .NET 8.0 from Microsoft

4. Aspire Tooling for .NET: This is a key element of our project. Learn about .NET Aspire tooling on Microsoft Learn

5. Aspir8: A global tool necessary for deployment processes. Instructions for Installing Aspir8

Please ensure that these tools and resources are installed and functioning correctly before starting the tutorial.

Quick Overview of Aspire Project Structure

In this tutorial, we’ll begin with a basic Aspire project setup, which includes two default components:

1. App Host: This is the core of our project. It initiates and manages the projects, containers, and executables. Within the App Host, we define the service relationships, outlining which services communicate with each other. Here’s an example of what an App Host project program.cs file could look like:

// Authentication/Authorization
var authenticationDb = builder.AddPostgresContainer("authentication-db", 5432)
.AddDatabase("authentication");

var authenticationdbApp = builder
.AddProject<Projects.ScaleStoreAuthenticationDb>("authentication-dbapp")
.WithReference(authenticationDb);

var authenticationHttpApi = builder
.AddProject<Projects.ScaleStoreAuthenticationWebApi>("authentication-webapi")
.WithReference(authenticationDb);


// Scaling
var scalingDb = builder.AddPostgresContainer("scalestore-db", 5433)
.AddDatabase("scalestore");


var scalingdbApp = builder.AddProject<Projects.ServiceScalingDb>("scalestore-dbapp")
.WithReference(scalingDb);

var scaleStoreHttpApi = builder.AddProject<Projects.ServiceScalingWebApi>("scalestore-webapi")
.WithReference(scalingDb)
.WithReference(authenticationHttpApi);

// web ui
var webUi = builder.AddProject<Projects.ScaleStoreWebUI>("scalestorewebui")
.WithReference(scaleStoreHttpApi);

builder.Build().Run();

2. Service Defaults: These are extension methods designed to streamline the integration of essential features like Prometheus, logging, health checks, and APM into our applications. These methods are straightforward, requiring no arguments, thereby simplifying the enhancement of our application’s functionality.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;

namespace Microsoft.Extensions.Hosting;

public static class Extensions
{
public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
{
builder.ConfigureOpenTelemetry();

builder.AddDefaultHealthChecks();

builder.Services.AddServiceDiscovery();

builder.Services.ConfigureHttpClientDefaults(http =>
{
// Turn on resilience by default
http.AddStandardResilienceHandler();

// Turn on service discovery by default
http.UseServiceDiscovery();
});

return builder;
}
// ...
}


// a web app that will use this extension method like so
// builder.AddServiceDefaults();
// without needing to add each and every little piece

// ...

As we progress through the tutorial, we’ll delve deeper into each component, understanding their roles and functionalities in our Aspire project.

Tutorial Time

In this tutorial, we’re expanding our microservices architecture to utilize a reverse proxy with two microservices:

1. Microservices Setup: We will have two distinct microservices. Each microservice will respond with simple text messages for demonstration purposes.

2. API Gateway Configuration:

  • The root route ("/") on our API Gateway will redirect to the root route ("/") of Microservice 1.
  • The "/admin" route on the API Gateway will navigate to the root route ("/") of Microservice 2.
  • Additionally, we’re adding a new route: "/admin/about". This will direct to the "/about" route of Microservice 2, illustrating a more complex routing scenario.

This setup allows us to effectively showcase the dynamic routing capabilities of YARP, handling various endpoints and directing them to the appropriate microservice.

Our End Goal Project Setup

Let’s start by setting up our Aspire project and creating the first microservice, microservice1.

Microservice 1 Setup:

1. Integrate Service Defaults: Include the service defaults in microservice1 for streamlined functionality.
2. Add to App Host: Incorporate microservice1 into the `program.cs` file of the App Host. Here’s how it should look:

var builder = DistributedApplication.CreateBuilder(args);

var microservice1 = builder.AddProject<Projects.microservice1>("microservice1");

var app = builder.Build();

app.Run();

Configuring microservice1:
Next, let’s set up the `program.cs` file in microservice1. Our objective is to establish a basic web application with a single route:

var builder = WebApplication.CreateBuilder(args);

// Add service defaults for essential features
builder.AddServiceDefaults();

var app = builder.Build();

// Define a GET endpoint
app.MapGet("/", () => "Hello, World! This is microservice1! This is visible at the '/' route!");

// Map default endpoints
app.MapDefaultEndpoints();
app.Run();

This configuration sets up microservice1 with a simple endpoint that returns a greeting, demonstrating its functionality at the root route ("/").

Once you’re set up with the App Host project, it’s time to see your microservice in action. Follow these steps to get started:

  1. Run the Project: Navigate to the App Host project directory in your terminal or command prompt.
  2. Start the Service: Execute dotnet watch. This command will build and run your application, and it will also listen for any file changes, automatically recompiling and restarting the app.

After running dotnet watch, you should see an output similar to this:

Aspire Dev Dashboard

At this point, the service is operational, but we haven’t implemented extensive functionality yet. To test the current setup:

  • Access the Endpoint: Click on the provided endpoint link. This action should return the response from microservice1.

You should receive a message like:

Hello, World! This is microservice1! This is visible at the '/' route!

This confirmation indicates that microservice1 is correctly set up and responding as expected at its root route.

Setting Up Microservice 2:

  1. Create the Project: Start by creating the microservice 2 project.
  2. Add to Solution (sln): Include microservice 2 in the solution file.
  3. Reference in App Host: Ensure that the App Host project references microservice 2.
  4. Reference Service Defaults: Like before, include the service defaults in the microservice 2 project.

Configuring program.cs in Microservice 2: Modify the program.cs file in the microservice 2 project to look like this:

var builder = WebApplication.CreateBuilder(args);

// Add service defaults
builder.AddServiceDefaults();

var app = builder.Build();

// Map default endpoints
app.MapDefaultEndpoints();

// Define GET endpoints
app.MapGet("/", () => "This is Microservice 2! This is visible at the '/admin' route!");
app.MapGet("/about", () => "This is Microservice 2 about endpoint! This is visible at the '/admin/about' route!");

app.Run();

This setup defines two endpoints for microservice 2: the root ("/") and the "/about" route.

Integrate into App Host Project: Next, add microservice 2 to the App Host project’s program.cs file:

var builder = DistributedApplication.CreateBuilder(args);

var microservice1 = builder.AddProject<Projects.microservice1>("microservice1");
var microservice2 = builder.AddProject<Projects.microservice2>("microservice2");

builder.Build().Run();

After integrating microservice 2, restart the App Host project. You should now see both microservices appearing in the dashboard, indicating successful setup and integration.

Let’s proceed by creating a new web project named apigateway, which will serve as our API gateway. This project will reference the service defaults, and it will also be referenced by the App Host.

Setting Up API Gateway:

  1. Create the apigateway Project: Begin by establishing a new web project named apigateway.
  2. Reference Service Defaults: Ensure that apigateway references the service defaults project.

Configuring the apigateway Project: Navigate to the apigateway directory and modify the .csproj file as follows, or in a similar manner:

<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Yarp.ReverseProxy" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery" Version="8.0.0-preview.2.23619.3" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery.Abstractions" Version="8.0.0-preview.2.23619.3" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery.Yarp" Version="8.0.0-preview.2.23619.3" />
<PackageReference Include="Microsoft.Extensions.ServiceDiscovery.Dns" Version="8.0.0-preview.2.23619.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\IngressSpike.ServiceDefaults\IngressSpike.ServiceDefaults.csproj" />
</ItemGroup>
</Project>

Note:

  • The Yarp.ReverseProxy package is critical for adding reverse proxy capabilities.
  • The Microsoft.Extensions.ServiceDiscovery.* packages are new additions from the Aspire project, enabling advanced service discovery features.

Configuring the App Host for API Gateway: Now, let’s switch to the App Host configuration:

var builder = DistributedApplication.CreateBuilder(args);

// Add microservices
var microservice1 = builder.AddProject<Projects.microservice1>("microservice1");
var microservice2 = builder.AddProject<Projects.microservice2>("microservice2");

// Configure API Gateway
var apiGateway = builder.AddProject<Projects.apigateway>("apigateway")
.WithReference(microservice1)
.WithReference(microservice2);

builder.Build().Run();

Understanding Service Discovery in Aspire:

Familiarity with service discovery, akin to concepts in Docker and Kubernetes, is crucial for grasping the functionality of the API Gateway. Here’s an example to illustrate:

var app1 = builder.AddProject<Projects.microservice1>("project1service");
var app2 = builder.AddProject<Projects.microservice2>("project2service").WithReference(app1);

In this setup:

This approach mirrors the service referencing methods used in Docker Compose and Kubernetes, streamlining communication between services.

Returning to our tutorial with a clearer understanding of service-to-service discovery, our next step is to finalize the configuration of the apigateway project.

Configuring Routing in apigateway: Our goal is to set up the following routing:

  • The root route ("/") should direct to microservice 1.
  • The "/admin" route should lead to microservice 2.

Updating appsettings.json: To achieve this, update your appsettings.json file in the apigateway project as follows:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ReverseProxy": {
"Routes": {
"route1": {
"ClusterId": "cluster1",
"Match": {
"Path": "{**catch-all}"
}
},
"adminRoute": {
"ClusterId": "cluster2",
"Match": {
"Path": "/admin/{**catch-all}"
},
"Transforms": [
{
"PathRemovePrefix": "/admin"
}
]
}
},
"Clusters": {
"cluster1": {
"Destinations": {
"destination1": {
"Address": "http://microservice1",
"Health": "http://microservice1/readiness"
}
}
},
"cluster2": {
"Destinations": {
"destination2": {
"Address": "http://microservice2",
"Health": "http://microservice2/readiness"
}
}
}
}
}
}

Here is the program.cs file in apigateway

var builder = WebApplication.CreateBuilder(args);

builder.AddServiceDefaults();

builder.Services.AddReverseProxy()
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"))
.AddServiceDiscoveryDestinationResolver();

var app = builder.Build();


app.MapReverseProxy();


app.Run();

Learning More About YARP: If you’re new to YARP or need a refresher, these resources can be helpful:

Testing the Configuration:

  1. Run the App Host: Navigate back to the App Host, run dotnet watch, and visit the apigateway endpoint.
  2. Verify the Routes:
  • Accessing the "/" route should yield the same results as visiting the root endpoint of microservice 1.
  • Navigating to "/admin" on the API gateway should display results identical to the root endpoint of microservice 2.
  • Visiting "/admin/about" on the API gateway should show the same content as the "/about" endpoint of microservice 2.

By following these steps, you’ll be able to test and confirm the successful routing configuration of your API gateway.

We have now completed the setup of our project. Upon restarting the App Host and accessing the dashboard, you will see the following:

Aspire Dashboard

Time to Automate Kubernetes Deployments with Aspir8!

Next, we’ll focus on deploying our applications to a local Kubernetes cluster using Aspir8. You can find detailed documentation for Aspir8 here.

Setting Up a Local Docker Registry:

Before proceeding with Aspir8, let’s set up a local Docker registry. This eliminates the need to publish containers to Docker Hub, Azure Container Registry, ECR, Harbor, or GitLab Registry. Run the following command:

docker run -d -p 5001:5000 --restart always --name registry registry:2

This command creates a Docker registry accessible on port 5001.

Medium blog on why you should use a local registry for spikes and poc projects: Simplify Your Local Helm Workflow with a Local Docker Registry.

Deploying with Aspir8

  1. Generate the Manifest File: Navigate to the App Host project and execute:
dotnet run --publisher manifest --output-path manifest.json

This command generates a manifest.json file, which outlines the app graph - a useful representation of the system's components and their interactions.

2. Initialize Aspir8 by Executing:

aspirate init

This creates the aspirate.json file

3. Build the Project with Aspir8 by Executing:

aspirate build

This builds the resources defined in the manifest.json file.

4. Generate Kubernetes Files by Executing:

aspirate generate

This step creates Kubernetes files for the project.

5. Apply the Deployment: Finally, deploy by running:

aspirate apply

Congratulations! You have successfully deployed your three microservices using Aspir8 in your local Kubernetes environment.

Feeling after doing this the hard way

So, if you go to the Rancher Desktop Cluster Dashboard

Where to find cluster dashboard

And look at your deployments, then you will see something along the lines of this.

Rancher Desktop Cluster Dashboard

But how do we verify that our deployment is working?

Okay this is really easy. Go to the main window on Rancher Desktop and click on port forwarding on the left side. Go ahead and click forward on the api gateway http service and just hit the checkmark after. You should see something along the lines of this.

Port Forwarding Section of Rancher Desktop

Now go to the http://localhost:{Local Port — whatever port it shows for you}

For me obviously, it’s going to be http://localhost:52263

If you go to that url you should see this

Microservice 1 “/” Endpoint
Microservice 1 “/” Endpoint
Microservice 2 “/” Endpoint
Microservice 2 “/admin” Endpoint

Note: you could’ve done that by modifying the yaml file from a cluster ip to a node port (for local development), however, that would defeat the point of simplifying things.

Conclusion: Simplifying Kubernetes Deployments with Aspire & Aspir8

As we wrap up this comprehensive tutorial, notice how we didn’t once touch a docker, docker compose, or yaml file. It’s clear that the combination of Aspire and Aspir8 significantly simplifies the process of deploying microservices in a Kubernetes environment. By following the steps outlined in this guide, you have successfully navigated the complexities of container orchestration and service discovery, leading to a functional, scalable microservices architecture.

Key Takeaways:

  • Understanding Kubernetes: This journey began with an overview of Kubernetes, emphasizing its role as a cloud-agnostic platform and its significance in avoiding vendor lock-in.
  • Aspire & Aspir8 Tools: We explored how Aspire provides an intuitive UI for development and Aspir8 streamlines the deployment process, making Kubernetes more accessible for developers.
  • Project Setup and Deployment: Through the tutorial, you learned to set up an Aspire project, configure microservices, and use Aspir8 for efficient deployment to a local Kubernetes cluster.
  • Testing and Verification: By leveraging tools like Rancher Desktop, you’ve seen firsthand how to verify and manage your deployments, ensuring that your services are up and running.

The journey through Kubernetes, with its initial complexities, has led to a rewarding destination. The knowledge you’ve gained here empowers you to build and deploy microservices with confidence, leveraging the full potential of Kubernetes without being overwhelmed by its intricacies. This isn’t a replacement of learning Kubernetes but is a nice to have for those who are new to Kubernetes and those who are just looking to move really fast. However, just want to make it clear that this isn’t a full-on replacement for learning Kubernetes and is instead a tool in the toolbox to move fast, create sandbox environments really quickly, and worry about details when you need to later on.

As Kubernetes continues to evolve, remember that tools like Aspire and Aspir8 are here to simplify and enhance your development experience. The future of cloud computing is in your hands, and with these skills, you’re well-equipped to innovate and excel in this ever-changing landscape.

--

--