Sunday 10 February 2019

Using the Cosmos DB emulator across the network

It is very useful to be able to use the Cosmos DB emulator across the network; a good example being that you may be building Linux docker images to run on Kubernetes and you want them to access the Cosmos DB Emulator running on your development Windows desktop. Currently the Cosmos DB emulator only runs inside a Windows container, so running it as a container here won't help.

This article describes some of the steps to get you there.

I start off with installing the emulator on a development PC (the server) and getting a different Windows machine to access the emulator across the network. This proves the connectivity and then I move to configuring my Linux docker app.

CLIENT: Configure a hostname for accessing the emulator on the client PC
I am using the hostname cosmosdb.test to access the emulator.

In the client machine, running as an administrator edit the hosts file C:\windows\system32\drivers\etc\hosts and add the line:

<my emulator machine ip> cosmosdb.test

Note that the <my emulator machine ip> cannot be 127.0.0.1 as any running container will resolve this locally and try and connect to itself rather than the host machine. It has to be the host machines real IP address! (Another alternative would be to use the host.docker.internal hostname throughout).

Flush the local DNS to be sure (especially if you've run this step before)
ipconfig /flushdns

SERVER: Install and Run the emulator
Install the Cosmos DB Emulator.

Start it.

Stop it.

SERVER: Configure Windows Defender Firewall
Ensure Cosmos DB Emulator has a firewall exception for port 8081.

Start > Run > Windows Defender Firewall > Advanced Settings > Inbound Rules > New Rule

Select Port > TCP and Specific Local Ports > Enter 8081 > Allow the Connection

Select the networks to which the rule should apply. If you are really unsure, select Domain, Network & Public (but at your own risk!)

Give the rule and name and description and select OK.

SERVER: Generate a self-signed SSL certificate allowing the host name

Start the emulator from the command-line, adding the parameter
/GenCert=cosmosdb.test,<IP address>

Make a note of the certificate thumbprint (the last 5 digits may be sufficient).

SERVER: Export the SSL certificate
Start > Manage Computer Certificates

Right click on the root node and select Find Certificates

Change Look in Field to SHA1 hash and enter the last 5 digits of the thumbprint from the previous step into the Contains field.

You should see one (or more) entries for the Cosmos DB certificate.

Right-click on one and select Export.

Select No, do not export the private key.

Select Base-64 encoded X.509 (CER)

Save
to a file. We will save it as cosmosdbemulator.cer (and we'll refer to that filename later).

CLIENT: Import the SSL certificate on the client machine
On the client machine:

Start > Manage Computer Certificates

Select Trusted Root Certification Authorities > Certificates

Select All Tasks > Import > Next

Select the filename and click Next until the certificate is imported.

Double-click on the newly imported certificate, select the Details tab and confirm that the Subject Alternative Name property includes cosmosdb.test.

SERVER: Create a new access key

Start the emulator from the command-line, adding the parameter
/GenKeyFile=MyKeyFile.txt

Open the key file and make a note of the access key as this will be set explicitly when we start the emulator in future.

In my example I will use:
1rAUqgeKitqNhTKyKMtFQ7UK79d9cQhEfoiXomlh3zc1Qz58uQQV6c49B2wGeC9FuGM+OmRViCHRuJu3llb0Vg==

SERVER: Configure and start the emulator

The next steps configure the emulator for allowing access across the network. You may want to modify the emulator shortcut to set these command-line parameters, or run the executable directly from the command-line. Just remember, in future you must always start the emulator with certain additional parameters.

Start the emulator with the following parameters:
/NoFirewall /AllowNetworkAccess /Key=<access key>

or in my example
/NoFirewall /AllowNetworkAccess /Key=1rAUqgeKitqNhTKyKMtFQ7UK79d9cQhEfoiXomlh3zc1Qz58uQQV6c49B2wGeC9FuGM+OmRViCHRuJu3llb0Vg==


The AllowNetworkAccess must be set for the emulator to accept connections across the network.

CLIENT: Check connectivity
On the client PC open the command prompt and type
telnet cosmosdb.test 8081

And you should connect. If you don't then your host name is pointing to the wrong IP address or the firewall is still blocking you. If so, check the previous steps.

CLIENT: Run the test code
Run the test app on the client PC.

I have found that the V3 Beta Cosmos DB SDK works better when connecting across the network. I recommend you use this for all new development.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos;
using Newtonsoft.Json;

namespace cosmosdb_test_v3
{
    class Program
    {
        static void Main(string[] args)
        {
            var cts = new CancellationTokenSource();

            Console.CancelKeyPress += (s, e) =>
            {
                e.Cancel = true;
                cts.Cancel();
            };

            MainAsync(args, cts.Token).Wait(cts.Token);

            Console.WriteLine("Program exited");
        }

        private static async Task MainAsync(string[] args, CancellationToken token)
        {
            try
            {
                const string databaseId = "testDb";
                const string accountEndpoint = "https://cosmosdb.test:8081";
                const string accountKey = "1rAUqgeKitqNhTKyKMtFQ7UK79d9cQhEfoiXomlh3zc1Qz58uQQV6c49B2wGeC9FuGM+OmRViCHRuJu3llb0Vg==";
                const string containerId = "testCollection";
                const string partitionKeyPath = "/id";
                var documentId = "1";

                if (args.Length > 0)
                {
                    documentId = args[0];
                }

                var cosmosConfiguration = new CosmosConfiguration(accountEndpoint, accountKey);
                using (var client = new CosmosClient(cosmosConfiguration))
                {
                    CosmosDatabase database = await client.Databases.CreateDatabaseIfNotExistsAsync(databaseId, cancellationToken: token);
                    CosmosContainer container = await database.Containers.CreateContainerIfNotExistsAsync(containerId, partitionKeyPath, cancellationToken: token);

                    var person = new Person
                    {
                        Id = "Bloggs_" + documentId,
                        FirstName = "Joe",
                        LastName = "Bloggs_" + documentId,
                        Age = 35
                    };

                    await container.Items.CreateItemAsync(person.Id, person, cancellationToken: token);
                    Console.WriteLine("Document " + documentId + " created");
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }
    }

    internal class Person
    {
        [JsonProperty("id")]
        public string Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
    }
}

SERVER: Confirm that the document has been written
Open Data Explorer and confirm that the document has been written to the emulator.

Congratulations! You should now have run the dotnet core console application on one machine and connected to your development machine. Now we are going to run the same dotnet core console app as a Linux container.

Run Client Code in a Linux Docker container
Create a cert folder in the project folder and copy cosmosdbemulator.cer into that folder.
Rename the certificate to use a crt extension: cosmosdbemulator.crt.

Add the following Dockerfile to the root of the project:

FROM microsoft/dotnet:2.1-sdk
WORKDIR /app

# copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# copy the SSL certificate
ADD cert/* /tmp/cert/
RUN apt-get install ca-certificates && \
    cp -R /tmp/cert/cosmosdbemulator.crt /usr/local/share/ca-certificates && \
    update-ca-certificates

# copy and build everything else
COPY . ./
RUN dotnet publish -c Release -o out
ENTRYPOINT ["dotnet", "out/cosmosdb-test-v3.dll"]

Using the command-line in the root of the project, type

docker build -t cosmosdb-test-v3 .

Look for the text
Updating certificates in /etc/ssl/certs...
1 added, 0 removed; done.

to check that the SSL certificate has been installed correctly.

Note for update-ca-certificates to work the certificate should be found in the /usr/local/share/ca-certificates folder and have the .crt file extension. A different file extension (such as .cer) will not work.

Now run the test app container.

docker run --rm cosmosdb-test-v3 <my document number>

You can set any document number (just don't repeat a number or you will get a duplicate document exception!)

docker run --rm cosmosdb-test-v3 15

You should see
Document 15 created
Program exited

Congratulations! You now have a Linux docker container running the dotenet core Cosmos DB V3 SDK, writing a document to your host development emulator.

Debugging the certificates
If at any time you need to check the successful installation of the certificates on the Linux container you can run the container with a shell:
docker run --entrypoint /bin/bash -it cosmosdb-test-v3

You can check connectivity with:
curl -kv https://host.docker.internal:8081 and
curl -kv https://cosmosdb.test:8081

You can check the specific certificate would work if it were installed in the CA store using:
curl -v https://cosmosdb.test:8081 --cacert /tmp/cert/<mycert>.crt

Subsequent Notes
This works only if you connect your Cosmos DB client to another machine on the network; it doesn't work if you connect your Cosmos DB client to the same machine.

No comments:

Post a Comment