Category: .NET

Migrating www.forcyclistsbycyclists.com to AWS from Azure (Part 1)

If you follow me on Twitter you might have seen that as a side project I run a cycling performance analytics website called Performance For Cyclists – this is currently running on Azure.

Its built in F# and deploys to Azure largely like this (its moved on a little since I drew this but not massively):

It runs fine but if you’ve been following me recently you’ll know I’ve been looking at AWS and am becoming somewhat concerned that Microsoft are falling behind in a couple of key areas:

  • Support for .NET – AWS seem to always be a step ahead in terms of .NET running in serverless environments with official support for the latest runtimes rolling out quickly and the ability to deploy custom runtimes if you need. Cold starts are much better and they have the capability to run an ASP.Net Core application serverlessly with much less fuss.

    I can also, already, run .NET on ARM on AWS which leads me to my second point (its almost as if I planned this)…
  • Lower compute costs – my recent tests demonstrated that I could achieve a 20% to 40% saving depending on the workload by making use of ARM on AWS. It seems clear that AWS are going to broaden out ARM yet further and I can imagine them using that to put some distance between Azure and AWS pricing.

    I’ve poked around this as best I can with the channels available to me but can’t get any engagement so my current assumption is Microsoft aren’t listening (to me or more broadly), know but have no response, or know but aren’t yet ready to reveal a response.

(just want to be clear about something – I don’t have an intrinsic interest in ARM, its the outcomes and any coupled economic opportunities that I am interested in)

I’m also just plain and simpe curious. I’ve dabbled with AWS, mostly when clients were using it when I freelanced, but never really gone at it with anything of significant size.

I’m going to have to figure my way through things a bit, and doubtless iterate, but at the moment I’m figuring its going to end up looking something like this:

Leaving Azure Maps their isn’t a mistake – I’m not sure what service on AWS offers the functionality I need, happy to here suggestions on Twitter!

I may go through this process and decide I’m going to stick with Azure but worst case is that I learn something! Either way I’ll blog about what I learn. I’ve already got the API up and running in ECS backed by Fargate and being built and deployed through GitHub Actions and so I’ll write about that in my next post.

Compute “Bang for Buck” on Azure and AWS – 20% to 40% advantage on AWS

As I normally post from a developer perspective I thought it might be worth starting off with some additional context for this post. If you follow me on Twitter you might know that about 14 months ago I moved into a CTO role at a rapidly growing business – we’re making ever increasing use of the cloud both by migrating workloads and the introduction of new workloads. Operational cost is a significant factor in my budget. To me the cloud can be summarised as “cloud = economics + capabilities” and so if I have a similar set of capabilities (or at least capabilities that map to my needs) then reduction in compute costs has the potential to drive the choice of vendor and unlock budget I can use to grow faster.

In the last few posts I’ve been exploring the performance of ARM processors in the cloud but ultimately what matters to me is not a processor architecture but the economics it brings – how much am I paying for a given level of performance and set of characteristics.

It struck me there were some interesting differences across ARM, x86, Azure and AWS and I’ve expanded my testing and attempted here to present these findings in (hopefully) useful terms.

All tests have been run on CentOS Linux (or the AWS derivative) using the .NET 5 runtime with Apache acting as a reverse proxy to Kestrel. I’ve followed the same setup process on every VM and then run performance tests directly against their public IP using loader.io all within the USA.

I’ve run two workloads:

  1. Generate a Mandelbrot – this is computationally heavy with no asynchronous yield points.
  2. A test that simulates handing off asynchronously to remote resources. I’ve included a small degree of randomness in this.

At the bottom of the post is a table containing the full set of tests I’ve run on the many different VM types available. I’m going to focus on some of the more interesting scenarios here.

Computational Workload

2 Core Tests

For these tests I picked what on AWS is a mid range machine and on Azure the entry level D series machine:

AWS (ARM): t4g.large – a 2 core VM with 8GiB of RAM and costing $0.06720 per hour
AWS (x86): t3.large – a 2 core VM with 8GiB of RAM and costing $0.08320 per hour
Azure (x86): D2s v4 – a 2 core VM with 8GiB of RAM and costing $0.11100 per hour

On these machines I then ran the workloads with different numbers of clients per seconds and measured their response times and the failure rate (failure being categorised as a response of > 10 seconds):

Both Intel VMs generated too many errors at the 25 client per second rate and the load tester aborted.

Its clear from these results that the ARM VM running on AWS has a significant bang for buck advantage – its more performant than the Intel machines and is 20% cheaper than the AWS Intel machine and 40% cheaper than the Azure machine.

Interestingly the Intel machine on AWS lags behind the Intel machine on Azure particularly when stressed. It is however around 20% cheaper and it feels as if performance between the Intel machines is largely on the same economic path (the AWS machine is slightly ahead if you normalise the numbers).

4 Core Tests

I wanted to understand what a greater number of cores would do for performance – in theory it should let me scale past the 20 client per second level of the smaller instances. Having concluded that ARM represented the best value for money for this workload on AWS I didn’t do an x86 test on AWS. I used:

AWS: t4g.xlarge (ARM) – a 4 core VM with 16GiB of RAM and costing $0.13440 per hour
Azure: D4s_v4 – a 4 core VM with 16GiB of RAM and costing $0.22200 per hour

I then ran the workloads with different numbers of clients per seconds and measured their response times and the failure rate (failure being categorised as a response of > 10 seconds):

The Azure instance failed the 55 client per second rate – it had so many responses above 10 seconds in duration that the load test tool aborted the test.

Its clear from these graphs that the ARM VM running on AWS outperforms Azure both in terms of response time and massively in terms of bang for buck – its nearly half the price of the corresponding Azure VM.

Starter Workloads

One of the nice things about AWS and Azure is they offer very cheap VMs. The Azure VMs are burstable (and there is some complexity here with banked credits) which makes them hard to measure but as we saw in a previous post the ARM machines perform very well at this level.

The three machines used are:

AWS (ARM): t4g.micro, 2 core, 1GiB of RAM costing $0.00840 per hour
Azure (x86): B1S, 1 core, 1GiB of RAM costing $0.00690 per hour
AWS (x86): t3.micro, 2 core, 1 GiB of RAM costing $0.00840 per hour

Its an easy victory for ARM on AWS here – its performant, cheap and predictable. The B1S instance on Azure couldn’t handle 15 or 20 clients per second at all but may be worth consideration if its bursting system works for you.

Simulated Async Workload

2 Core Tests

For these tests I used the same configurations as in the computational workload.

Their is less to separate the processors and vendors with a less computationally intensive workload. Interestingly the AWS machines have a less stable response time with more > 10 second response times but, in the case of the ARM chip, it does this while holding a lower average response time while under load.

Its worth noting that the ARM VM is doing this at 40% of the cost of the Azure VM and so I would argue again represents the best bang for buck. The AWS x86 VM is 20% cheaper than the Azure equivelant – if you can live with the extra “chop” that may still be worth it or you can use that saving to purchase a bigger tier unit.

4 Core Tests

For these tests I used the same virtual machines as for the computational workload:

There is little to separate the two VMs until they come under heavy load at which point we see mixed results – I would argue the ARM VM suffers more as it becomes much more spiky with no consistent benefit in average response time.

