Building a Secure Containerized Microservice With .NET Core
This detailed tutorial will show you how to build and secure a containerized microservice using .NET Core, for use in cross-platform applications.
Join the DZone community and get the full member experience.
Join For FreeThe microservices craze is in full swing, and for good reason. It's not a silver bullet for every problem, but it's certainly becoming a practical solution for scalability and resilience in enterprise software systems.
The .Net Core project has also made some great progress in this arena, enabling you to make solid, cross-platform applications utilizing pre-written code from the .Net Core Framework. This enables you to develop lean microservices on a Windows, OSX or Linux workstation, and deploy them to either a Windows, OSX or Linux server. The ability to generate Linux binaries means you can take advantage of containerization on this platform.
Today I'm going to show how easy it is to build a REST Microservice in .Net Core 2 (Web API) and deploy it in a container to a Debian server. There's ample documentation covering parts of this process, but this is a comprehensive tutorial showing the process from start to finish.
Create the .Net Core Project
We will use the Dotnet CLI to create our application. For this, you will need:
A machine with the .NET Core SDK installed (Can be Windows, Mac or Linux)
A text editor (I'm using Visual Studio Code, but it's optional)
A way to test the Web API (I'm using POSTMan)
As a note, you can use Visual Studio in Windows to create this project with the built-in wizards and the result will be the same. I prefer doing it manually.
First, you need to create the project, I'm doing this from the command prompt.
dotnet new webapi -o friendlyphonenumber
This will scaffold out a new .Net Core Web API project. You'll have everything you need to be included in the /friendlyphonenumber directory. This works identically on all three OS platforms.
Set Up Serialization
Open up your friendlyphonenumber.csproj file and add the following in the itemgroup containing your package references:
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
<PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
This will add our Serialization services to the project. Then run
dotnet restore
This will enable you to control the serialization, specifically in naming the properties with your chosen format, as opposed to C# naming conventions.
Create Some Models
This service uses a REST API, and we're going to be sending JSON objects to it. Then it will process the data and return a new object using web requests. Although there are only single properties here we'll create a model for each object sent and returned.
Save the file. Now we'll create a container for the outgoing phone number that will be formatted in a similar fashion.
Create another class named FormattedPhoneNumber.cs. Make sure and include System.Runtime Serialization for these objects.
using System.Runtime.Serialization;
namespace friendlyphonenumber.Models {
[DataContract(Name = "FormattedPhoneNumber")]
public class FormattedPhoneNumber {
[DataMember(Name = "PhoneNumber")]
public string PhoneNumber {
get;
set;
}
}
}
Finally, let's create the class that's the core of the service, the phone number converter. This takes a Numeric phone number and converts it to our friendly text.
using System;
namespace friendlyphonenumber.Models {
public class PhoneNumberFormatter {
public long NumericPhoneNumber {
get;
set;
}
public string FormattedPhoneNumber {
get;
set;
}
public void ConvertPhoneNumber() {
FormattedPhonenumber = String.Format("{0:(###) ###-####}", NumericPhoneNumber);
}
}
}
This is a nice testable object we can extend later if we feel like.
Creating the Controller
Next, we'll create a controller. In the new project, delete ValuesControllers.cs in the controllers folder. That's the sample one added by the .Net CLI and we won't be using it.
Create a new class and call it FormatPhoneNumber.cs.
In this class, we'll create a single method that will take a POST with the numeric phone number in it, and return an object with the formatted object in it.
We'll add usings for Microsoft.AspNetCore.Mvc and our models.
Make the sure the class extends the Controller class for proper functionality.
using Microsoft.AspNetCore.Mvc;
using friendlyphonenumber.Models;
namespace friendlyphonenumber.Controllers {
[Route("api/[controller]")]
public class FormatPhoneNumber: Controller {
PhoneNumberFormatter formatter = new PhoneNumberFormatter();
public IActionResult Post([FromBody] NumericPhoneNumber phoneNumber) {
if (phoneNumber != null) {
formatter.NumericPhoneNumber = phoneNumber.PhoneNumber;
formatter.ConvertPhoneNumber();
FormattedPhoneNumber friendlyNumber = new FormattedPhoneNumber() {
PhoneNumber = formatter.FormattedPhoneNumber
};
return Ok(friendlyNumber);
} else {
return BadRequest();
}
}
}
}
The controller takes in a Post with an object formatted with a numeric number and returns an IActionResult with an object containing the "friendly" or formatted phone number.
Save these files, and now we're ready to run our application.
dotnet run
You should see something like this:
Our Web API is running on http://localhost:5000. So we'll do a quick smoke test with Postman.
In Postman, we create a simple POST that sends in raw JSON to http://localhost:5000/api/FormatPhoneNumber.
The object we send looks like this:
{
"PhoneNumber": "5035551212"
}
After the service processes this object, we get a nicely formatted result:
So now we know our service is working as expected. So let's publish a framework dependent build of the application:
dotnet publish -f netcoreapp2.0 -c Release
What this does is build an application that will run on any supported target and use the installed .Net Core framework from the machine.
A quick note: you can publish a self-contained deployment with a specified target, in our case it would Debian 9. It would publish everything needed to run, including the framework. It can be run on a machine without the .Net Core Framework installed. However these builds are larger by comparison, and since we're creating a microservice we want to build a smaller, leaner container that we can replicate to scale if needed.
After this build completes, we have our application's artifacts:
I'll use scp to transfer the files to my Debian machine:
scp -r * <yourusername>@<your host>:/home/<yourusername>/apps/friendlyphonenumber
Setting Up the Linux Host
We're now going to deploy the application to a Linux server. For this, you'll need:
A Linux Server connected to the internet (I'm using Debian 9)
The .NET Core SDK installed
Docker Installed
I have copied over my artifacts and the .NET Core SDK is installed, so I should be able to just run the DLL:
dotnet friendlyphonenumber.dll
And it appears to be running great.
But when we test it out, you'll quickly notice something.
If we check the response from localhost it produces output. It throws an error because we're not sending it JSON, but we can at least see the response is processed. If we try to access it externally:
You can see it's blocked and doesn't work. This is because our application is only listening on the localhost interface. We have some more steps to do with our application. Ideally, we should be using something like Nginx as a proxy, but that's outside the scope of this article so we'll set up the application to listen on the outside interface directly. Open up Program.cs in your project, and add the following to the create default builder method:
.UseKestrel(options => {
options.Listen(IPAddress.Any, 5000);
})
Your BuildWebHost method should now look like this:
Now, rebuild the project and transfer the new artifacts over. When we run the file again:
dotnet friendlyphonenumber.dll
And we can now access the server externally. So, we just got an email from our project manager, turns out this needs to be secured with SSL. As it turns out this isn't as big of a problem as it sounds.
Add SSL to Our Service
Now we need to generate a certificate to secure to our service, we'll use Let's Encrypt to build a certificate that we can use to secure our connections.
Note: These are distribution specific steps for setting up let's encrypt on Debian 9. You can skip this if you already have let's encrypt or have installed certificates on your server. If you're using a different distribution consult the documentation for setting up Let's encrypt on your server.
Next, we'll install Let's Encrypt, which is now a part of the Debian 9 distribution:
sudo apt-get install python-certbot-nginx -t stretch
Now, we'll run the certbot to just set up a certificate only without installing it to a web server:
sudo certbot certonly
Since I don't have a webserver installed, it asks how I want to handle the authentication piece, and I'll just select a temporary webserver (standalone):
After filling out the information, I now have a certificate created here:
/etc/letsencrypt/live/sandbox.jeremymorgan.com/fullchain.pem
Your location will vary depending on the URL you're using. Now, we need to convert this to a .pfx to use with Kestrel (the .Net Core web server). So to do that, I'll do the following to generate a .pfx cert in the application's folder:
sudo openssl pkcs12 -export -out friendlyphonenumber.pfx \
-inkey /etc/letsencrypt/live/sandbox.jeremymorgan.com/privkey.pem \
-in /etc/letsencrypt/live/sandbox.jeremymorgan.com/cert.pem \
-certfile /etc/letsencrypt/live/sandbox.jeremymorgan.com/chain.pem
Again, for your server, you'll want to replace "sandbox.jeremymorgan.com" with your domain name for the locations.
This will ask you for an export password, create one and make a note of it because we'll be using it in a minute.
Change the ownership of the .pfx so you can use it:
sudo chown jeremy friendlyphonenumber.pfx
Now we've generated a pfx file for this site. Next, we need to modify the application again to listen with an SSL connection.
In program.cs, replace the Kestrel options we added earlier with the following:
options.Listen(IPAddress.Any, 5001, listenOptions => {
listenOptions.UseHttps("friendlyphonenumber.pfx", "(your password)");
});
So it should now look like this:
And we'll have to add another package reference to our .csproj:
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Https" Version="2.0.1" />
Now rebuild and redeploy the app to the Linux host.
After this is completed we can only access the endpoint securely:
Now we're ready to put it into a container.
Building the Docker Container
We have Docker installed and configured on this server, so I want to build a container for this application.
Now I'm going to create a new directory for a docker container
~/containers/friendlyphonenumber
Next, I'll create an artifacts folder in this directory, and copy our binaries and certificate into it.
mkdir artifacts
cp -r ../../apps/friendlyphonenumber artifacts/
Now we'll create the docker file in our directory. This is going to be a fairly simple configuration:
FROM microsoft/aspnetcore:2.0
WORKDIR /app
COPY ./artifacts .
EXPOSE 5001
ENTRYPOINT ["dotnet", "friendlyphonenumber.dll"]
This file simply:
Starts with the aspnetcore base image
Creates a working directory
Copies our artifacts into the container
Opens the 5001 port
Runs the application
Now that we have our Docker file we'll build an image:
docker build -t friendlyphonenumber1 .
And it builds our first image.
So let's run it:
docker run -d -p 5001:5001 friendlyphonenumber1:latest
We're going to run this container detached and map port 5001 to 5001 on the host machine, and we hit it with curl and see it's available from the outside again, but this time running in a Docker container:
Now if we want or need to, we can spin up another identical container to this:
docker run -d -p 5002:5001 friendlyphonenumber1:latest --name friendlyphonenumber2
This container is identical but listens to port 5002 in addition to the one we have listening on 5001.
In fact, you could create a bunch of these and use something like Kubernetes to do load balancing and container management.
There are lots of possibilities here, and you can easily scale this application out to use a lot more containers and even more servers.
Conclusion
In this article, we covered creating an SSL Secured and Containerized Microservice with .Net Core. We covered the process from start to finish. If you were to build a production application for this you'd certainly want some better error handling, and use something like Nginx as a proxy, and Kubernetes to manage your containers. It's very easy to set up and the .Net Core package makes it really easy to build solid microservices you can scale to the cloud.
The source code and Docker files for this application are available here.
If you have any questions or remarks, feel free to leave them in the comments.
Opinions expressed by DZone contributors are their own.
Trending
-
Never Use Credentials in a CI/CD Pipeline Again
-
Future of Software Development: Generative AI Augmenting Roles and Unlocking Co-Innovation
-
Stack in Data Structures
-
DevOps vs. DevSecOps: The Debate
Comments