Thursday, March 8, 2018

Azure Container Instance (5): Building HTTPS Websites


HTTPS websites are more and more popular recently. Now you can also build websites with HTTPS connections via Azure Container Instance, with secret volumes and DNS Name Labels. This article is to demonstrate the steps about how to setup HTTS connections for a Node.js website.

Prerequisite: Have a Certificate

You may have a certificate for the SSL connection. If your DNS name label xyz, and your container group is going to be created in WestUS region, the fully qualified domain name looks like xyz.westus.azurecontainer.io and your certificate should match it. If you have a CName for your website, your certificate should match the CName.

Step 1: Build the Image

If you are going to build your website with Node.js, you may setup HTTPS connections with the following code:

const fs = require('fs');
const https = require('https');
const express = require('express');
const morgan = require('morgan');
const options = {
    pfx: fs.readFileSync('certificate.pfx'),
    passphrase: fs.readFileSync('certificatepassword.txt')
};

const app = express();
app.use(morgan('combined'));

app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html')
});

var listener = https.createServer(options, app).listen(process.env.PORT || 443, function () {
    console.log('listening on port ' + listener.address().port);
});


Notice that the two files ‘certificate.pfx’ and ‘certificatepassword.txt’ are the certificate and its password. Before we start to run Node.js, these two files are copied from the path /mnt/secrets, as shown in Dockerfile:

CMD cp /mnt/secrets/sslcertificateData /usr/src/app/certificate.pfx && cp /mnt/secrets/sslcertificatePwd /usr/src/app/certificatepassword.txt && node /usr/src/app/index.js

We are going to pass these secrets from the Azure deployment template.

All other code for the Node.js website and the Docker file are shared at https://github.com/zhedahht/aci-ssl-helloworld. And the built image is shared at https://hub.docker.com/r/containerinstance/helloworld/.

Step2: Define the Deployment Template

The next step is to define Azure deployment template, and the following is a sample:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "containergroupname": {
      "type": "string",
      "metadata": {
        "description": "Name for the container group"
      }
    },
    "containername": {
      "type": "string",
      "metadata": {
        "description": "Name for the container"
      },
      "defaultValue": "container1"
    },
    "imagename": {
      "type": "string",
      "metadata": {
        "description": "Name for the image"
      },
      "defaultValue": "containerinstance/helloworld:ssl"
    },
    "volumename": {
      "type": "string",
      "metadata": {
        "description": "Name for the secret volume"
      },
      "defaultValue": "volume1"
    },
    "dnsnamelabel": {
      "type": "string",
      "metadata": {
        "description": "The DSN name label"
      }
    },
    "sslcertificateData": {
      "type": "securestring",
      "metadata": {
        "description": "Base-64 encoded authentication PFX certificate."
      }
    },
    "sslcertificatePwd": {
      "type": "securestring",
      "metadata": {
        "description": "Base-64 encoded password of authentication PFX certificate."
      }
    },
    "port": {
      "type": "string",
      "metadata": {
        "description": "Port to open on the container and the public IP address."
      },
      "defaultValue": "443"
    },
    "cpuCores": {
      "type": "string",
      "metadata": {
        "description": "The number of CPU cores to allocate to the container."
      },
      "defaultValue": "1.0"
    },
    "memoryInGb": {
      "type": "string",
      "metadata": {
        "description": "The amount of memory to allocate to the container in gigabytes."
      },
      "defaultValue": "1.5"
    }
  },
  "variables": {},
  "resources": [
    {
      "name": "[parameters('containergroupname')]",
      "type": "Microsoft.ContainerInstance/containerGroups",
      "apiVersion": "2018-02-01-preview",
      "location": "[resourceGroup().location]",
      "dependsOn": [],
      "properties": {
        "containers": [
          {
            "name": "[parameters('containername')]",
            "properties": {
              "command": [],
              "image": "[parameters('imagename')]",
              "ports": [
                {
                  "port": "[parameters('port')]"
                }
              ],
              "resources": {
                "requests": {
                  "cpu": "[parameters('cpuCores')]",
                  "memoryInGb": "[parameters('memoryInGb')]"
                }
              },
              "volumeMounts": [
                {
                  "name": "[parameters('volumename')]",
                  "mountPath": "/mnt/secrets",
                  "readOnly": false
                }
              ]
            }
          }
        ],
        "osType": "Linux",
        "ipAddress": {
          "type": "Public",
          "dnsNameLabel": "[parameters('dnsnamelabel')]",
          "ports": [
            {
              "protocol": "tcp",
              "port": "[parameters('port')]"
            }
          ]
        },
        "volumes": [
          {
            "name": "[parameters('volumename')]",
            "secret": {
              "sslcertificateData": "[parameters('sslcertificateData')]",
              "sslcertificatePwd": "[base64(parameters('sslcertificatePwd'))]"
            }
          }
        ]
      }
    }
  ],
  "outputs": {
    "containerIPAddressFqdn": {
      "type": "string",
      "value": "[reference(resourceId('Microsoft.ContainerInstance/containerGroups/', parameters('containergroupname'))).ipAddress.fqdn]"
    }
  }
}

In the template above, the certificate is mounted as the file /mnt/secrets/sslcertificateData, and the password is mounted as the file /mnt/secrets/sslcertificatePwd. These files will be copied and pasted as /user/src/app/certificate.pfx and /user/src/app/certificatepassword.txt respondingly, and they are accessible to Node.js.

The DNS name labels is defined in the property dnsNameLabel of ipAddress. The fully qualified domain name is a concatenation of the DSN name label, the location and “azurecontainer.io”. 

The deployment template and the corresponding parameters are shared at https://github.com/Azure/azure-quickstart-templates/tree/master/201-aci-linuxcontainer-volume-secret.

AKS (1) - Five seconds latency when resolving DNS

We intermittently meet 5s latencies in an AKS clusters with CNI when it’s resolving DNS. This article is to summarize what we have learned...