However in terms of bang for buck – this ARM VM is nearly half the price of the Azure VM. There’s no contest. I could put two of these behind a load balancer for nearly the same cost.

Starter Workloads

For these tests I used the same virtual machines as for the computational workload:

Its a pretty even game here until we hit the 100 client per second range at which point the AWS VMs begin to outperform the Azure VM though at the 200 client per second range at the expense of more long response times.

Conclusions

Given the results, at least with these workloads, its hard not to conclude that AWS currently offers significantly greater bang for buck than Azure for compute. Particularly with their use of ARM processors AWS seem to have taken a big leap ahead in terms of value for money for which, at the moment, Azure doesn’t look to have any response.

Perhaps tailoring Azure VMs to your specific workloads may get you more mileage.

I’ve tried to measure raw compute here in the simplest way I can – I’d stress that if you use more managed services you may see a different story (though ultimately its all running on the same infrastructure so my suspicion is not). And as always, particularly if you’re considering a switch of vendor, I’d recommend running and measuring representative workloads.

Full Results

TestVendorInstanceClients per secondMinMaxAverageSuccessful ResponsesTimeouts> 10 secondsPrice per hour
MandelbrotAzureA2_V2 (x64)29179279346000.0%$0.10600
MandelbrotAzureA2_V2 (x64)51263664939755600.0%$0.10600
MandelbrotAzureA2_V2 (x64)101205102037985342138.2%$0.10600
MandelbrotAzureA2_V2 (x64)15ERROR RATE TOO HIGH#DIV/0!$0.10600
MandelbrotAzureA2_V2 (x64)20ERROR RATE TOO HIGH#DIV/0!$0.10600
AsyncAzureA2_V2 (x64)2017334325260000.0%$0.10600
AsyncAzureA2_V2 (x64)50196504274149800.0%$0.10600
AsyncAzureA2_V2 (x64)10023942402484179400.0%$0.10600
AsyncAzureA2_V2 (x64)20042389295475172500.0%$0.10600
MandelbrotAzureB1S (x86)2670255111715700.0%$0.00690
MandelbrotAzureB1S (x86)51612552132527200.0%$0.00690
MandelbrotAzureB1S (x86)1012591000171157222.7%$0.00690
MandelbrotAzureB1S (x86)15ERROR RATE TOO HIGH$0.00690
AsyncAzureB1S (x64)2020638326858000.0%$0.00690
MandelbrotAzureB1S (x86)20ERROR RATE TOO HIGH$0.00690
AsyncAzureB1S (x64)50209436278149800.0%$0.00690
AsyncAzureB1S (x64)10029231511892225200.0%$0.00690
AsyncAzureB1S (x64)20048277084474213600.0%$0.00690
MandelbrotAzureD1 v2 (x64)27488287876000.0%$0.08780
MandelbrotAzureD1 v2 (x64)52858424236467000.0%$0.08780
MandelbrotAzureD1 v2 (x64)1011921000175235758.1%$0.08780
MandelbrotAzureD1 v2 (x64)15ERROR RATE TOO HIGH$0.08780
MandelbrotAzureD1 v2 (x64)20ERROR RATE TOO HIGH$0.08780
AsyncAzureD1 v2 (x64)20$0.08780
AsyncAzureD1 v2 (x64)50168407244149900.0%$0.08780
AsyncAzureD1 v2 (x64)10024133981986215600.0%$0.08780
AsyncAzureD1 v2 (x64)20040791714927195100.0%$0.08780
MandelbrotAzureD2as_v425596045666000.0%$0.11100
MandelbrotAzureD2as_v455872606159613300.0%$0.11100
MandelbrotAzureD2as_v41013055920354113400.0%$0.11100
MandelbrotAzureD2as_v41513589607559612600.0%$0.11100
AsyncAzureD2as_v42020030523960000.0%$0.11100
MandelbrotAzureD2as_v4206381237974351043324.1%$0.11100
MandelbrotAzureD2as_v4251459102938850587054.7%$0.11100
AsyncAzureD2as_v450200312238149800.0%$0.11100
AsyncAzureD2as_v4100202347247300000.0%$0.11100
AsyncAzureD2as_v420029541292053427600.0%$0.11100
AsyncAzureD2as_v43003291126931904334230.5%$0.11100
AsyncAzureD2as_v440033817305397842472054.6%$0.11100
MandelbrotAWSt2.micro (x86)2675114010105800.0%$0.01160
MandelbrotAWSt2.micro (x86)5651532433327200.0%$0.01160
MandelbrotAWSt2.micro (x86)10186710193699956812.5%$0.01160
MandelbrotAWSt2.micro (x86)151445102039458324457.9%$0.01160
AsyncAWSt2.micro (x64)2024241229860000.0%$0.01160
MandelbrotAWSt2.micro (x86)201486102068895114078.4%$0.01160
AsyncAWSt2.micro (x64)50241545312149700.0%$0.01160
AsyncAWSt2.micro (x64)10024498292260198900.0%$0.01160
AsyncAWSt2.micro (x64)200347173753858211825210.6%$0.01160
MandelbrotAWSt3.micro (x86)27018857446000.0%$0.01040
MandelbrotAWSt3.micro (x86)58783313206910800.0%$0.01040
MandelbrotAWSt3.micro (x86)108558037449810300.0%$0.01040
MandelbrotAWSt3.micro (x86)159731020269308499.7%$0.01040
AsyncAWSt3.micro (x64)2023340227960000.0%$0.01160
MandelbrotAWSt3.micro (x86)201030102158495743532.1%$0.01040
AsyncAWSt3.micro (x64)502354912407149800.0%$0.01160
AsyncAWSt3.micro (x64)100235545292299400.0%$0.01160
AsyncAWSt3.micro (x64)20023417376259830892607.8%$0.01160
MandelbrotAWSt4g.large (ARM)26327796546000.0%$0.06720
MandelbrotAWSt4g.large (ARM)56982436175313700.0%$0.06720
MandelbrotAWSt4g.large (ARM)1019366284368213700.0%$0.06720
MandelbrotAWSt4g.large (ARM)1521209927562413300.0%$0.06720
MandelbrotAWSt4g.large (ARM)208651020774721003123.7%$0.06720
MandelbrotAWSt4g.large (ARM)25757102078432568058.8%$0.06720
AsyncAWSt4g.large (ARM)2023439828059900.0%$0.06720
AsyncAWSt4g.large (ARM)50229395275149800.0%$0.06720
AsyncAWSt4g.large (ARM)100236426287299200.0%$0.06720
AsyncAWSt4g.large (ARM)20031617359208040262606.1%$0.06720
AsyncAWSt4g.large (ARM)300241173813322306063917.3%$0.06720
AsyncAWSt4g.large (ARM)4003491312733464038108821.2%$0.06720
MandelbrotAWSt4g.micro (ARM)26187516386000.0%$0.00840
MandelbrotAWSt4g.micro (ARM)57652794170913200.0%$0.00840
MandelbrotAWSt4g.micro (ARM)107616958388213000.0%$0.00840
MandelbrotAWSt4g.micro (ARM)1575910203570412710.8%$0.00840
AsyncAWSt4g.micro (ARM)2023637127560000.0%$0.00840
MandelbrotAWSt4g.micro (ARM)208021020774591191410.5%$0.00840
AsyncAWSt4g.micro (ARM)502224178373149800.0%$0.00840
AsyncAWSt4g.micro (ARM)100231414286299400.0%$0.00840
AsyncAWSt4g.micro (ARM)20031017388202839952004.8%$0.00840
AsyncAzureD4s_v42016723920060000.0%$0.22200
AsyncAzureD4s_v450165242197149900.0%$0.22200
AsyncAzureD4s_v4100153243198300000.0%$0.22200
AsyncAzureD4s_v4200165270204600000.0%$0.22200
AsyncAzureD4s_v430020899621395790000.0%$0.22200
AsyncAzureD4s_v440021116283204976951141.5%$0.22200
MandelbrotAzureD4s_v423043343136000.0%$0.22200
MandelbrotAzureD4s_v4541567550015000.0%$0.22200
MandelbrotAzureD4s_v41048834501670238800.0%$0.22200
MandelbrotAzureD4s_v4154864371301225600.0%$0.22200
MandelbrotAzureD4s_v4207276572402723900.0%$0.22200
MandelbrotAzureD4s_v42514538024512723500.0%$0.22200
MandelbrotAzureD4s_v4308869282598823800.0%$0.22200
MandelbrotAzureD4s_v435613100056850196198.8%$0.22200
MandelbrotAzureD4s_v4401817133527905215146.1%$0.22200
MandelbrotAzureD4s_v44524121020786392044116.7%$0.22200
MandelbrotAzureD4s_v4507471020789538015866.4%$0.22200
MandelbrotAzureD4s_v455ERROR RATE TOO HIGH
MandelbrotAzureD2s v424594824696000.0%$0.11100
MandelbrotAzureD2s v458833449176412300.0%$0.11100
MandelbrotAzureD2s v4104806747405312300.0%$0.11100
MandelbrotAzureD2s v41548310202628611810.8%$0.11100
MandelbrotAzureD2s v420506102067636862824.6%$0.11100
MandelbrotAzureD2s v425ERROR RATE TOO HIGH$0.11100
AsyncAzureD2s v42016827020658000.0%$0.11100
AsyncAzureD2s v450164266205149900.0%$0.11100
AsyncAzureD2s v4100167310217300000.0%$0.11100
AsyncAzureD2s v420025938182233399000.0%$0.11100
AsyncAzureD2s v43002491560335923808120.3%$0.11100
AsyncAzureD2s v440033016811434139142034.9%$0.11100
MandelbrotAWSt3.large (x86)27118787536000.0%$0.08320
MandelbrotAWSt3.large (x86)57583150202411300.0%$0.08320
MandelbrotAWSt3.large (x86)1010237656439311500.0%$0.08320
MandelbrotAWSt3.large (x86)15234010202661510443.7%$0.08320
MandelbrotAWSt3.large (x86)202453104068479475453.5%$0.08320
MandelbrotAWSt3.large (x86)25ERROR RATE TOO HIGH$0.08320
AsyncAWSt3.large (x86)2023438028060000.0%$0.08320
AsyncAWSt3.large (x86)50230386276144800.0%$0.08320
AsyncAWSt3.large (x86)100235424287299000.0%$0.08320
AsyncAWSt3.large (x86)20023717373280830262708.2%$0.08320
AsyncAWSt3.large (x86)300230173823358312760316.2%$0.08320
AsyncAWSt3.large (x86)4005511312734513664108822.9%$0.08320
AsyncAWSt4g.xlarge (ARM)2023138427459900.0%$0.13440
AsyncAWSt4g.xlarge (ARM)50221380273149800.0%$0.13440
AsyncAWSt4g.xlarge (ARM)100221382273299500.0%$0.13440
AsyncAWSt4g.xlarge (ARM)200222395276600000.0%$0.13440
AsyncAWSt4g.xlarge (ARM)300266102097068699560.6%$0.13440
AsyncAWSt4g.xlarge (ARM)4002581110619537793108812.3%$0.13440
MandelbrotAWSt4g.xlarge (ARM)26337796466000.0%$0.13440
MandelbrotAWSt4g.xlarge (ARM)5581111573715000.0%$0.13440
MandelbrotAWSt4g.xlarge (ARM)106134203154026400.0%$0.13440
MandelbrotAWSt4g.xlarge (ARM)1510796666270826400.0%$0.13440
MandelbrotAWSt4g.xlarge (ARM)2011786820372327200.0%$0.13440
MandelbrotAWSt4g.xlarge (ARM)257518171442526400.0%$0.13440
MandelbrotAWSt4g.xlarge (ARM)3067710231555526562.2%$0.13440
MandelbrotAWSt4g.xlarge (ARM)35687102456356239259.5%$0.13440
MandelbrotAWSt4g.xlarge (ARM)40895103347353229228.8%$0.13440
MandelbrotAWSt4g.xlarge (ARM)4510411020780441995321.0%$0.13440
MandelbrotAWSt4g.xlarge (ARM)5012361020686241737630.5%$0.13440
MandelbrotAWSt4g.xlarge (ARM)55119310206917910516160.5%$0.13440

