This exercise will cover the following:
- Building a simple web service with .Net core
- Deploying the service to PCF
- Using Steeltoe to bind to a redis cloud instance
- Doing a blue/green deployment with PCF
- Install the .Net core SDK from this URL: https://dotnet.microsoft.com/download
- Verify the install by opening a terminal or command window and typing
dotnet --version. You should see a version string to match the version you installed
- Install the Cloud Foundry CLI from this URL: https://docs.cloudfoundry.org/cf-cli/install-go-cli.html
- Verify the install by opening a terminal or command window and typing
cf --version. You should see a version string to match the version you installed
- Install either Visual Studio IDE or Visual Studio Code from this URL: https://visualstudio.microsoft.com/
- If you choose Visual Studio Code, also install the C# extension
If you are using a private installation of PCF, then obtain credentials and API enpoint information from your PCF platform team. If you are using Pivotal Web Services (the public PCF instance hosted by Pivotal), then go to https://run.pivotal.io/ and register for a free account.
Once you have credentials, login with the CLI...
- Open a terminal or command window and login to PCF with the command
cf login -a api.run.pivotal.io(or whatever API endpoint you are using if not Pivotal Web Services) - Enter the email you registered and the password you set
-
Create a basic web service project and open it in VS Code
mkdir PaymentService cd PaymentService dotnet new webapi dotnet add package Swashbuckle.AspNetCore -v 5.0.0 dotnet add package Microsoft.Extensions.Caching.Redis -v 2.2.0 dotnet add package Steeltoe.Management.CloudFoundryCore -v 2.4.0 dotnet add package Steeltoe.Extensions.Configuration.CloudFoundryCore -v 2.4.0 dotnet add package Steeltoe.CloudFoundry.ConnectorCore -v 2.4.0 dotnet add package Steeltoe.Extensions.Logging.DynamicLogger -v 2.4.0 code .
-
Run the new web service with
dotnet run(or just press F5 in Visual Studio/Code), then navigate to https://localhost:5001/WeatherForecast -
Stop the service with
ctrl-c(or press the stop button in Visual Studio/Code)
-
Create a new
Servicesdirectory -
Create a new class
PaymentServicein theServicesdirectory, set its contents to the following:using System; namespace PaymentService.Services { public class PaymentCalculator { public Decimal Calculate(double Amount, double Rate, int Years) { if (Rate == 0.0) { return CalculateWithoutInterest(Amount, Years); } else { return CalculateWithInterest(Amount, Rate, Years); } } private Decimal CalculateWithInterest(double Amount, double Rate, int Years) { double monthlyRate = Rate / 100.0 / 12.0; int numberOfPayments = Years * 12; double payment = (monthlyRate * Amount) / (1.0 - Math.Pow(1.0 + monthlyRate, -numberOfPayments)); return ToMoney(payment); } private Decimal CalculateWithoutInterest(double Amount, int Years) { int numberOfPayments = Years * 12; return ToMoney(Amount / numberOfPayments); } private Decimal ToMoney(double d) { Decimal bd = new Decimal(d); return Decimal.Round(bd, 2, MidpointRounding.AwayFromZero); } } }
-
Create a new interface
IHitCountServicein theServicesdirectory, set its contents to the following:namespace PaymentService.Services { public interface IHitCountService { long GetAndIncrement(); void Reset(); } }
-
Create a new Class
MemoryHitCountServicein theServicesdirectory, set its contents to the following:namespace PaymentService.Services { public class MemoryHitCountService: IHitCountService { private long HitCount = 0; public long GetAndIncrement() { return ++HitCount; } public void Reset() { HitCount = 0; } } }
-
Create a new Class
CrashServicein theServicesdirectory, set its contents to the following:using System; using System.Threading.Tasks; namespace PaymentService.Services { public class CrashService { public void CrashIt() { // ends the app after a 2 second delay Task.Run(async delegate { await Task.Delay(2000); Environment.Exit(22); }); } } }
-
Create a new
Modelsdirectory -
Create a new class
CalculatedPaymentin theModelsdirectory, set its contents to the following:namespace PaymentService.Models { public class CalculatedPayment { public double Amount {get; set;} public double Rate {get; set;} public int Years {get; set;} public decimal Payment {get; set;} public string Instance {get; set;} public long Count {get; set;} } }
-
Create a new class
PaymentControllerin theControllersdirectory, set its contents to the following:using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using PaymentService.Models; using PaymentService.Services; using Steeltoe.Extensions.Configuration.CloudFoundry; namespace PaymentService.Controllers { [Route("/[controller]")] [ApiController] public class PaymentController { private PaymentCalculator PaymentCalculator; private CloudFoundryApplicationOptions AppOptions; private IHitCountService HitCountService; private readonly ILogger _logger; public PaymentController(PaymentCalculator paymentCalculator, IOptions<CloudFoundryApplicationOptions> appOptions, IHitCountService hitCountService, ILogger<PaymentController> logger) { PaymentCalculator = paymentCalculator; AppOptions = appOptions.Value; HitCountService = hitCountService; _logger = logger; } [HttpGet] public ActionResult<CalculatedPayment> calculatePayment(double Amount, double Rate, int Years) { var Payment = PaymentCalculator.Calculate(Amount, Rate, Years); _logger.LogDebug("Calculated payment of {Payment} for input amount: {Amount}, rate: {Rate}, years: {Years}", Payment, Amount, Rate, Years); return new CalculatedPayment { Amount = Amount, Rate = Rate, Years = Years, Instance = AppOptions.InstanceIndex.ToString(), Count = HitCountService.GetAndIncrement(), Payment = Payment }; } } }
-
Create a new class
ResetCountControllerin theControllersdirectory, set its contents to the following:using Microsoft.AspNetCore.Mvc; using PaymentService.Services; namespace PaymentService.Controllers { [Route("/[controller]")] [ApiController] public class ResetCountController { private IHitCountService HitCountService; public ResetCountController(IHitCountService hitCountService) { HitCountService = hitCountService; } [HttpGet] public ActionResult<string> ResetCount() { HitCountService.Reset(); return "OK"; } } }
-
Create a new class
CrashControllerin theControllersdirectory, set its contents to the following:using Microsoft.AspNetCore.Mvc; using PaymentService.Services; namespace PaymentService.Controllers { [Route("/[controller]")] [ApiController] public class CrashController { private CrashService CrashService; public CrashController(CrashService crashService) { CrashService = crashService; } /// <summary> /// Warning! Executing this API will crash the application. /// </summary> [HttpGet] public ActionResult<string> CrashIt() { CrashService.CrashIt(); return "OK"; } } }
-
Modify
Startup.csby adding the following lines at the end of theConfigureServicesmethod:services.AddCors(); services.AddOptions(); services.ConfigureCloudFoundryOptions(Configuration); services.AddSingleton<PaymentCalculator>(); services.AddSingleton<CrashService>(); services.AddSingleton<IHitCountService, MemoryHitCountService>();
-
Modify
Startup.csby adding the following line in theConfiguremethod prior to the existing lineapp.UseHttpsRedirection();:app.UseCors(builder => builder.AllowAnyOrigin());
-
Modify
Program.csby modifying theCreateHostBuildermethod so that it looks like this:public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); webBuilder.UseCloudFoundryHosting(); webBuilder.AddCloudFoundry(); });
-
Start the application either with the debugger (F5), or by entering the command
dotnet run -
Try the web service with the URL https://localhost:5001/payment?amount=100000&rate=4.5&years=30
-
Verify that a payment of $506.69 is returned
Swagger is a REST documentation and UI tool, that also includes code generation tools for clients. For us, it will act as a very simple and almost free UI for the web service we've just created. There are two implementations of swagger: Swashbuckle and NSwag. For this exercise, we will use Swashbuckle.
-
Modify
Startup.cs, add the following to the end of theConfigureServicesmethod:services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); });
-
Modify
Startup.cs, add the following to the end of theConfiguremethod:app.UseSwagger(); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); c.RoutePrefix = string.Empty; });
-
Start the application. The swagger UI should now be available at the application root (https://localhost:5001)
-
Notice that Swagger has documented all web services - the services we wrote as well as the default service generated by dotnet. If you want, you can delete the generated web service by deleting
WeatherForecastController.csandWeatherForecast.cs
-
Create a file
manifest.ymlin the project root directory. Set its contents to the following:applications: - name: PaymentService-1.0 random-route: true
-
cf push
During the push process, PCF will create a route for the app. Make note of the route - you can access the application at this URL once the application has started.
- Start the loan-calculator-client web page by using this URL: https://jeffgbutler.github.io/payment-calculator-client/
- Enter the URL to your application (like https://paymentservice-10-shy-zebra.cfapps.io) in the Base URL textbox
- Press the "Start" button. You should see random traffic being generated to the application. You should also see the all traffic is routed to instance 0
- Scale the app by entering the command
cf scale PaymentService-1.0 -i 2- this will request two instances of the app running. Eventually you should see traffic being routed to the two app instances. Notice that the hit count is not consistent. Why? - If you press the "Crash It!" button on the client page, then one of the app instances will crash. Which one depends on how the request was routed. Cloud Foundry will notice that an instance has crashed and will start a new instance automatically - this may take a few minutes to show
- You can scale the app back down by entering
cf scale PaymentService-1.0 -i 1
You should have noticed that the hit counter is not consistent among the instances, and that it is reset when an app instance crashes. This will demonstrate the idea of epehemeral containers and that Cloud Foundry is designed for stateless applications. We will store the hit count in an external Redis cache in a later section to correct this issue.
-
Modify
Program.cs, add the following to theCreateHostBuilderafter the thewebBuilder.AddCloudFoundry();call:public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); webBuilder.UseCloudFoundryHosting(); webBuilder.AddCloudFoundry(); webBuilder.AddCloudFoundryActuators(); // added });
-
Modify
appsettings.jsonand add the following:"info": { "app": { "name": ".Net Core Payment Calculator" } }
-
Start the application. You should be able to access the management enpoints at https://localhost:5001/actuator
-
cf push
- Login to Pivotal Apps Manager at https://run.pivotal.io/ - or your local instance of Cloud Foundry
- Inspect the application...specifically:
- On the app overview page, you should see the Steeltoe logo
- On the app overview page you should be able to inspect details of the app health
- On the logs page you should see the recent logs for the application, and be able to change logging levels
- On the threads page you should be able to obtain a thread dump
- On the settings page, there should be a Steeltoe Info section. In that section you should be able to see the text you added to the info actuator by pressing the "View Raw JSON" button
-
Create an Redis service instance in your PCF space. Name the service instance
xxxrediswhere "xxx" is your initials- If you are using Pivotal Web Services, add a Redis Cloud instance using the 30MB (free) plan
- If you are using a private PCF installation, configure a Redis instance as directed by your local team
-
If you are using Azure Redis Cache through the Azure Service Broker on PCF, then modify
appsettings.jsonand add the following configuration:"redis": { "client": { "urlEncodedCredentials": true } }
-
Create a class
RedisHitCountServicein theServicesfolder. Set its contents to the following:using StackExchange.Redis; namespace PaymentService.Services { public class RedisHitCountService: IHitCountService { private IConnectionMultiplexer _conn; public RedisHitCountService (IConnectionMultiplexer conn) { _conn = conn; } public long GetAndIncrement() { IDatabase db = _conn.GetDatabase(); return db.StringIncrement("loan-calculator"); } public void Reset() { IDatabase db = _conn.GetDatabase(); db.StringSet("loan-calculator", 5000); } } }
-
Modify the constructor in
Startup.csto accept and keep theIWebHostEnvironment. The modified constructor and new instance variable look like this:public Startup(IConfiguration configuration, IWebHostEnvironment env) { Configuration = configuration; Env = env; } public IWebHostEnvironment Env {get; }
-
Change the
ConfigureServicesmethod so that the memory based hit counter is used in the development environment and the Redis based hit counter is used in other environments:if (Env.IsDevelopment()) { services.AddSingleton<IHitCountService, MemoryHitCountService>(); } else { services.AddRedisConnectionMultiplexer(Configuration); services.AddSingleton<IHitCountService, RedisHitCountService>(); }
-
Modify
manifest.ymlto add the service binding (change the app version, and specify the correct name of the redis instance you created above):applications: - name: PaymentService-1.1 random-route: true services: - xxxredis
-
cf push -
Exercise the application as before. You should now see the hit counter remaining consistent across instances and after crashes.
-
Create a new route with the cf cli:
cf create-route your_space apps.pcfpoc.jgbpcf.com --hostname xxx-loancalculator
Where xxx is your initials
-
Assign the route to version 1.0 of the app:
cf map-route PaymentService-1.0 apps.pcfpoc.jgbpcf.com --hostname xxx-loancalculator
-
Verify that the app responds to the new URL.
-
Assign the route to version 1.1 of the app:
cf map-route PaymentService-1.1 apps.pcfpoc.jgbpcf.com --hostname xxx-loancalculator
Where xxx is your initials
-
Repeatedly try the app at the new route. You should see traffic bouncing back and forth between the two versions of the app
-
Remove the route from version 1.0 of the app:
cf unmap-route PaymentService-1.0 apps.pcfpoc.jgbpcf.com --hostname xxx-loancalculator
Where xxx is your initials
-
You should now see traffic only going to the new version of the app
-
Delete version 1.0 of the app:
cf delete PaymentService-1.0 -r
This deletes the app and its route