getZxy with Azure Function app entry point
This PR adds code that can be used to deploy protomaps on Azure. See README.md for more info.
It is intended to be deployed in an Azure Function App what is exposed though a CDN.
The pmtiles file can be stored at any URL, but if you use Azure then you probably want to deploy it to Azure Blob Storage. Just make sure the function app has access to the container/file.
This exact code has note been tested. I just used it as base for a Function App, which has some modifications that I didn't want to include here.
Most of the code should work for any Source and any back-end, with minor modifications.
Thanks, this looks like a good starting point. The two vital features needed are
- the azure function needs to be able to authenticate with a private Blob Storage bucket - were you able to accomplish this?
- The azure function needs to integrate with an edge-cached CDN, which I think in Azure requires Front Door - or are you using a different CDN?
Thanks, this looks like a good starting point. The two vital features needed are
- the azure function needs to be able to authenticate with a private Blob Storage bucket - were you able to accomplish this?
- The azure function needs to integrate with an edge-cached CDN, which I think in Azure requires Front Door - or are you using a different CDN?
- No, I have tried it. I used a public container. But it should be possible to do it with a private container and a SAS token.
- I used Azure Classic CDN. It is cheaper and is probably better for static content.
I will try with SAS token next week and get back to you.
@tengl the preference is for the authentication between Azure Functions and Blob Storage to be role-based without needing to specify a hardcoded key/secret. in Cloudflare this is done via Workers Bindings and in AWS via IAM roles.
The principle of the AWS and Cloudflare integrations is to present a well-tested and uniform solution for deploying protomaps within a single provider. It is of course possible to mix-and-match - e.g. Azure Functions serving from an AWS S3 Bucket - but the cost model is less efficient and it becomes difficult to debug across providers. So an integrated solution for Azure will be extremely valuable.
I see. I guess it should be possible using this tutorial, https://learn.microsoft.com/en-us/azure/app-service/tutorial-connect-app-access-storage-javascript?tabs=azure-portal. I've only browsed through it, so I'm not sure.
We could make an AzureStorageSource and use that instead of the FetchSource. But I will probably not have time to do it this week, we'll see if I can manage next week. Feel free to modify the PR if you have the time.
The SAS token and private container works, as expected. We will go for that solution until we find a way to use the storage blob SDK.
@bdon I just pushed some updates. I made an AzureStorageSource that should be able to use managed identities. I haven't tested it properly yet, but I will hopefully do it next week.
It still supports FetchSource.
@bdon I pushed some fixes. It seems to work now, at least on one of my pmtiles files.
I've tested on two different pmtiles files and both local debug and on Azure. All combinations work when using PMTILES_PATH and FetchSource and as well as when using AzureStorageSource (Azure Storage managed identities) by specifying the storage details.
A caveat though, it doesn't work if multiple maps are enabled and they mix sources. In that case I get an error saying incorrect header check for one of the pmtiles files. It probably has something to do with caching.
A note, using FetchSource and SAS token URLs seems to be a lot faster than using Azure Managed Identities. I'm sure/hope that it can be optimized, because right now it is too slow.
I consider it done, but there are some fixes for edge cases and optimizations to be made. I won't have the time to continue work on this at the moment.
A caveat though, it doesn't work if multiple maps are enabled and they mix sources. In that case I get an error saying incorrect header check for one of the pmtiles files. It probably has something to do with caching.
What does "mix sources" mean? In the AWS and Cloudflare setups, a single instance of the function can serve multiple PMTiles files, so the cache in global memory needs to key portions of each file based on the filename.
What does "mix sources" mean? In the AWS and Cloudflare setups, a single instance of the function can serve multiple PMTiles files, so the cache in global memory needs to key portions of each file based on the filename.
I made it a bit more flexible, so that the code can accept any kind of source. You can have one pmtiles files served by a FetchSource and another served by a AzureStorageSource. The file is resolved by appending {map_name} to the parameter. If I only use one type of source for multiple pmtiles files, it works, but if I have one FetchSource and one AzureStorageSource I get the error. I've tried both ResolvedValueCache and SharedPromiseCache, so the problem should not be related to the caches.
@bdon I fixed it now. It was something wrong with the buffer. I don't really understand what, but if you look at commit https://github.com/protomaps/PMTiles/pull/197/commits/6dd2e6109dd7a54ed3bb845290f0f31b264818d0 maybe you can figure it out.
Before that change, the data.buffer.length would always be 8192, even if data.length was 1905.
Finally revisiting this, some thoughts:
- It looks like since SAS tokens are the only efficient way to authenticate requests to a storage account / container, we might as well use raw
Fetch. TheFetchSourcefrom thepmtileslibrary is, as of v3.0.0, specific to browsers, so we probably want our own implementation that just does something like this:
async getBytes(
offset: number,
length: number,
signal?: AbortSignal,
etag?: string
): Promise<RangeResponse> {
const requestHeaders = new Headers();
requestHeaders.set("range", `bytes=${offset}-${offset + length - 1}`);
if (etag) requestHeaders.set("if-match", etag);
const url = `https://${storageAccountName}.blob.core.windows.net/${containerName}/${this.archiveName}${sasToken}`;
let resp = await fetch(url, {
signal: signal,
headers: requestHeaders,
});
The SAS token can be scoped to a container's read operations, but it looks like the way to use them is to set an expiration date far into the future. Is there any alternative SAS token generation feature equivalent to AWS Roles?
Also, ideally we can deploy the code as-is using az functionapp deployment source config-zip -g RESOURCE_GROUP -n FUNCTIONAPP_NAME --src ZIPFILE.zip - it seems like it expects a certain zip file structure for NodeJS apps, were you able to deploy using the CLI or are you using some IDE integration?
- It looks like since SAS tokens are the only efficient way to authenticate requests to a storage account / container, we might as well use raw
Fetch.
Yes, we use SAS tokens and it's working great so far.
The
FetchSourcefrom thepmtileslibrary is, as of v3.0.0, specific to browsers,
I didn't realize that :-/. We used the FetchSource and it's working fine.
so we probably want our own implementation that just does something like this:
Yes, that might be good enough. The only thing (I think) specific to Azure Functions is the entry point, every thing else should be generic.
I think the only parameter to the function should be the URL, which includes the SAS token because that's how you copy it from Azure Portal. No need to split it up in multiple parameters. It also makes it easier to support other hosting scenarios, only the entry points needs to be customized as long as the pmtiles file is hosted via http.
The SAS token can be scoped to a container's read operations, but it looks like the way to use them is to set an expiration date far into the future. Is there any alternative SAS token generation feature equivalent to AWS Roles?
Not that I am aware of, apart from Managed Identities.
Also, ideally we can deploy the code as-is using
az functionapp deployment source config-zip -g RESOURCE_GROUP -n FUNCTIONAPP_NAME --src ZIPFILE.zip- it seems like it expects a certain zip file structure for NodeJS apps, were you able to deploy using the CLI or are you using some IDE integration?
I used VS Code integration. In my experience that is the best way if you want to make changes to the code. But I'm no expert in Azure Functions, I just dot the minimal to get the job done :-)
If we want to deploy directly to Azure the CLI is probably the best way. Maybe just create a bash and ps1 script for that?
I didn't realize that :-/. We used the FetchSource and it's working fine.
It should work fine, but has some logic that is irrelevant for non-browser usage, and won't have efficient ETag handling that we just added to AWS / Cloudflare.
Another option for Azure is to use Azure Container Apps and not use Functions at all, but I don't use Azure enough to get a sense of which is better supported. Azure Functions seems like a mess with multiple runtimes, versions and upload methods, but Containers might have slow start times.
FYI, I have been mostly working on the Google Cloud integration which uses a Docker container on Docker Registry.
https://docs.protomaps.com/deploy/google-cloud
If this deployment method works scale-to-zero on Azure Containers, that is preferable over a custom Function App because it's simply less code. But I haven't succeeded in deploying this (or getting a scale-to-zero config). Anyone with Azure insight want to try it out?
Azure Service Connectors are the solution for connecting Container Apps and Blob Storage:
https://learn.microsoft.com/en-us/azure/service-connector/quickstart-portal-container-apps?tabs=SMI
I have a basic POC somewhat working, but evaluating how the DX and latency is.
Great, sounds promising.
Sorry, I had to drop this, our current solution works great for our needs and I needed to focus on other things.
Edit; I had a quick look and it seems that service connectors was not supported for Azure Functions in our region. Maybe it is for Container Apps, but they are more expensive.
How do Cold Starts look for your Azure Functions deployment?
For my basic Container Apps pmtiles deployment I see about 7 seconds scaling from 0 to 1.
https://github.com/microsoft/azure-container-apps/issues/997
That might be a dealbreaker, but it looks like there's also a billing model where scale=1 is minimum with idle pricing - not true scale to 0 like Function Apps though.
I'm going to close this PR in favor of moving forward with Azure Container Apps, which don't require us to maintain another PMTiles decoding implementation against Azure APIs, just the same Docker Image used for Google Cloud.
https://github.com/protomaps/docs/pull/52/files
Would appreciate if anyone using Azure could try out the instructions on that PR and leave feedback.
Thank you for the contributions here.
How do Cold Starts look for your Azure Functions deployment?
For my basic Container Apps pmtiles deployment I see about 7 seconds scaling from 0 to 1.
microsoft/azure-container-apps#997
That might be a dealbreaker, but it looks like there's also a billing model where scale=1 is minimum with idle pricing - not true scale to 0 like Function Apps though.
I haven't noticed any issues with cold starts in Azure Functions. When I looked at it, Container Apps would have been a lot more expensive if we were to have scale=1. But there might be more and better options now.
I'm going to close this PR in favor of moving forward with Azure Container Apps, which don't require us to maintain another PMTiles decoding implementation against Azure APIs, just the same Docker Image used for Google Cloud.
https://github.com/protomaps/docs/pull/52/files
Would appreciate if anyone using Azure could try out the instructions on that PR and leave feedback.
Thank you for the contributions here.
For now, I will continue to use Azure Functions but I might try Container Apps later. Thanks for a great project!
We added support for ETag detection in https://github.com/protomaps/go-pmtiles/releases/tag/v1.21.0
so https://docs.protomaps.com/deploy/azure with Azure Blob Storage now supports updating a pmtiles file in-place on cloud storage.
@tengl Hello, Would you be able to provide the link to the last stable version of the Azure Function? I have tried using the 'go-pmtiles' image for the Azure Container App but have reached a dead-end with the container app not working and a microsoft tech support suggesting not to use 'Service Connector' as it is in 'Preview' and unstable.
I understand that this is not a recommended way of using in the Azure environment by @bdon. However, I am stuck with the Azure environment and I am unable to make the Azure container app to work. Thanks, Kiran
@tengl Hello, Would you be able to provide the link to the last stable version of the Azure Function?
Sure, I made a new repo as the one we use has some specific solutions. You might need to do some adjustments. It only has code for Azure Functions and is intended to be used behind a CDN.
https://github.com/tengl/pmtiles-func
Thank you. Apologize for the late reply.