.NET 5 – ARM vs x64 in the Cloud Part 2 – Azure

Having conducted my ARM and x64 tests on AWS yesterday I was curious to see how Azure would fair – it doesn’t support ARM but ultimately that’s a mechanism for delivering value (performance and price) and not an end in and of itself. And so this evening I set about replicating the tests on Azure.

In the end I’ve massively limited my scope to two instance sizes:

  1. A2 – this has 2 CPUs and 4Gb of RAM (much more RAM than yesterdays) and costs $0.120 per hour
  2. B1S – a burstable VM that has 1 CPUand 1Gb RAM (so most similar to yesterdays t2.micro) and costs $0.0124 per hour

Note – I’ve begun to conduct tests on D series too, preliminary findings is that the D1 is similar to the A2 in performance characteristics.

I was struggling to find Azure VMs with the same pricing as AWS and so had to start with a burstable VM to get something in the same kind of ballpark. Not ideal but they are the chips you are dealt on Azure! I started with the B1S which was still more expensive than the ARM VM. I created the VM, installed software, and ran the tests – the machine comes with 30 credits for bursting. However after running tests several times it was still performing consistently so these were either exhausted quickly, made little difference, or were used consistently.

I moved to the A2_V2 because, frankly, the performance was dreadful on my early tests with the B1S and I also wanted something that wouldn’t burst. I was also trying to match the spec of the AWS machines – 2 cores and 1Gb of RAM. I’ll attempt the same tests with a D series when I can.

Test setup was the same and all tests are run on VMs accessed directly on their public IP using Apache as a reverse proxy to Kestrel and our .NET application.

I’ve left the t2.micro instance out of this analysis

Mandelbrot

With 2 clients per test we see the following response times:

We can see that the two Azure instances are already off to a bad start on this computationally heavy test.

At 10 clients per second we continue to see this reflected:

However at this point the two Azure instances begin to experience timeout failures (the threshold being set at 10 seconds in the load tester):

The A2_V2 instance is faring particularly badly particularly given it is 10x the cost of the AWS instances.

Unfortunately their is no meaningful compaison I can make under higher load as both Azure instances collapse when I push to 15 clients per second. For complete sake here are the results on AWS at 20 clients per second (average response and total requests):

Simulated Async Workload

With our simulated async workload Azure fares better at low scale. Here are the results at 20 requests per second:

