Using function proxies with Azure Static Web Apps
Learn how to use Azure Functions Proxies with Azure Static Web Apps
Azure Static Web Apps is a service that provides you with a way to serve static content with an Azure Functions backend API. It's currently in preview, but it already looks promising, giving you a variety of useful features to build a web app. You can pull your code directly from GitHub, then Azure will add a GitHub Actions workflow to build and deploy to Azure Static Web Apps. It truly is a great service, and as of the day of writing this blog post, it's completely free, since it's on preview. You can read about all of Azure Static Web Apps features here.
The integrated API is a nice way to not worry about things like CORS, since it will be hosted on the same domain as the website on the /api
route. However, it currently imposes some limitations on what you can do with your function app:
- Not every language is supported. You can only create your function app on JavaScript, C# and Python. This will probably be fixed in the future, as there's no good reason to not support other languages IMO.
- You can only add HTTP triggers. This one probably is going to remain this way, since it's purpose is to process requests from the frontend, but maybe I'm wrong. Input and output bindings works normally.
- Logs are only available through Application Insights. This means that you can't really troubleshoot the resource without creating an Application Insights resource along with your Static Web App. I'm not sure if this means that you cannot add other log providers through DI, but I'll investigate further later.
With that in mind, being limited to only HTTP triggers is not so bad, as you can use the Static Web App's function app as a proxy to other services using function proxies.
Function Proxies
Azure Function Proxies allow you to specify routes that will be implemented by another resource, with the option to override the HTTP method, the query string or the request headers. It's a pretty good tool for breaking a big service into smaller parts in a serverless architecture.
To add a proxy to your function app, you just need to define a proxies.json
file on your project (Don't forget to copy the file to the build output!) and configure it according to your needs. This is how a proxies file look like:
{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"<proxy-name>": {
"matchCondition": {
"route": "/api/todo"
},
"backendUri": "%service-url%/api/todo",
"requestOverrides": {
"backend.request.querystring.<querystring-name>": "example-value",
"backend.request.header.<header-name>": "example-value",
},
"responseOverrides": {
"response.body": {
"message": "Hello, world!"
},
"response.statusCode": "418"
}
}
}
}
The example above does some things that are worth explaining, we're doing the following:
- Creating a proxy with the name
<proxy-name>
that will be triggered when the app receives a request on the/api/todo
route. You can also specify HTTP methods on the match condition. - Setting the
backendUri
to the service that implements our functionality. This is what will be called when the request matches the condition defined.%service-url%
is the syntax used for configuration (yourlocal.settings.json
file or your environment variables). - Overriding the request's
<header-name>
header and<querystring-name>
query string key with the value 'example-value'. - Overriding the response's body and status code.
There are more things that can be configured in your proxies file, you can read more about it here.
Function Proxies on SWAs
Since SWA allows us to use its function app as a proxy, I made a static app that calls two other function apps through function proxies. This is how our services will communicate:
The code for this blog post is available on this GitHub repository:
Catalog Service
This function app has one HTTP function that returns an array of catalog items. You can page the results by changing the index
value on the query string.
Orders Service
This function app also has one HTTP function that return an order creation result. If you send any order item on the request, you'll receive a 200 OK with an order ID and a status. If you send an empty array, you'll receive a 400 BAD REQUEST.
SWA Frontend
This is a Blazor WebAssembly app that allows you to browse through catalog items and add them to an in-memory shopping cart. After that, you can finish your shopping by creating an order. It also has a routes.json
configured to serve the same file (index.html
) on every route, this is required and you can read more about it here.
SWA Backend
This is a simple function app that includes a proxies.json
file that maps to the two other function apps' backendUri
using environment variables (%CONNECTIONSTRINGS__CATALOG%
and %CONNECTIONSTRINGS__ORDERS%
). Both proxies override the query string's code
value to include the function app's function key that is required for authentication in production, we'll see how to set these values later on, but they're not required in your development environment.
NOTE: Currently, you can't add configuration settings on a SWA that doesn't have any functions in it, so I also included a dummy HTTP function to allow configuration in production. I believe that it should be a valid scenario to only have proxies on your backend function app, so I created an issue on the Static Web App's GitHub repository. I'll update this post with any info they provide me.
On the local.settings.json
file, there are two important settings that are worth explaining:
"AZURE_FUNCTION_PROXY_DISABLE_LOCAL_CALL": true,
"SERVICE__CATALOG__KEY": "catalog_key" ,
"SERVICE__ORDERS__KEY": "orders_key"
},
"Host": {
"CORS": "*"
AZURE_FUNCTION_PROXY_DISABLE_LOCAL_CALL
: For some reason, when calling your proxy in development gives an error saying that a recursive call was detected, even though it's correctly configured to call another function app listening in another port. Setting this variable totrue
fixes the problem.CORS:*
: Since we're running this app locally, you'll receive an error if this setting is not present. That's because the Blazor app is running on a different port than the Backend function app. It will work correctly on production.
Running the application
To make testing easier, I've added a tye.yaml
file that spins up all four services with one command. It also gives us a nice dashboard with logs and other useful info about the running services. If you never heard about Tye, you should really check it out, it's a wonderful tool that helps a lot with the development of microservices, but all you need to know for now is how to install the tool (dotnet tool install -g Microsoft.Tye --version "0.6.0-alpha.21070.5"
) and run it (tye run
).
name: my-store
services:
- name: frontend
project: app/MyStore.App.csproj
bindings:
- protocol: http
- name: bff
azureFunction: bff/
bindings:
- protocol: http
port: 7070
- name: catalog
azureFunction: catalog/
bindings:
- protocol: http
connectionString: ${protocol}://${host}:${port}
- name: orders
azureFunction: orders/
bindings:
- protocol: http
connectionString: ${protocol}://${host}:${port}
The YAML file configures the SWA backend app to always run on the port 7070 and assign the other three services random ports. It also sets the environment variables required for the SWA backend to call the Orders and Catalog service, that happens through the connectionString
binding defined on the file. When running the command tye run
, you should see the following:
The dashboard is hosted on http://localhost:8000
, it should list you all four services:
Finally, when you access the frontend app, you should be able to add items to your card and finish your order!
NOTE: For some reason, requests going through the proxy always take at least two seconds. Do not panic, as this does not seem to be happening in production.
Creating and configuring your Azure Static Web App
Now that you've seen the app in action, it's time to deploy it to production and configure it. If you need instructions on how to create an Azure Static Web App, check out this Microsoft article.
Now we need to configure our SWA function app and add our function keys and URLs. You can get the function key for each function app running the following commands on the Azure CLI :
az functionapp function keys list -g <ResourceGroupName> -n <CatalogFunctionAppName> --function-name get-catalog
az functionapp function keys list -g <ResourceGroupName> -n <OrdersFunctionAppName> --function-name create-order
These should be added in the Configuration section as SERVICE__CATALOG__KEY
and SERVICE__ORDERS__KEY
, respectively. As for the URLs, they should be add as CONNECTIONSTRINGS__CATALOG
and CONNECTIONSTRINGS__ORDERS
. You can get them using the following command:
az functionapp config hostname list -g <ResourceGroupName> --webapp-name <FunctionAppName>
After all that, your should be up and running, and your Configuration section should be looking like this:
Conclusion
I hope you found this post useful. Static Web Apps have amazing potential and I'm hopeful the service will only get better, if you have any questions or feedback, please let me know in the comments!