Improved Local Dev With CosmosDB and devcontainers

Wednesday, Aug 24, 2022 4 minute read Tags: javascript vscode cosmosdb

Last year I wrote a post on using the CosmosDB Docker-based emulator with devcontainers and since then I’ve used that pattern many times to build applications, but there was one thing that kept bothering me, having to disable SSL for Node.js.

Sure, disabling SSL with NODE_TLS_REJECT_UNAUTHORIZED wasn’t a huge pain, but it did feel like a dirty little workaround, it also hit a snag - dotnet projects.

I had the idea that I should add the CosmosDB emulator to the devcontainer used by FSharp.CosmosDb, as I kept deleting the Azure resource that I used between when I was working on it. But when I’d set the account host to https://cosmos:8081 for the connection string, it’d fail to do queries as the self-signed certificate was rejected.

I guess it’s time to install the certificate.

The emulator provides the certificate at a well-known endpoint, which you can get using cURL:

1
curl -k https://$ipaddr:8081/_explorer/emulator.pem > emulatorcert.crt

But when should we run that, and what’s the IP of the Cosmos emulator container?

Installing the certificate

Because we need to wait until the containers have started, we’ll use the postCreateCommand in the devcontainer.json file, and we’ll have it call a bash script. Here’s the bash script:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/usr/bin/env bash

set -euxo pipefail

ipAddress=https://$(docker inspect cosmos -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'):8081

# Try to get the emulator cert in a loop
until sudo curl -ksf "${ipAddress}/_explorer/emulator.pem" -o '/usr/local/share/ca-certificates/emulator.crt'; do
  echo "Downloading cert from $ipAddress"
  sleep 1
done

sudo update-ca-certificates

To get the IP of the emulator, we’ll use docker inspect and in the docker-compose I set a name for the container, cosmos, so that it’s a well-known name (we could make an assumption of the name, based off the way compose names containers, but this is safest), and we provide a template to grab the IP from the JSON response - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}. This is combined with the protocol/port information to make a variable for the IP address to then download and install the certificate as described here.

Setting the connection info

With the certificate installed, it might be convenient to set the connection string information so that it can be used. Initially, I thought to use environment variables (since we have the IP as a bash variable) and load them with the Microsoft.Extensions.Configuration.EnvironmentVariables NuGet package, so we can add a export ipAddress to the end of the bash script (or maybe make the variable something easier to parse into the dotnet config system), but it turns out that you can’t export variables from postCreateCommands (see this issue).

Since that was off the table, an alternative solution would be to dump the info out as a file on disk. Here’s the dotnet approach for my project, you just have to adapt the file (and its contents) for your project needs:

1
2
3
4
if [ ! -f ./samples/FSharp.CosmosDb.Samples/appsettings.Development.json ]
then
  echo '{ "Cosmos": { "EndPoint" : "'$ipAddress'" } }' >> ./samples/FSharp.CosmosDb.Samples/appsettings.Development.json
fi

Note: I have the Access Key for cosmos in the docker-compose file, but you could also dump it out here if you prefer.

And with that, when the container starts, the connection to Cosmos is ready for your application to use.

Summary

In this post we’ve seen how we can run the Docker CosmosDB emulator side-by-side with our app container using a VS Code devcontainer. The full definitions that I published for my project can be found here.

Now that I’ve figured out how to do this, I’m going to be going back and retrofitting some other repos so that I don’t have to disable SSL validation for Node.js apps, making it more secure to run them locally.

Addendum

After writing this post and going back to some JavaScript/Node.js projects, I found that they were still failing with an invalid certificate and it turns out that if I’d read the docs fully I’d have know this. It seems that while dotnet applications running on Linux respect the certificate store, Node.js apps don’t, so you need to explicitly add the certificate using the NODE_EXTRA_CA_CERTS environment variable, so I’ve added "NODE_EXTRA_CA_CERTS": "/usr/local/share/ca-certificates/emulator.crt" to the remoteEnv section of the devcontainer.json file… sigh.