As we push the scale up things get interesting with different patterns across the two vendors. Here are the average response times at 200 clients per second:

At first glance AWS looks to be running away with things however both the t4g.micro and t3.micro suffer from performance degradation at the extremes – the max response time is 17 seconds for both while for the Azure instances it is around 9 seconds.

You can see this reflected in the success and total counts where the AWS instances see a number of timeout failures (> 10 seconds) while the Azure instances stay more consistent:

However the AWS instances have completed many more requests overall. I’ve not done a percentile breakdown (see comments yesterday) but it seems likely that at the edges AWS is fraying and degrading more severely than Azure leading to this pattern.

Conclusions

The different VMs clearly have different strengths and weaknesses however in the computational test the Azure results are disappointing – the VMs are more expensive yet, at best, offer performance with different characteristics (more consistent when pushed but lower average performance – pick your poison) and at worst offer much lower performance and far less value for money. They seem to struggle with computational load and nosedive rapdily when pushed in that scenario.

Full Results

TestVendorInstanceClients per secondMinMaxAverageSuccessful ResponsesTimeouts
MandelbrotAWSt4g.micro (ARM)2618751638600
MandelbrotAWSt4g.micro (ARM)5765279417091320
MandelbrotAWSt4g.micro (ARM)10761695838821300
MandelbrotAWSt4g.micro (ARM)157591020357041271
MandelbrotAWSt4g.micro (ARM)2080210207745911914
MandelbrotAWSt3.micro (x64)2701885744600
MandelbrotAWSt3.micro (x64)5878331320691080
MandelbrotAWSt3.micro (x64)10855803744981030
MandelbrotAWSt3.micro (x64)15973102026930849
MandelbrotAWSt3.micro (x64)2010301021584957435
MandelbrotAWSt2.micro (x64)267511401010580
MandelbrotAWSt2.micro (x64)565153243332720
MandelbrotAWSt2.micro (x64)101867101936999568
MandelbrotAWSt2.micro (x64)1514451020394583244
MandelbrotAWSt2.micro (x64)2014861020688951140
MandelbrotAzureA2_V2 (x64)2917927934600
MandelbrotAzureA2_V2 (x64)5126366493975560
MandelbrotAzureA2_V2 (x64)1012051020379853421
MandelbrotAzureA2_V2 (x64)15ERROR RATE TOO HIGH
MandelbrotAzureA2_V2 (x64)20ERROR RATE TOO HIGH
MandelbrotAzureB1S (x64)267025511171570
MandelbrotAzureB1S (x64)5161255213252720
MandelbrotAzureB1S (x64)101259100017115722
MandelbrotAzureB1S (x64)15ERROR RATE TOO HIGH
MandelbrotAzureB1S (x64)20ERROR RATE TOO HIGH
AsyncAWSt4g.micro (ARM)202363712756000
AsyncAWSt4g.micro (ARM)50222417837314980
AsyncAWSt4g.micro (ARM)10023141428629940
AsyncAWSt4g.micro (ARM)2003101738820283995200
AsyncAWSt3.micro (x64)202334022796000
AsyncAWSt3.micro (x64)50235491240714980
AsyncAWSt3.micro (x64)10023554529229940
AsyncAWSt3.micro (x64)2002341737625983089260
AsyncAWSt2.micro (x64)202424122986000
AsyncAWSt2.micro (x64)5024154531214970
AsyncAWSt2.micro (x64)1002449829226019890
AsyncAWSt2.micro (x64)2003471737538582118252
AsyncAzureA2_V2 (x64)201733432526000
AsyncAzureA2_V2 (x64)5019650427414980
AsyncAzureA2_V2 (x64)1002394240248417940
AsyncAzureA2_V2 (x64)2004238929547517250
AsyncAzureB1S (x64)202063832685800
AsyncAzureB1S (x64)5020943627814980
AsyncAzureB1S (x64)1002923151189222520
AsyncAzureB1S (x64)2004827708447421360

.NET 5 – ARM vs x64 in the Cloud

With Microsoft and Apple both now beginning to use ARM chips in laptops, what was traditionally the domain of x86/x64 architecture, I found myself curious as to the ramifications of this move – particularly by Apple who are transitioning their entire lineup to ARM over the next 2 years.

While musing on the pain points of this I found myself wandering if Azure supported ARM processors, they don’t, and got pointed to AWS who do. @thebeebs (an AWS developer advocate) mentioned that some customers had seen significant cost reductions by moving some workloads over to ARM and so I, inevitably, found myself curious as to how typical .NET workloads might run in comparison to x64 and set about some tests.

The Tests

I quickly rustled up a simple API containing two invocable workloads:

  1. A computation heavy workload – I’m rendering a Mandelbrot and returning it as an image. This involves floating point maths.
  2. A simulated await workload – often with APIs we hand off to some other system (e.g. a database) and then do a small amount of computation. I’ve simulated this with Task.Delay and a (very small) random factor to simulate the slight variations you will get with any network / remote service request and then around this I compute two tiny Mandelbrots and return a couple of numbers. It would be nice to come back at some point and use a more structured approach for the simulated remote latency.

I’ve written this in F# (its not particularly “functional”) using Giraffe on top of ASP.Net Core just because that’s my go to language these days. Its running under the .NET 5 runtime.

The code for this is here. Its not particularly elegant and I simply converted some old JavaScript code of mine into F# for the Mandelbrot. It does a job.

The Setup

Within AWS I created three EC2 Linux instances:

  1. t4g.micro – ARM based, 2 vCPU, 1Gb memory, $0.0084 per hour
  2. t3.micro – x64 based, 2 vCPU, 1Gb memory, $0.0104 per hour
  3. t2.micro – x64 based, 1 vCPU, 1Gb memory, $0.0116 per hour

Its worth noting that my ARM instance is costing me 20% less than the t3.micro.

I’ve deliberately chosen very small instances in order to make it easier to stress them without having to sell a kidney to fund the load testing. We should be able to stress these instances quite quickly.

I then SSHed into each box and installed .NET 5 from the appropriate binaries and setup Apache as a reverse proxy. On the ARM machine I also had to install GCC and compile a version of libicui18n for .NET to work.

Next I used git clone to bring down the source and ran dotnet restore followed by dotnet run. At this point I had the same code working on each of my EC2 instances. Easy to verify as the root of the site shows a Mandelbrot:

This was all pretty easy to set up. You can also do it using a Cloud Formation sample that I was pointed at (again by @thebeebs).

I still think its worth remarking how much .NET has changed in the last few years – I’ve not touched Windows here and have the same source running on two different CPU architectures with no real effort on my part. Yes its “get through the door” stakes these days but it was hard to imagine this a few years back.

Benchmarks

My tests were fairly simple – I’ve used loader.io to maintain a steady state of a given number of clients per second and gathered up the response times and total execution counts along with the number of timeouts. I had the timeout threshold set at 10 seconds.

Time allowing I will come back to this and run some percentile analysis – loader doesn’t support this and so I would need to do some additional work.

I’ve run the test several times and averaged the results – though they were all in the same ballpark.

Mandelbrot

Firstly as a baseline lets look at things running with just two clients per second:

With little going on we can see that the ARM instance already has a slight advantage – its consistently (min, max and average) around 100ms faster than the closest x64 based instance.

