├── .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: "*"
--------------------------------------------------------------------------------