├── .config └── dotnet-tools.json ├── .gitignore ├── Article.md ├── MyApi.csproj ├── Program.cs ├── Query.cs ├── README.md ├── aws-lambda-tools-defaults.json ├── bootstrap └── serverless.yml /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "amazon.lambda.tools": { 6 | "version": "5.2.0", 7 | "commands": [ 8 | "dotnet-lambda" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | dist 4 | .serverless 5 | -------------------------------------------------------------------------------- /Article.md: -------------------------------------------------------------------------------- 1 | ## Table of contents 2 | 3 | 1. [Introduction](#introduction) 4 | 1. [Why GraphQL](#why-graphql) 5 | 1. [Why Lambda](#why-lambda) 6 | 1. [Initial application setup](#initial-application-setup) 7 | 1. [Running locally](#running-locally) 8 | 1. [Lambda runtimes](#lambda-runtimes) 9 | 1. [Bootstrapping](#bootstrapping) 10 | 1. [Creating the Lambda package](#creating-the-lambda-package) 11 | 1. [Deploying to AWS](#deploying-to-aws) 12 | 1. [Calling the Lambda](#calling-the-lambda) 13 | 1. [Cleaning up](#cleaning-up) 14 | 1. [Bonus: Running on ARM](#bonus-running-on-arm) 15 | 1. [Summary](#summary) 16 | 17 | ## Introduction 18 | 19 | I recently set up an API for a client in my role as _Lead Cloud Architect_. .NET and AWS were givens, the remaining choices were up to me. This article is my way of writing down all the things I wish I knew when I started that work. 20 | 21 | I assume you already know your way around [.NET 6](https://docs.microsoft.com/en-us/dotnet/core/whats-new/dotnet-6), [C# 10](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-10), [GraphQL](https://graphql.org) and have your ~/.aws/credentials [configured](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-methods). 22 | 23 | ## Why GraphQL 24 | 25 | [GraphQL](https://graphql.org) has quickly become my primary choice when it comes to building most kinds of APIs for a number of reasons: 26 | * Great frameworks available for a variety of programming languages 27 | * Type safety and validation for both input and output is built-in (including client-side if using codegen) 28 | * There are different interactive "swaggers" available, only much better 29 | 30 | Something often mentioned about GraphQL is that the client can request only whatever fields it needs. In practice I find that a less convincing argument because most of us are usually developing our API for a single client anyway. 31 | 32 | For the .NET platform my framework of choice is [Hot Chocolate](https://chillicream.com/docs/hotchocolate). It has great docs and can generate a GraphQL schema _in runtime_ based on existing .NET types. 33 | 34 | ## Why Lambda 35 | 36 | Serverless is all the hype now. What attracts me most is the ease of deployment and the ability to dynamically scale based on load. 37 | 38 | [AWS Lambda](https://aws.amazon.com/lambda/) is usually marketed (and used) as a way to run small isolated functions. Usually with 10 line Node.js examples. But it is so much more! I would argue it is the quickest and most flexible way to run any kind of API. 39 | 40 | The only real serverless alternative on AWS is [ECS on Fargate](https://aws.amazon.com/fargate/), but that comes with a ton of configuration and also requires you to run your code in Docker. 41 | 42 | ## Initial application setup 43 | 44 | We start by creating a new dotnet project: 45 | 46 | `dotnet new web -o MyApi && cd MyApi` 47 | 48 | Add AspNetCore and HotChocolate: 49 | 50 | `dotnet add package DotNetCore.AspNetCore --version "16.*"` 51 | `dotnet add package HotChocolate.AspNetCore --version "12.*"` 52 | 53 | Add a single GraphQL field: 54 | 55 | ```csharp 56 | // Query.cs 57 | using static System.Runtime.InteropServices.RuntimeInformation; 58 | 59 | public class Query { 60 | public string SysInfo => 61 | $"{FrameworkDescription} running on {RuntimeIdentifier}"; 62 | } 63 | ``` 64 | 65 | Set up our AspNetCore application (using the new minimal API): 66 | 67 | ```csharp 68 | // Program.cs 69 | var builder = WebApplication.CreateBuilder(args); 70 | 71 | builder.Services 72 | .AddGraphQLServer() 73 | .AddQueryType(); 74 | 75 | var app = builder.Build(); 76 | 77 | app.UseRouting(); 78 | 79 | app.UseEndpoints(endpoints => 80 | endpoints.MapGraphQL()); 81 | 82 | await app.RunAsync(); 83 | ``` 84 | 85 | ## Running locally 86 | 87 | Let's verify that our GraphQL API works locally. 88 | 89 | Start the API: 90 | `dotnet run` 91 | 92 | Verify using [curl](https://curl.se): 93 | `curl "http://localhost:/graphql?query=%7B+sysInfo+%7D"` 94 | 95 | You should see a response similar to: 96 | ```json 97 | { "data": { "sysInfo":".NET 6.0.1 running on osx.12-x64" } } 98 | ``` 99 | 100 | ## Lambda runtimes 101 | 102 | AWS offers a number of different [managed runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) for Lambda, including .NET Core, Node, Python, Ruby, Java and Go. For .NET the latest supported version is .NET Core 3.1, which I think is too old to base new applications on. 103 | 104 | .NET 6 was released a few months ago, so that's what we'll be using. There are two main alternatives for running on a newer runtime than what AWS provides out of the box: 105 | * Running your Lambda in Docker 106 | * Using a custom runtime 107 | 108 | Running your Lambda in Docker was up until recently the easiest way for custom runtimes. The Dockerfile was only two or three lines and easy to understand. But I still feel it adds a complexity that isn't always justified. 109 | 110 | Therefore we will be using a custom runtime. 111 | 112 | ### Using a custom runtime 113 | 114 | There is a hidden gem available from AWS, and that is the _Amazon.Lambda.AspNetCoreServer.Hosting_ [nuget package](https://www.nuget.org/packages/Amazon.Lambda.AspNetCoreServer.Hosting/). It's hardly mentioned anywhere except in a few GitHub issues, and has a whopping 425 (!) downloads as I write this. But it's in version 1.0.0 and should be stable. 115 | 116 | Add it to the project: 117 | `dotnet add package Amazon.Lambda.AspNetCoreServer.Hosting --version "1.*"` 118 | 119 | Then add this: 120 | 121 | ```csharp 122 | // Program.cs 123 | ... 124 | builder.Services 125 | .AddAWSLambdaHosting(LambdaEventSource.HttpApi); 126 | ... 127 | ``` 128 | 129 | The great thing about this (except it being a one-liner!) is that if the application is not running in Lambda, that method will do nothing! So we can continue and run our API locally as before. 130 | 131 | ## Bootstrapping 132 | 133 | There are two main ways of bootstrapping our Lambda function: 134 | * Changing the assembly name to _bootstrap_ 135 | * Adding a shell script named _bootstrap_ 136 | 137 | Changing the assembly name to _bootstrap_ could be done in our `.csproj`. Although it's a seemingly harmless change, it tends to confuse developers and others when the "main" dll goes missing from the build output and an extensionless _bootstrap_ file is present instead. 138 | 139 | Therefore my preferred way is adding a shell script named _bootstrap_: 140 | 141 | ```bash 142 | // bootstrap 143 | #!/bin/bash 144 | 145 | ${LAMBDA_TASK_ROOT}/MyApi 146 | ``` 147 | 148 | `LAMBDA_TASK_ROOT` is an environment variable available when the Lambda is run on AWS. 149 | 150 | We also need to reference this file in our `.csproj` to make sure it's always published along with the rest of our code: 151 | 152 | ```xml 153 | // MyApi.csproj 154 | ... 155 | 156 | 157 | Always 158 | 159 | 160 | ... 161 | ``` 162 | 163 | ## Creating the Lambda package 164 | 165 | We will be using the _dotnet lambda cli tool_ to package our application. (I find it has some advantages over a plain `dotnet publish` followed by `zip`.) 166 | 167 | `dotnet new tool-manifest` 168 | `dotnet tool install amazon.lambda.tools --version "5.*"` 169 | 170 | I prefer to install tools like this locally. I believe global tools will eventually cause you to run into version conflicts. 171 | 172 | We also add a default parameter to msbuild, so we don't have to specify it on the command line. 173 | 174 | ```json 175 | // aws-lambda-tools-defaults.json 176 | { 177 | "msbuild-parameters": "--self-contained true" 178 | } 179 | ``` 180 | 181 | Building and packaging the application is done by 182 | `dotnet lambda package -o dist/MyApi.zip` 183 | 184 | ## Deploying to AWS 185 | 186 | The way I prefer to deploy simple Lambdas is by using the [Serverless framework](https://www.serverless.com). 187 | 188 | (For an excellent comparison between different tools of this kind for serverless deployments on AWS, check out [this post](https://dev.to/tastefulelk/serverless-framework-vs-sam-vs-aws-cdk-1g9g) by Sebastian Bille.) 189 | 190 | You might argue that Terraform has emerged as the de facto standard for IaC. I would tend to agree, but it comes with a cost in terms of complexity and state management. For simple setups like this, I still prefer the _Serverless_ framework. 191 | 192 | We add some basic configuration to our `serverless.yml` file: 193 | 194 | ```yaml 195 | // serverless.yml 196 | service: myservice 197 | 198 | provider: 199 | name: aws 200 | region: eu-west-2 201 | httpApi: 202 | payload: "2.0" 203 | lambdaHashingVersion: 20201221 204 | 205 | functions: 206 | api: 207 | runtime: provided.al2 208 | package: 209 | artifact: dist/MyApi.zip 210 | individually: true 211 | handler: required-but-ignored 212 | events: 213 | - httpApi: "*" 214 | ``` 215 | 216 | Even though we are using AspNetCore, a Lambda is really just a function. AWS therefore requires an API Gateway in front of it. _Serverless_ takes care of this for us. The combination of _httpApi_ and _2.0_ here means that we will use the new _HTTP_ trigger of the API Gateway. This would be my preferred choice, as long as we don't need some of the [functionality](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html) still only present in the older _REST_ trigger. 217 | 218 | _runtime: provided.al2_ means we will use the custom runtime based on Amazon Linux 2. 219 | 220 | Now we are finally ready to deploy our Lambda! 221 | 222 | `npx serverless@^2.70 deploy` 223 | 224 | The output should look something like this: 225 | 226 | ``` 227 | ... 228 | endpoints: 229 | ANY - https://ynop5r4gx2.execute-api.eu-west-2.amazonaws.com 230 | ... 231 | ``` 232 | 233 | Here you'll find the URL where our Lambda can be reached. Let's call this <YourUrl>. 234 | 235 | ## Calling the Lambda 236 | 237 | Using [curl](https://curl.se): 238 | `curl "https:///graphql?query=%7B+sysInfo+%7D"` 239 | 240 | You should see a response similar to: 241 | ```json 242 | { "data": { "sysInfo":".NET 6.0.1 running on amzn.2-x64" } } 243 | ``` 244 | 245 | ## Cleaning up 246 | 247 | Unless you want to keep our Lambda running, you can remove all deployed AWS resources with: 248 | `npx serverless@^2.70 remove` 249 | 250 | [Take me to the summary!](#summary) 251 | 252 | ## Bonus: Running on ARM 253 | 254 | AWS recently announced the possibility to run Lambda on the new ARM-based Graviton2 CPU. It's marketed as [faster and cheaper](https://aws.amazon.com/about-aws/whats-new/2021/09/better-price-performance-aws-lambda-functions-aws-graviton2-processor/). Note that ARM-based Lambdas are not yet available in all AWS regions and that they might not work with pre-compiled x86/x64 dependencies. 255 | 256 | If we want to run on Graviton2 a few small changes are necessary: 257 | * Compiling for ARM 258 | * Configuring Lambda for ARM 259 | * Add additional packages for ARM 260 | 261 | ### Compiling for ARM 262 | 263 | Here we need to add our runtime target for the dotnet lambda tool to pick up: 264 | 265 | ```json 266 | // aws-lambda-tools-defaults.json 267 | { 268 | "msbuild-parameters": 269 | "--self-contained true --runtime linux-arm64" 270 | } 271 | ``` 272 | 273 | ### Configure Lambda for ARM 274 | 275 | We need to specify the architecture of the Lambda function: 276 | 277 | ```yaml 278 | // serverless.yml 279 | functions: 280 | api: 281 | ... 282 | architecture: arm64 283 | ... 284 | ``` 285 | 286 | ### Adding additional packages for ARM 287 | 288 | According to this [GitHub issue](https://github.com/aws/aws-lambda-dotnet/issues/920) we need to add and configure an additional package when running a custom runtime on ARM: 289 | 290 | ```xml 291 | // MyApi.csproj 292 | ... 293 | 294 | 297 | 300 | 301 | ... 302 | ``` 303 | 304 | When adding this the API stops working on non-ARM platforms though. A more portable solution is to use a condition on the `ItemGroup`, like this: 305 | 306 | ```xml 307 | // MyApi.csproj 308 | ... 309 | 310 | 313 | 316 | 317 | ... 318 | ``` 319 | 320 | ### Building, deploying, and calling it once more 321 | 322 | Build and deploy as before. 323 | 324 | Call the Lambda as before. 325 | 326 | You should see a response similar to: 327 | ```json 328 | { "data": { "sysInfo":".NET 6.0.1 running on amzn.2-arm64" } } 329 | ``` 330 | confirming that we are now running on ARM! 331 | 332 | Clean up as before. 333 | 334 | ## Summary 335 | 336 | That's it! We have now deployed a minimal serverless GraphQL API in .NET 6 on AWS Lambda. Full working code is available at [GitHub](https://github.com/memark/GraphQL-in-DotNet-6-on-AWS-Lambda). 337 | 338 | Opinionated take aways: 339 | * Use GraphQL for most APIs 340 | * Use Hot Chocolate for GraphQL on .NET 341 | * Use Lambda for entire APIs, not just simple functions 342 | * Use _dotnet lambda cli tool_ for packaging 343 | * Use _Amazon.Lambda.AspNetCoreServer.Hosting_ for custom runtimes 344 | * Use a simple _bootstrap_ script to start the API 345 | * Use _Serverless_ framework for deployment 346 | * Use ARM if you can 347 | 348 | Any comments or questions are welcome! 349 | -------------------------------------------------------------------------------- /MyApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Always 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | builder.Services 4 | .AddGraphQLServer() 5 | .AddQueryType(); 6 | 7 | builder.Services 8 | .AddAWSLambdaHosting(LambdaEventSource.HttpApi); 9 | 10 | var app = builder.Build(); 11 | 12 | app.UseRouting(); 13 | 14 | app.UseEndpoints(endpoints => 15 | endpoints.MapGraphQL()); 16 | 17 | await app.RunAsync(); -------------------------------------------------------------------------------- /Query.cs: -------------------------------------------------------------------------------- 1 | using static System.Runtime.InteropServices.RuntimeInformation; 2 | 3 | public class Query { 4 | public string SysInfo => $"{FrameworkDescription} running on {RuntimeIdentifier}"; 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL-in-DotNet-6-on-AWS-Lambda 2 | 3 | This is the accompanying code to this [dev.to blog post](https://dev.to/memark/running-a-graphql-api-in-net-6-on-aws-lambda-17oc). 4 | -------------------------------------------------------------------------------- /aws-lambda-tools-defaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "msbuild-parameters": "--self-contained true --runtime linux-arm64" 3 | } -------------------------------------------------------------------------------- /bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ${LAMBDA_TASK_ROOT}/MyApi -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: myservice 2 | 3 | provider: 4 | name: aws 5 | region: eu-west-2 6 | httpApi: 7 | payload: "2.0" 8 | lambdaHashingVersion: 20201221 9 | 10 | functions: 11 | api: 12 | runtime: provided.al2 13 | architecture: arm64 14 | package: 15 | artifact: dist/MyApi.zip 16 | individually: true 17 | handler: required-but-ignored 18 | events: 19 | - httpApi: "*" --------------------------------------------------------------------------------