Unsurprisingly if we push things a little harder to 5 clients per second this becomes magnified:

We’re getting no errors or timeouts at this point and you can see the total throughput over the 30 second run below:

The ARM instance has completed around 20% more requests than the nearest x64 instance, with a 18% improvement in average response time and at 80% of the cost.

And if we push this out to 20 clients per second (my largest scale test) the ARM instance looks better again:

Its worth noting that at this point all three instances are generating timeouts in our load test suite but again the ARM instance wins out here – we get fewer timeouts and get through more overall requests:

You can see from this that our ARM instance is performing much better under this level of load. We can say that:

  • Its successfully completed 60% more requests than the nearest x64 instance
  • It has a roughly 12% improvement on average response time
  • And it is doing this at 80% of the cost of the x64 instance

With our Mandelbrot test its clear that the ARM instance has a consistent advantage both in performance and cost.

Simulated Async Workload

Starting again with a low scale test (in this case 50 clients per second – this test spends significant time awaiting) in this case we can see that our t2 x64 instance had an advantage of around 40ms:

However if we move up to 100 clients per second we can see the t2 instance essentially collapse while out t4g ARM instance and t3 x64 instance are essentially level pegging (286ms and 292ms) respectively:

We get no timeouts at this point and our ARM and x64 instance level peg again on total requests:

However if we push on to a higher scale test (200 clients per second) we can see the ARM instance begin to pull ahead:

Conclusions

Going into this I really didn’t know what to expect but these fairly simple tests suggest their is an economic advantage to running under ARM in the cloud. At worst you will see comparable performance at a lower price point but for some workloads you may see a significant performance gain – again at a lower price point.

20% performance gain at 80% the price is most certainly not to be sniffed at and for large workloads could quickly offset the cost of moving infrastructure to ARM.

Presumably the price savings are due to the power efficiency of the ARM chips. However what is hard to tell is how much of the pricing is “early adopter” to encourage people to move to CPUs that have long term advantage to cloud vendors (even minor power efficiency gains over cloud scale data centers must total significant numbers on the bottom line) and how much of that will be sustained and passed on to users in the long term.

Doubtless we’ll land somewhere in the middle.

Question I have now is: where the heck is Azure in all this? Between Lambda and ARM on AWS its hard not to feel as if the portability advantages, both processor and OS, of .NET Core / 5 are being realised more effectively by Amazon than they are by Microsoft themselves. Strange times.

Full Results

Response Times (ms)
TestInstanceClients per secondMinMaxAverageSuccessful ResponsesTimeouts
Mandelbrott4g.micro (ARM)2618751638600
Mandelbrott4g.micro (ARM)5765279417091320
Mandelbrott4g.micro (ARM)10761695838821300
Mandelbrott4g.micro (ARM)157591020357041271
Mandelbrott4g.micro (ARM)2080210207745911914
Mandelbrott3.micro (x64)2701885744600
Mandelbrott3.micro (x64)5878331320691080
Mandelbrott3.micro (x64)10855803744981030
Mandelbrott3.micro (x64)15973102026930849
Mandelbrott3.micro (x64)2010301021584957435
Mandelbrott2.micro (x64)267511401010580
Mandelbrott2.micro (x64)565153243332720
Mandelbrott2.micro (x64)101867101936999568
Mandelbrott2.micro (x64)1514451020394583244
Mandelbrott2.micro (x64)2014861020688951140
Asynct4g.micro (ARM)202363712756000
Asynct4g.micro (ARM)50222417837314980
Asynct4g.micro (ARM)10023141428629940
Asynct4g.micro (ARM)2003101738820283995200
Asynct3.micro (x64)202334022796000
Asynct3.micro (x64)50235491240714980
Asynct3.micro (x64)10023554529229940
Asynct3.micro (x64)2002341737625983089260
Asynct2.micro (x64)202424122986000
Asynct2.micro (x64)5024154531214970
Asynct2.micro (x64)1002449829226019890
Asynct2.micro (x64)2003471737538582118252

An Azure Reference Architecture

There are an awful lot of services available on Azure but I’ve noticed a pattern emerging in a lot of my work around web apps. At their core they often have a similar architecture, deployment in Azure, and process for build and release.

For context a lot my hands on work over the last 3 years has been as a freelancer developing custom systems for people or on my own side projects (most recently https://www.forcyclistsbycyclists.com). In these situations I’ve found productivity to be super important in a few key ways:

  1. There’s a lot to get done, one or two people, and not much time – so being able to crank out a lot of work quickly and to a good level of quality is key.
  2. Adaptability – if its an externally focused green field system there’s a reasonable chance that there’s a degree of uncertainty over what the right feature set is. I generally expect to have to iterate a few times.
  3. I can’t be wasting time repeating myself or undertaking lengthy manual tasks.

Due to this I generally avoid over complicating my early stage deployment with too much separation – but I *do* make sure I understand where my boundaries and apply principles that support the later distribution of a system in the code.

With that out the way… here’s an architecture I’ve used as a good starting point several times now. And while it continues to evolve and I will vary specific decisions based on need its served me well and so I thought I’d share it here.

I realise there are some elements on here that are not “the latest and greatest” however its rarely productive to be on the bleeding edge. It seems likely, for example, that I’ll adopt the Azure SPA support at some point – but there’s not much in it for me doing that now. Similarly I can imagine giving GitHub Actions ago at some point – but what do I really gain by throwing what I know away today. From the experiments I’ve run I gain no productivity. Judging this stuff is something of a fine line but at the risk of banging this drum too hard: far too many people adopt technology because they see it being pushed and talked about on Twitter or dev.to (etc.) by the vendor, by their DevRel folk and by their community (e.g. MVPs) and by those who have jumped early and are quite possibly (likely!) suffering from a bizarre mix of Stockholm Syndrome and sunk cost fallacy “honestly the grass is so much greener over here… I’m so happy I crawled through the barbed wire”.

Rant over. If you’ve got any questions, want to tell me I’m crazy or question my parentage: catch me over on Twitter.

Architecture

Build & Release

I’ve long been a fan of automating at least all the high value parts of build & release. If you’re able to get it up and running quickly it rapidly pays for itself over the lifetime of a project. And one of the benefits of not CV chasing the latest tech is that most of this stuff is movable from project to project. Once you’ve set up a pipeline for a given set of assets and components its pretty easy to use on your next project. Introduce lots of new components… yeah you’ll have lots of figuring out to do. Consistency is underrated in our industry.

So what do I use and why?

  1. Git repository – I was actually an early adopter of Git. Mostly because I was taking my personal laptop into a disconnected environment on a regular basis when it first started to emege and I’m a frequent committer.

    In this architecture it holds all the assets required to build & deploy my system other than secrets.
  2. Azure DevOps – I use the pipelines to co-ordinate build & release activities both directly using built in tasks, third party tasks and scripts. Why? At the time I started it was free and “good enough”. I’ve slowly moved over to the YAML pipelines. Slowly.
  3. My builds will output four main assets: an ARM template, Docker container, a built single page application, and SQL migration scripts. These get deployed into a an Azure resource group, Azure container registry, blob storage, and a SQL database respectively.

    My migration scripts are applied against a SQL database using DbUp and my ARM templates are generated using Farmer and then used to provision a resource group. I’m fairly new to Farmer but so far its been fantastic – previously I was using Terraform but Farmer just fit a little nicer with my workflow and I like to support the F# community.

Runtime Environment

So what do I actually use to run and host my code?

  1. App Service – I’ve nearly always got an API to host and though I will sometimes use Azure Functions for this I more often use the Web App for Containers support.

    Originally I deployed directly into a “plain” App Service but grew really tired with the ongoing “now this is really how you deploy without locked files” fiasco and the final straw was the bungled .NET Core release.

    Its just easier and more reliable to deploy a container.
  2. Azure DNS – what it says on the tin! Unless there is a good reason to run it elsewhere I prefer to keep things together, keeps things simple.
  3. Azure CDN – gets you a free SSL cert for your single page app, is fairly inexpensive, and helps with load times.
  4. SQL Database – still, I think, the most flexible general purpose and productive data solution. Sure at scale others might be better. Sure sometimes less structured data is better suited to less structured data sources. But when you’re trying to get stuff done there’s a lot to be said for having an atomic, transactional data store. And if I had a tenner for every distributed / none transactional design I’ve seen that dealt only with the happy path I would be a very very wealthy man.

    Oh and “schema-less”. In most cases the question is is the schema explicit or implicit. If its implicit… again a lot of what I’ve seen doesn’t account for much beyodn the happy path.

    SQL might not be cool, and maybe I’m boring (but I’ll take boring and gets shit done), but it goes a long way in a simple to reason about manner.
  5. Storage accounts – in many systems you come across small bits of data that are handy to dump into, say, a blob store (poor mans NoSQL right there!) or table store. I generally find myself using it at some point.
  6. Service Bus – the unsung hero of Azure in my opinion. Its reliable. Does what it says on the tin and is easy to work with. Most applications have some background activity, chatter or async events to deal with and service bus is a great way of handling this. I sometimes pair this (and Azure Functions below) with SignalR.
  7. Azure Functions – great for processing the Service Bus, running code on a schedule and generally providing glue for your system. Again I often find myself with at least a handful of these. I often also use Service Bus queues with Functions to provide a “poor mans admin console”. Basically allow me to kick off administrative events by dropping a message on a queue.
  8. Application Insights – easy way of gathering together logs, metrics, telemetry etc. If something does go wrong or your system is doing something strange the query console is a good way of exploring what the root cause might be.

Code

I’m not going to spend too long talking about how I write the system itself (plenty of that on this blog already). In generally I try and keep things loosely coupled and normally start with a modular monolith – easy to reason about, well supported by tooling, minimal complexity but can grow into something more complex when and if that’s needed.

My current tools of choice is end to end F# with Fable and Saturn / Giraffe on top of ASP.Net Core and Fable Remoting on top of all that. I hopped onto functional programming as:

  1. It seemed a better fit for building web applications and APIs.
  2. I’d grown tired with all the C# ceremony.
  3. Collectively we seem to have decided that traditional OO is not that useful – yet we’re working in languages built for that way of working. And I felt I was holding myself back / being held back.

But if you’re looking to be productive – use what you know.

Why (not) .NET?

A frequent topic of conversation on Twitter, particularly if you follow a number of Microsoft employees, is why isn’t .NET used more or talked about more. The latest round of this I saw was in connection with microserviecs – “where’s all the chat about .NET Core microservices”. There’s often a degree of surprise, frustration and curiosity expressed that people outside of the existing .NET audience aren’t looking at .NET Core – or at least seemingly not in large numbers (I’m sure someone can pull out a statistic suggseting otherwise, and someone else pull out a statistic suggesting it is the case).

One indicator of usage is the TIOBE index which is focused on langauge. VB and C# currently rank 5 and 6 in terms of popularity with their history shown below:

Based on this .NET is certainly a successful runtime and you could argue that the release of .NET Core in 2016 (announced in 2014 with a lengthy preview period) halted what looked to be a significant declince in C# usage – I would assume due to the lack of cross-platform and growing popularity of the cloud.

But since then C# has pretty much flatlined – maybe trending up slightly (the peaks and troughs seem to be going up over the last year). VB… I know far less about the VB world an what could have caused that spike. I’m assuming it has little to do with .NET Core given the limitations of running on the platform.

I find myself in an interesting position with regard to topics like this – I’ve been using .NET for a long time (since the very first beta), its the ecosystem I’m most familiar with, and in general I’m fairly happy with it. On the other hand I now work as a CTO managing teams with a variety of tech stacks (.NET, Node / React, and PHP for the most part) and when looking at new build projects wrangle with what to build it in.

And you know what? There is very little about .NET (Core or otherwise) that stands out.

That it is now cross-platform is great and opens it up to use cases and developers for which it would otherwise be a no-go (myself included these days) but that’s a catch-up play by Microsoft not a unique feature. And the majority of developers who are not on Windows have already spent significant time getting productive in other runtimes and ecosystems – to trade that away and incur a productivity debt there has to be something to offset that debt.

Now being able to write end to end browser through to backend C# (and I’ll swing back to C# later) with Blazor looks compelling but you can do this with just about any language and runtime now through the wizardry of transpilers. Sure Blazor targets WASM and that has some advantages – but also has a boat load of disadvantages: it can be a big download and is an isolated ecosystem. And it can suffer from some of the same quirks transpilers do – in my, brief, time with it I ran into over eager IL stripping issues that were obtuse to say the least. The advantage of the approaches taken by things like Fable (F#), Reason (OCaml), and even Bridge (C#) is that it lets you continue to use the ecosystem you know. For example I was easily able to move to using F# on the front end with Fable and take my React knowledge with me. I can’t do that with Blazor. Why not use Bridge? Well then you’re back to the kind of issue sometimes levelled at Fable – reliance on a small band of OSS contributors on a fringe project. To be fair to Blazor – it seems fairly clearly targeted at the .NET hardcore and those who have not yet (and there can be many good reasons not to) stepped out of the pure Microsoft world.

Microsoft’s great tooling for .NET is often mentioned and I agree – its excellent. If you’re on Windows. JetBrains do have Rider as an excellent cross platform option but there are third party solutions for many languages and runtime. So again, from a Microsoft point of view, we’re back to the existing .NET audience.

.NET Core is fast right? So does that make it compelling? Its certainly a bonus and could be a swing factor if you’re pushing large volumes of transactions or crunching lots of data but for many applications and systems performance blocks are IO and architecture related. There’s a lot of room in that space before hitting runtime issues. But yes – I think .NET Core does offer some advantage in this space though its not the fastest thing out there if you care about benchmarks. Its worth noting it also doesn’t play well in serverless environments – its a poor performer on both AWS and Azure when it comes to coldstarts.

C# is a language I’ve used a lot over the last (nearly) 20 years and I’ve enjoyed my time with it. Its evolved over that period which has been mostly great but its starting to feel like a complicated and strange beast. Many of the changes have been fantastic: generics, expressions and lambdas to name but a few but it increasingly feels like its trying to be all things to all people as it shoehorns in further functional programming features and resulting in something with no (to me at least) obvious path of least resistance. It also tends to come with a significant amount of typing and ceremony – this doesn’t have to be the case and there have been some attempts to demonstrate this but again the path of least resistance is fairly confused.

Finally how about the “Microsoft problem”? What do I mean by this – a generation or two of developers, particularly outside of the enterprise space, have been (to put it politely) reluctant to use Microsoft technologies due to Microsofts behaviour in the 90s and early 2000s – the abuse of their Windows monopoly, attitude to the web, embrace extend extinguish, and hostility to open source all contributed to this. However there is plenty of evidence to suggest this is at least diminishing as a factor – look at the popularity of Visual Studio Code and TypeScript. Both widely adopted and in the kind of scenarios where Microsoft would have been anathema just a few years ago. The difference, to me, is that they don’t require you to throw anything away and offer signficant clear advantages: you can add TypeScript to an existing JS solution and quite quickly begin to leverage better tooling, fewer required unit tests, more readable code and you don’t need a team of developers to upend their world. Good luck doing the same with Blazor!

The choice of a language, runtime and architecture is rarely simple and if you’ve previous experience or existing teams is going to be as much about the past as it is the future – what do people know, what do they like, what are they familiar with. Change has to have a demonstratable benefit and .NETs problem is its just not bringing anything compelling to the table for those not already in its sphere and its not providing an easy route in.

My expectation is it continues to bubble along with its current share and its current audience. I think .NET Core was important to arrest a fall – the lack of cross-platform was seriously hurting it .NET – but its not going to drive any significant growth or change. My advice to any folk working on .NET at Microsoft: if you’re serious about pushing out of the bubble then look at your successes elsewhere (TypeScript and Code) and give people a way to bridge worlds. You’ll get nowhere with all or nothing and reinventing everything yourselves. Oh – and sort out the branding. 4.x, Core, now 5 – its confusing as all heck even for those of us who follow it and when the rebuttal of this is links to articles trying to explain it thats a sign you’ve got a problem not a solution. If I was looking at it from the outside I’d probably bounce off all this in confusion.

When I look ahead to where I think I’ll be heading personally: ever more down the functional programming route. It offers significant advantages (that I’ve spoken about before) and, at least with F#, a clear path of least resistance. As for runtimes I’ll probably stick in this hybrid world of React and .NET Core for some time – I can’t see any compelling reason to drop either of those at the moment.

As for my day job… the jury is out. Its going to come down to people, productivity and project more than anything else. I see no inherent advantage in .NET Core as a piece of technology.

.NET Open Source – A Mugs Game

I recognise my own motivations have at least as large a bearing on my views as the behaviour of vendors and so I should note that this is a purely personal perspective and from someone who currently largely operates in the .NET and Microsoft space. Your mileage may differ.

If you follow me on Twitter you might have noticed a growing disillusionment on my part with open source software – not its usefulness but rather the culture, business and economics.

OSS used to be a threat to big tech but, particularly with the growth of the cloud as a business model, its become a source of ideas, free products to package and host, and free labour for big tech. They figured out how to monteise it. The old Microsoft mantra of embrace, extend, extinguish is back but now applied to open source (the recent debacles around AppGet and, the excellent, Farmer are recent examples of this).

During a discussion on Twitter around this @HowardvRooijen made this comment:

Part of the issue is, 20 years in, everyone is still focusing on low level components… They are fun to hobby code (and then increasingly wearisome), if you move up the value stack MS are less likely to compete.

https://twitter.com/HowardvRooijen/status/1274412962439192578?s=20

He’s right but for me, emotionally, OSS was a sandpit where you could still almost scratch that 8-bit low level itch in a useful way. In someways a holdout of the “good old days” of the bedroom coder and there was a certain spirit to it that, for me, has been lost.

If you move up the value chain you’re talking about building products or things that more obviously can be turned into products. If these things are the things that are “safer” from big tech and Microsoft then absolutely we should not be creating them for free. Thats our income, our livelihood.

I have switched (more later) to move up the value chain with my cycling performance website but I currently have no intention of making this OSS. If it starts to get traction and feels worth it (i.e. it provides enough value to warrant a subscription) I will absolutely look to charge for it. OSS doesn’t help me build it and it doesn’t help me sell it – so it doesn’t make sense to.

What of the .NET Foundation? Is it a beacon of hope? Christ no. From its maturity model to silence over recent events (and seeming ignorance of the collateral damage of things like the AppGet issue) its very slanted towards a) Microsoft interests and b) the consumption of OSS. I’ve seen little support for small contributors. I was really concerned by the approach to the Maturity Model as that was entirely about trying to get contributors to “professionalise” to ease the adoption by “risk averse businesses”. Translation: get contributors to do more work so that profitable business can benefit.

I joined the Foundation with the idea that I could speak from the inside. In reality I’ve struggled to find the will to engage and I’ll almost certainly let my membership lapse at the end of its current term. The only thing I can see keeping me in is their support for user groups.

So where does this leave me? Basically I’m out. I’m done with it. At this point I think .NET OSS is a mugs game. I’m conciously upping my sponsorships of OSS projects I use so I continue to give something back but I’m not participating in this any more. There’s no good outcome here for contributors.

I’ll try and organise a handover of Function Monkey but I feel very dispirited by things at the moment and, frankly, am struggling to look at it.

I’ll continue to work on community contributions but in the form of writing and videos. But heck I’m even thinking that has to lead somewhere deliberate.

Where does that leave others? If you’re planning on starting a new OSS project I’d first encourage you to think long and hard about why and at the very least reconcile yourself with the likely end game (obscurity, hijacked in some way by Microsoft, hired perhaps) and understand that you’ve given all the value away. Are you really ok with that? Maybe you can build a consultancy business round it but history shows how hard this is and I think its getting harder.

I think there are easier and more effective ways to achieve most (if not all) of the outcomes OSS contribution might lead to.

Finally I’ll finish by saying: I think there are plenty of well meaning people at Microsoft. I think most of those promoting OSS from within Microsoft do so meaning well but ultimately as an organisation they have a very specific business model around open source software (absolutely its been discussed at senior levels of the business) and its one that is at odds with how I at least, perhaps naively, saw the “spirit” of open source software.

Using Function Monkey with MediatR

There are a lot of improvements coming in v4 of Function Monkey and the beta is currently available on NuGet. As the full release approaches I thought it would make sense to introduce some of these new capabilities here.

In order to simplyify Azure Functions development Function Monkey makes heavy use of commanding via a mediator and ships with my own mediation library. However there’s a lot of existing code out their that makes use of the popular MediatR library which, if Function Monkey supported, could fairly easily be moved into a serverless execution environment.

Happily Function Monkey now supports just this! You can use my existing bundled mediator, bring your own mediator, or add the shiny new FunctionMonkey.MediatR NuGet package. Here we’re going to take a look at using the latter.

First begin by creating a new, empty, Azure Functions project and add three NuGet packages:

FunctionMonkey
FunctionMonkey.Compiler
FunctionMonkey.MediatR

At the time of writing be sure to use the prerelease packages version 4.0.39-beta.4 or later.

Next create a folder called Models. Add a class called ToDoItem:

public class ToDoItem
{
    public Guid Id { get; set; }
    
    public string Title { get; set; }
    
    public bool IsComplete { get; set; }
}

Now add a folder called Services and add an interface called IRepository:

internal interface IRepository
{
    Task<ToDoItem> Create(string title);
}

And a memory based implementation of this called Repository:

internal class Repository : IRepository
{
    private readonly List<ToDoItem> _items = new List<ToDoItem>();
    
    public Task<ToDoItem> Create(string title)
    {
        ToDoItem newItem = new ToDoItem()
        {
            Title = title,
            Id = Guid.NewGuid(),
            IsComplete = false
        };
        _items.Add(newItem);
        return Task.FromResult(newItem);
    }
}

Now create a folder called Commands and in here create a class called CreateToDoItemCommand:

public class CreateToDoItemCommand : IRequest<ToDoItem>
{
    public string Title { get; set; }
}

If you’re familiar with Function Monkey you’ll notice the difference here – we’d normally implement the ICommand<> interface but here we’re implementing MediatR’s IRequest<> interface instead.

Next create a folder called Handlers and in here create a class called CreateToDoItemCommandHandler as shown below:

internal class CreateToDoItemCommandHandler : IRequestHandler<CreateToDoItemCommand, ToDoItem>
{
    private readonly IRepository _repository;

    public CreateToDoItemCommandHandler(IRepository repository)
    {
        _repository = repository;
    }
    
    public Task<ToDoItem> Handle(CreateToDoItemCommand request, CancellationToken cancellationToken)
    {
        return _repository.Create(request.Title);
    }
}

Again the only real difference here is that rather than implement the ICommandHandler interface we implement the IRequestHandler interface from MediatR.

Finally we need to add our FunctionAppConfiguration class to the root of the project to wire everything up:

public class FunctionAppConfiguration : IFunctionAppConfiguration
{
    public void Build(IFunctionHostBuilder builder)
    {
        builder
            .Setup(sc => sc
                .AddMediatR(typeof(FunctionAppConfiguration).Assembly)
                .AddSingleton<IRepository, Repository>()
            )
            .UseMediatR()
            .Functions(functions => functions
                .HttpRoute("todo", route => route
                    .HttpFunction<CreateToDoItemCommand>(HttpMethod.Post)
                )
            );
    }
}

Again this should look familiar however their are two key differences. Firstly in the Setup block we use MediatR’s IServiceCollection extension method AddMediatR – this will wire up the request handlers in the dependency injector. Secondly the .UseMediatR() option instructs Function Monkey to use MediatR for its command mediation.

And really that’s all their is to it! You can use both requests and notifications and you can find a more fleshed out example of this on GitHub.

As always feedback is welcome on Twitter or over on the GitHub issues page for Function Monkey.

Avoiding Gremlin Injection Attacks with Azure Cosmos DB

I’ve written previously about some of the issues with using Cosmos DB as a graph database from .NET. One of the more serious issues, I think, is that the documentation doesn’t really demonstrate how to avoid an injection attack when using Gremlin as it presents examples using hard coded strings which are then just picked up and run through the Gremlin.NET library:

// Gremlin queries that will be executed.
private static Dictionary<string, string> gremlinQueries = new Dictionary<string, string>
{
    { "Cleanup",        "g.V().drop()" },
    { "AddVertex 1",    "g.addV('person').property('id', 'thomas').property('firstName', 'Thomas').property('age', 44)" },
    { "AddVertex 2",    "g.addV('person').property('id', 'mary').property('firstName', 'Mary').property('lastName', 'Andersen').property('age', 39)" },
    { "AddVertex 3",    "g.addV('person').property('id', 'ben').property('firstName', 'Ben').property('lastName', 'Miller')" },
    { "AddVertex 4",    "g.addV('person').property('id', 'robin').property('firstName', 'Robin').property('lastName', 'Wakefield')" },
    { "AddEdge 1",      "g.V('thomas').addE('knows').to(g.V('mary'))" },
    { "AddEdge 2",      "g.V('thomas').addE('knows').to(g.V('ben'))" },
    { "AddEdge 3",      "g.V('ben').addE('knows').to(g.V('robin'))" },
    { "UpdateVertex",   "g.V('thomas').property('age', 44)" },
    { "CountVertices",  "g.V().count()" },
    { "Filter Range",   "g.V().hasLabel('person').has('age', gt(40))" },
    { "Project",        "g.V().hasLabel('person').values('firstName')" },
    { "Sort",           "g.V().hasLabel('person').order().by('firstName', decr)" },
    { "Traverse",       "g.V('thomas').out('knows').hasLabel('person')" },
    { "Traverse 2x",    "g.V('thomas').out('knows').hasLabel('person').out('knows').hasLabel('person')" },
    { "Loop",           "g.V('thomas').repeat(out()).until(has('id', 'robin')).path()" },
    { "DropEdge",       "g.V('thomas').outE('knows').where(inV().has('id', 'mary')).drop()" },
    { "CountEdges",     "g.E().count()" },
    { "DropVertex",     "g.V('thomas').drop()" },
};

Focusing in on one of the add vertex examples and how it might be executed with the Gremlin.NET library:

private async Task BadExample()
{
    using (GremlinClient client = new GremlinClient(_gremlinServer, new GraphSON2Reader(),
        new GraphSON2Writer(), GremlinClient.GraphSON2MimeType))
    {
        await client.SubmitAsync(
            "g.addV('person').property('id', 'thomas').property('firstName', 'Thomas').property('age', 44)");
    }
}

We know from years of SQL that examples like this quickly become widespread injection prone pieces of code like the below, particularly if people are new to working with a new database (and in the case of graph databases and Gremlin – that’s most people):

private async Task BadExample(string firstName, int age)
{
    using (GremlinClient client = new GremlinClient(_gremlinServer, new GraphSON2Reader(),
        new GraphSON2Writer(), GremlinClient.GraphSON2MimeType))
    {
        await client.SubmitAsync(
            $"g.addV('person').property('id', '{firstName.ToLower()}').property('firstName', '{firstName}').property('age', {age})");
    }
}

The issue, if you’re not familiar with injection attacks, is that as a user I can enter a ‘ character in the input and break out to add my own code through a user interface that executes on the server – for example I could supply a firstName of:

James').property('myinjectedproperty','hahaha got ya

And I’ve managed to attach some data of my own choosing. Obviously I could do more nefarious things too.

Fortunately Gremlin does support parameterised queries and we can rewrite the code above more safely to look like this and leave the libraries and database to take care of this:

private async Task BetterExample(string firstName, int age)
{
    using (GremlinClient client = new GremlinClient(_gremlinServer, new GraphSON2Reader(),
        new GraphSON2Writer(), GremlinClient.GraphSON2MimeType))
    {
        Dictionary<string, object> arguments = new Dictionary<string, object>
        {
            { "firstNameAsId", firstName.ToLower() },
            { "firstName", firstName },
            { "age", age }
        };
        await client.SubmitAsync(
            $"g.addV('person').property('id', firstNameAsId).property('firstName', firstName).property('age', age)", arguments);
    }
}

With the uptake of Cosmos and Graph databases being new to most people I really wish the Cosmos team would update these docs with a security first mindset and its something I’ve fed back to them previously. Leaving the documentation as it stands is almost certainly going to lead to more insecure code being written than would otherwise be the case.

I’ll probably drop out a few short Cosmos posts over the next few days – I’m doing a lot of (quite interesting!) work with it at the moment.

Contact

  • If you're looking for help with C#, .NET, Azure, Architecture, or would simply value an independent opinion then please get in touch here or over on Twitter.

Recent Posts

Recent Tweets

Invalid or expired token.

Recent Comments

Archives

Categories

Meta

GiottoPress by Enrique Chavez