Azure Container Instances fait partie des nombreuses façons d’exécuter des containers sur Azure.

J’ai déjà présenté dans un précédent article l’exécution de container sur App Service. Cette méthode expose automatiquement une URL en azurewebsites.net reconnue par les navigateurs. Malheureusement, on ne peut exposer qu’un port.

Microsoft propose déjà une méthode à base de NGINX pour exposer un point de terminaison en SSL. Malheureusement, la méthode décrite se base sur des certificats générés au préalable, voire auto-signés si on suit la doc !

Point d’attention sur Azure Files et client Let’s Encrypt

Azure Files est utilisé conjointant avec Azure Container Instances pour assurer la persistance des données. Les disques managés ne sont pas supportés.

De fait, Azure Files souffre de quelques limitations comme le non-support des liens symboliques, la gestion des droits POSIX, etc.

Mon premier choix pour l’enrôlement de certificat s’était porté sur certbot - le client recommandé par Let’s Encrypt - et son image Docker. Malheureusement, il s’est avéré que certbot utilise des liens symboliques pour gérer les certificats.

Let’s Encrypt propose une longue liste de client. Je me suis donc tourné vers acme.sh qui propose également une image Docker.

Les étapes

Le processus complet va se découper en 2 grandes étapes :

  1. L’émission du certificat

  2. L’utilisation du certificat

Etape 1 : émission du certificat

Cette étape va consister en :

  • La création du compte de stockage (Storage Account) et des partages associés (Files). Au moins 2 partages doivent être créés pour :

    • La config d’acme.sh

    • Les certificats à utiliser avec NGINX ultérieurement

  • La création de 2 répertoires nécessaires à acme.sh

  • La génération du certificat en utilisant le mode autonome d’acme.sh

  • L’installation des certificats pour l’utilisation par NGINX

Tout a été automatisé avec un ARM template. Ainsi, l’ARM template permet de :

  • Créer les ressources Azure

  • Création d’un Azure Container Instance s’appuyant sur l’image microsoft/azure-cli pour créer les répertoires. Cela correspond à exécuter commande Az CLI az storage directory create --name certs --share-name acme-cert

  • Création d’un Azure Container Instance avec l’image neilpang/acme.sh pour l’émission du certificat. Cela revient à exécuter la commande acme.sh --issue --standalone -d $FQDN

  • Création d’un Azure Container Instance avec l’image neilpang/acme.sh pour l’installation du certificat.

Le FQDN utilisable dans ce contexte est de la forme <dnsLabel>.<region>.azurecontainer.io, dnsLabel étant un paramètre que l’on peut définir.

Ci-dessous l’ARM template utilisé :

certificate.json
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "containerGroupName": {
            "type": "string",
            "metadata": {
                "description": "Container Group name."
            }
        },
        "dnsLabel": {
            "type": "string",
            "defaultValue": "",
            "metadata": {
                "description": "DNS label used to by the container group. The FQDN is <dnsLabel>.<region>.azurecontainer.io"
            }
        },
        "storageAccountName": {
            "type": "string",
            "metadata": {
                "description": "Name of the Storage Account"
            }
        },
        "storageAccountType": {
            "type": "string",
            "defaultValue": "Standard_LRS",
            "allowedValues": [
                "Standard_LRS",
                "Standard_GRS",
                "Standard_ZRS",
                "Premium_LRS"
            ],
            "metadata": {
                "description": "Storage Account type"
            }
        },
        "accessTier": {
            "type": "string",
            "defaultValue": "Hot",
            "allowedValues": [
                "Hot",
                "Cool"
            ],
            "metadata": {
                "description": "The access tier used for billing."
            }
        },
        "storageAccountKind": {
            "type": "string",
            "defaultValue": "StorageV2",
            "allowedValues": [
                "StorageV2",
                "Storage",
                "BlobStorage",
                "FileStorage",
                "BlockBlobStorage"
            ],
            "metadata": {
                "description": "Storage Account type"
            }
        },
        "advancedThreatProtectionEnabled": {
            "type": "bool",
            "defaultValue": false,
            "metadata": {
                "description": "Enable or disable Advanced Threat Protection."
            }
        },
        "shares": {
            "type": "array",
            "metadata": {
                "description": "List of the file share names."
            }
        },
        "location": {
            "type": "string",
            "defaultValue": "[resourceGroup().location]",
            "metadata": {
                "description": "The region to deploy the resources into"
            }
        },
        "tagValues": {
            "type": "object",
            "defaultValue": {
            }
        }
    },
    "variables": {
        "createFolderPrivateContainerGroupName": "[concat(parameters('containerGroupName'),'-cli-private')]",
        "createFolderCertsContainerGroupName": "[concat(parameters('containerGroupName'),'-cli-certs')]",
        "installContainerGroupName": "[concat(parameters('containerGroupName'),'-install')]",
        "dnsLabel": "[if(empty(parameters('dnsLabel')), parameters('containerGroupName'), parameters('dnsLabel'))]",
        "fqdn": "[toLower(concat(variables('dnsLabel'),'.',replace(parameters('location'), ' ', ''),'.azurecontainer.io'))]"
    },
    "resources": [
        {
            "type": "Microsoft.Storage/storageAccounts",
            "name": "[parameters('storageAccountName')]",
            "location": "[parameters('location')]",
            "apiVersion": "2018-07-01",
            "sku": {
                "name": "[parameters('storageAccountType')]"
            },
            "kind": "[parameters('storageAccountKind')]",
            "properties": {
                "accessTier": "[parameters('accessTier')]",
                "encryption": {
                    "keySource": "Microsoft.Storage",
                    "services": {
                        "blob": {
                            "enabled": true
                        },
                        "file": {
                            "enabled": true
                        }
                    }
                },
                "supportsHttpsTrafficOnly": true
            },
            "resources": [
                {
                    "condition": "[parameters('advancedThreatProtectionEnabled')]",
                    "type": "providers/advancedThreatProtectionSettings",
                    "name": "Microsoft.Security/current",
                    "apiVersion": "2017-08-01-preview",
                    "dependsOn": [
                        "[resourceId('Microsoft.Storage/storageAccounts/', parameters('storageAccountName'))]"
                    ],
                    "properties": {
                        "isEnabled": true
                    }
                }
            ]
        },
        {
            "type": "Microsoft.Storage/storageAccounts/fileServices/shares",
            "apiVersion": "2019-04-01",
            "name": "[concat(parameters('storageAccountName'), '/default/', parameters('shares')[copyIndex()])]",
            "copy": {
                "name": "sharecopy",
                "count": "[length(parameters('shares'))]"
            },
            "dependsOn": [
                "[parameters('storageAccountName')]"
            ]
        },
        {
            "name": "[variables('createFolderCertsContainerGroupName')]",
            "type": "Microsoft.ContainerInstance/containerGroups",
            "apiVersion": "2018-10-01",
            "location": "[parameters('location')]",
            "dependsOn": [
                "sharecopy"
            ],
            "properties": {
                "containers": [
                    {
                        "name": "create-folder-private",
                        "properties": {
                            "image": "microsoft/azure-cli",
                            "resources": {
                                "requests": {
                                    "cpu": 0.1,
                                    "memoryInGb": 0.1
                                }
                            },
                            "command": [
                                "az",
                                "storage",
                                "directory",
                                "create",
                                "--name",
                                "certs",
                                "--share-name",
                                "acme-cert"
                            ],
                            "environmentVariables": [
                                {
                                    "name": "AZURE_STORAGE_KEY",
                                    "value": "[listKeys(parameters('storageAccountName'),'2017-10-01').keys[0].value]"
                                },
                                {
                                    "name": "AZURE_STORAGE_ACCOUNT",
                                    "value": "[parameters('storageAccountName')]"
                                }
                            ]
                        }
                    }
                ],
                "osType": "Linux",
                "restartPolicy": "Never"

            }
        },
        {
            "name": "[variables('createFolderPrivateContainerGroupName')]",
            "type": "Microsoft.ContainerInstance/containerGroups",
            "apiVersion": "2018-10-01",
            "location": "[parameters('location')]",
            "dependsOn": [
                "sharecopy"
            ],
            "properties": {
                "containers": [
                    {
                        "name": "create-folder-private",
                        "properties": {
                            "image": "microsoft/azure-cli",
                            "resources": {
                                "requests": {
                                    "cpu": 0.1,
                                    "memoryInGb": 0.1
                                }
                            },
                            "command": [
                                "az",
                                "storage",
                                "directory",
                                "create",
                                "--name",
                                "private",
                                "--share-name",
                                "acme-cert"
                            ],
                            "environmentVariables": [
                                {
                                    "name": "AZURE_STORAGE_KEY",
                                    "value": "[listKeys(parameters('storageAccountName'),'2017-10-01').keys[0].value]"
                                },
                                {
                                    "name": "AZURE_STORAGE_ACCOUNT",
                                    "value": "[parameters('storageAccountName')]"
                                }
                            ]
                        }
                    }
                ],
                "osType": "Linux",
                "restartPolicy": "Never"
            }
        },
        {
            "name": "[parameters('containerGroupName')]",
            "type": "Microsoft.ContainerInstance/containerGroups",
            "apiVersion": "2018-10-01",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[variables('createFolderPrivateContainerGroupName')]",
                "[variables('createFolderCertsContainerGroupName')]"
            ],
            "properties": {
                "containers": [
                    {
                        "name": "acme-sh",
                        "properties": {
                            "image": "neilpang/acme.sh",
                            "command": [
                                "--issue",
                                "--standalone",
                                "-d",
                                "[variables('fqdn')]"
                            ],
                            "resources": {
                                "requests": {
                                    "cpu": 0.1,
                                    "memoryInGb": 0.1
                                }
                            },
                            "ports": [
                                {
                                    "port": 80
                                }
                            ],
                            "volumeMounts": [
                                {
                                    "name": "acme-config",
                                    "mountPath": "/acme.sh/"
                                }
                            ]
                        }
                    }
                ],
                "osType": "Linux",
                "restartPolicy": "Never",
                "ipAddress": {
                    "type": "Public",
                    "ports": [
                        {
                            "port": 80
                        }
                    ],
                    "dnsNameLabel": "[variables('dnsLabel')]"
                },
                "volumes": [
                    {
                        "name": "acme-config",
                        "azureFile": {
                            "shareName": "acme-config",
                            "storageAccountName": "[parameters('storageAccountName')]",
                            "storageAccountKey": "[listKeys(parameters('storageAccountName'),'2017-10-01').keys[0].value]"
                        }
                    }
                ]
            }
        },
        {
            "name": "[variables('installContainerGroupName')]",
            "type": "Microsoft.ContainerInstance/containerGroups",
            "apiVersion": "2018-10-01",
            "location": "[parameters('location')]",
            "dependsOn": [
                "[parameters('containerGroupName')]"
            ],
            "properties": {
                "containers": [
                    {
                        "name": "acme-sh",
                        "properties": {
                            "image": "neilpang/acme.sh",
                            "command": [
                                "--install-cert",
                                "--key-file",
                                "/etc/pki/tls/private/key.pem",
                                "--fullchain-file",
                                "/etc/pki/tls/certs/fullchain.pem",
                                "-d",
                                "[variables('fqdn')]"
                            ],
                            "resources": {
                                "requests": {
                                    "cpu": 0.1,
                                    "memoryInGb": 0.1
                                }
                            },
                            "volumeMounts": [
                                {
                                    "name": "acme-config",
                                    "mountPath": "/acme.sh/"
                                },
                                {
                                    "name": "acme-cert",
                                    "mountPath": "/etc/pki/tls/"
                                }
                            ]
                        }
                    }
                ],
                "osType": "Linux",
                "restartPolicy": "Never",
                "volumes": [
                    {
                        "name": "acme-cert",
                        "azureFile": {
                            "shareName": "acme-cert",
                            "storageAccountName": "[parameters('storageAccountName')]",
                            "storageAccountKey": "[listKeys(parameters('storageAccountName'),'2017-10-01').keys[0].value]"
                        }
                    },
                    {
                        "name": "acme-config",
                        "azureFile": {
                            "shareName": "acme-config",
                            "storageAccountName": "[parameters('storageAccountName')]",
                            "storageAccountKey": "[listKeys(parameters('storageAccountName'),'2017-10-01').keys[0].value]"
                        }
                    }
                ]
            }
        }

    ],
    "outputs": {
    }
}

Et les paramètres associés :

certificate.parameters.json
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "containerGroupName": {
            "value": "testacmenginx"
        },
        "storageAccountName": {
            "value": "stotestacmenginx"
        },
        "storageAccountType": {
            "value": "Standard_LRS"
        },
        "accessTier": {
            "value": "Hot"
        },
        "storageAccountKind": {
            "value": "StorageV2"
        },
        "advancedThreatProtectionEnabled": {
            "value": false
        },
        "shares": {
            "value": [
                "acme-config",
                "acme-cert"
            ]
        },
        "tagValues": {
            "value": {}
        },
        "location": {
            "value": "West Europe"
        }
    }
}
Au moment de la rédaction de cet article, Azure Container Instances n’est pas disponible en France. J’ai donc forcé l’utilisation d’Europe de l’Ouest.

Il suffit alors de déployer cet ARM template et ses paramètres. Je recommande l’utilisation de la cmdlet PowerShell New-AzResourceGroupDeployment.

Une fois le déploiement réalisé, il faut supprimer les ACI manuellement :

rg=XXX
cn=testacmenginx
az container delete -g $rg -y -n ${cn}
az container delete -g $rg -y -n ${cn}-cli-private
az container delete -g $rg -y -n ${cn}-cli-certs
az container delete -g $rg -y -n ${cn}-install

Etape 2 : utilisation des certificats par NGINX

Dans cette étape, nous allons déployer un NGINX avec SSL.

La configuration SSL a été renforcée afin de viser un A+ sur SSL Labs.

Voici ce que donne la configuration NGINX :

site.template
server {
    listen 80;
    server_name ${NGINX_HOST};
    server_tokens off;

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl;
    server_name ${NGINX_HOST};
    server_tokens off;

    ssl_certificate /etc/pki/tls/certs/fullchain.pem;
    ssl_certificate_key /etc/pki/tls/private/key.pem;

    ssl_session_cache shared:le_nginx_SSL:1m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    ssl_protocols TLSv1.3 TLSv1.2;
    ssl_prefer_server_ciphers on;
    #ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
    ssl_ciphers EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA512:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:ECDH+AESGCM:ECDH+AES256:DH+AESGCM:DH+AES256:RSA+AESGCM:!aNULL:!eNULL:!LOW:!RC4:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS;
    ssl_ecdh_curve secp384r1;

    ssl_stapling on;
    ssl_stapling_verify on;

    add_header Strict-Transport-Security "max-age=15768000; includeSubdomains; preload;" ;
    #add_header Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; script-src 'self'; img-src 'self'; style-src 'self'; base-uri 'self'; form-action 'self';";
    add_header Referrer-Policy "no-referrer, strict-origin-when-cross-origin";
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";

    location / {
        proxy_pass  http://localhost:3000;
        proxy_set_header    Host                $http_host;
        proxy_set_header    X-Real-IP           $remote_addr;
        proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
    }
}

Cette configuration sera passée comme un secret au container NGINX. Pour pouvoir le passer en secret, il faut alors l’encoder en base64. J’ai utilisé mon Windows Subsystem for Linux (WSL) mais il existe d’autres façons d’encoder en base64. Sous linux, la commande est :

base64 -w 0 mysite.template

J’ai introduit la variable ${NGINX_HOST} qui sera remplacée au démarrage. Pour ce faire, j’utilise la commande envsubst, que je n’ai pas inventée car donnée par la documentation de l’image Docker de NGINX.

2 petites choses à noter cependant :

  1. Par défaut envsubst va substituer toutes les variables de la forme $VAR ou ${VAR}. Du coup, dans ma configuration, $http_host était remplacée…​ par une chaîne vide. J’ai donc restreint la substitution à la seule variable ${NGINX_HOST}

  2. En Docker, il est possible de passer des commandes en Shell Form ou Exec Form. Je recommande cet article pour bien comprendre la différence. Azure Container Instances ne supporte que la forme Exec. Or, pour le démarrage de NGINX, j’avais besoin d’une séquence de programme pour réaliser la substitution et démarrer le démon. J’ai donc triché en utilisant la forme Exec et la commande sh -c :

"command": [
    "sh",
    "-c",
    "envsubst '$NGINX_HOST' < /tmp/nginx/mysite.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"
]

Comme à l’étape 1, le déploiement se fait avec un ARM template. Derrière le NGINX, j’ai mis un grafana dont le mot de passe admin est fixé par paramètre.

nginx.parameters.json
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "grafanaAdminPassword": {
            "type": "securestring",
            "metadata": {
                "description": "Password for Grafana admin."
            }
        },
        "containerGroupName": {
            "type": "string",
            "metadata": {
                "description": "Container Group name."
            }
        },
        "dnsLabel": {
            "type": "string",
            "defaultValue": "",
            "metadata": {
                "description": "DNS label used to by the container group. The FQDN is <dnsLabel>.<region>.azurecontainer.io"
            }
        },
        "storageAccountName": {
            "type": "string",
            "metadata": {
                "description": "Name of the Storage Account"
            }
        },
        "location": {
            "type": "string",
            "defaultValue": "[resourceGroup().location]",
            "metadata": {
                "description": "The region to deploy the resources into"
            }
        },
        "tagValues": {
            "type": "object",
            "defaultValue": {
            }
        }
    },
    "variables": {
        "dnsLabel": "[if(empty(parameters('dnsLabel')), parameters('containerGroupName'), parameters('dnsLabel'))]",
        "fqdn": "[toLower(concat(variables('dnsLabel'),'.',replace(parameters('location'), ' ', ''),'.azurecontainer.io'))]"
    },
    "resources": [
        {
            "name": "[parameters('containerGroupName')]",
            "type": "Microsoft.ContainerInstance/containerGroups",
            "apiVersion": "2018-10-01",
            "location": "[parameters('location')]",
            "properties": {
                "containers": [
                    {
                        "name": "nginx",
                        "properties": {
                            "image": "nginx:alpine",
                            "command": [
                                "sh",
                                "-c",
                                "envsubst '$NGINX_HOST' < /tmp/nginx/mysite.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"
                            ],
                            "environmentVariables": [
                                {
                                    "name": "NGINX_HOST",
                                    "value": "[variables('fqdn')]"
                                }
                            ],
                            "resources": {
                                "requests": {
                                    "cpu": 0.5,
                                    "memoryInGb": 0.5
                                }
                            },
                            "ports": [
                                {
                                    "port": 80
                                },
                                {
                                    "port": 443
                                }
                            ],
                            "volumeMounts": [
                                {
                                    "name": "acme-cert",
                                    "mountPath": "/etc/pki/tls/"
                                },
                                {
                                    "name": "nginx-config",
                                    "mountPath": "/tmp/nginx/"
                                }
                            ]
                        }
                    },
                    {
                        "name": "grafana",
                        "properties": {
                            "image": "grafana/grafana",

                            "ports": [
                                {
                                    "port": 3000
                                }
                            ],
                            "environmentVariables": [
                                {
                                    "name": "GF_SECURITY_ADMIN_PASSWORD",
                                    "value": "[parameters('grafanaAdminPassword')]"
                                },
                                {
                                    "name": "GF_SERVER_DOMAIN",
                                    "value": "[variables('fqdn')]"
                                }
                            ],
                            "resources": {
                                "requests": {
                                    "cpu": 1,
                                    "memoryInGb": 1
                                }
                            },
                            "volumeMounts": [
                                {
                                    "name": "acme-cert",
                                    "mountPath": "/etc/pki/tls/"
                                },
                                {
                                    "name": "nginx-config",
                                    "mountPath": "/etc/nginx/conf.d/"
                                }
                            ]
                        }
                    }
                ],
                "osType": "Linux",
                "restartPolicy": "Never",
                "ipAddress": {
                    "type": "Public",
                    "ports": [
                        {
                            "port": 80
                        },
                        {
                            "port": 443
                        }
                    ],
                    "dnsNameLabel": "[variables('dnsLabel')]"
                },
                "volumes": [
                    {
                        "name": "acme-cert",
                        "azureFile": {
                            "shareName": "acme-cert",
                            "storageAccountName": "[parameters('storageAccountName')]",
                            "storageAccountKey": "[listKeys(resourceId('Microsoft.Storage/storageAccounts',parameters('storageAccountName')),'2017-10-01').keys[0].value]"
                        }
                    },
                    {
                        "name": "nginx-config",
                        "secret": {
                            "mysite.template": "c2VydmVyIHsKICAgIGxpc3RlbiA4MDsKICAgIHNlcnZlcl9uYW1lICR7TkdJTlhfSE9TVH07CiAgICBzZXJ2ZXJfdG9rZW5zIG9mZjsKCiAgICBsb2NhdGlvbiAvIHsKICAgICAgICByZXR1cm4gMzAxIGh0dHBzOi8vJGhvc3QkcmVxdWVzdF91cmk7CiAgICB9Cn0KCnNlcnZlciB7CiAgICBsaXN0ZW4gNDQzIHNzbDsKICAgIHNlcnZlcl9uYW1lICR7TkdJTlhfSE9TVH07CiAgICBzZXJ2ZXJfdG9rZW5zIG9mZjsKCiAgICBzc2xfY2VydGlmaWNhdGUgL2V0Yy9wa2kvdGxzL2NlcnRzL2Z1bGxjaGFpbi5wZW07CiAgICBzc2xfY2VydGlmaWNhdGVfa2V5IC9ldGMvcGtpL3Rscy9wcml2YXRlL2tleS5wZW07CiAgICAgICAgCiAgICBzc2xfc2Vzc2lvbl9jYWNoZSBzaGFyZWQ6bGVfbmdpbnhfU1NMOjFtOwogICAgc3NsX3Nlc3Npb25fdGltZW91dCAxZDsKICAgIHNzbF9zZXNzaW9uX3RpY2tldHMgb2ZmOwoKICAgIHNzbF9wcm90b2NvbHMgVExTdjEuMyBUTFN2MS4yOwogICAgc3NsX3ByZWZlcl9zZXJ2ZXJfY2lwaGVycyBvbjsKICAgICNzc2xfY2lwaGVycyAiRUVDREgrQUVTR0NNOkVESCtBRVNHQ006QUVTMjU2K0VFQ0RIOkFFUzI1NitFREgiOwogICAgc3NsX2NpcGhlcnMgRUVDREgrRUNEU0ErQUVTR0NNOkVFQ0RIK2FSU0ErQUVTR0NNOkVFQ0RIK0VDRFNBK1NIQTUxMjpFRUNESCtFQ0RTQStTSEEzODQ6RUVDREgrRUNEU0ErU0hBMjU2OkVDREgrQUVTR0NNOkVDREgrQUVTMjU2OkRIK0FFU0dDTTpESCtBRVMyNTY6UlNBK0FFU0dDTTohYU5VTEw6IWVOVUxMOiFMT1c6IVJDNDohM0RFUzohTUQ1OiFFWFA6IVBTSzohU1JQOiFEU1M7CiAgICBzc2xfZWNkaF9jdXJ2ZSBzZWNwMzg0cjE7CgogICAgc3NsX3N0YXBsaW5nIG9uOwogICAgc3NsX3N0YXBsaW5nX3ZlcmlmeSBvbjsKCiAgICBhZGRfaGVhZGVyIFN0cmljdC1UcmFuc3BvcnQtU2VjdXJpdHkgIm1heC1hZ2U9MTU3NjgwMDA7IGluY2x1ZGVTdWJkb21haW5zOyBwcmVsb2FkOyIgOwogICAgI2FkZF9oZWFkZXIgQ29udGVudC1TZWN1cml0eS1Qb2xpY3kgImRlZmF1bHQtc3JjICdub25lJzsgZnJhbWUtYW5jZXN0b3JzICdub25lJzsgc2NyaXB0LXNyYyAnc2VsZic7IGltZy1zcmMgJ3NlbGYnOyBzdHlsZS1zcmMgJ3NlbGYnOyBiYXNlLXVyaSAnc2VsZic7IGZvcm0tYWN0aW9uICdzZWxmJzsiOwogICAgYWRkX2hlYWRlciBSZWZlcnJlci1Qb2xpY3kgIm5vLXJlZmVycmVyLCBzdHJpY3Qtb3JpZ2luLXdoZW4tY3Jvc3Mtb3JpZ2luIjsKICAgIGFkZF9oZWFkZXIgWC1GcmFtZS1PcHRpb25zIFNBTUVPUklHSU47CiAgICBhZGRfaGVhZGVyIFgtQ29udGVudC1UeXBlLU9wdGlvbnMgbm9zbmlmZjsKICAgIGFkZF9oZWFkZXIgWC1YU1MtUHJvdGVjdGlvbiAiMTsgbW9kZT1ibG9jayI7CgogICAgbG9jYXRpb24gLyB7CiAgICAgICAgcHJveHlfcGFzcyAgaHR0cDovL2xvY2FsaG9zdDozMDAwOwogICAgICAgIHByb3h5X3NldF9oZWFkZXIgICAgSG9zdCAgICAgICAgICAgICAgICAkaHR0cF9ob3N0OwogICAgICAgIHByb3h5X3NldF9oZWFkZXIgICAgWC1SZWFsLUlQICAgICAgICAgICAkcmVtb3RlX2FkZHI7CiAgICAgICAgcHJveHlfc2V0X2hlYWRlciAgICBYLUZvcndhcmRlZC1Gb3IgICAgICRwcm94eV9hZGRfeF9mb3J3YXJkZWRfZm9yOwogICAgfQp9"
                        }
                    }
                ]
            }
        }

    ],
    "outputs": {
    }
}
nginx.parameters.json
{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "grafanaAdminPassword": {
            "value": "secretPwd"
        },
        "containerGroupName": {
            "value": "testacmenginx"
        },
        "storageAccountName": {
            "value": "stotestacmenginx"
        },
        "tagValues": {
            "value": {}
        },
        "location": {
            "value": "West Europe"
        }
    }
}

Un nouveau, l’on peut déployer avec la cmdlet PowerShell New-AzResourceGroupDeployment.

Et voilà :

A+ avec SSL Labs

Conclusion

L’utilisation d’acme.sh m’a permis de récupérer un certificat Let’s Encrypt et l’utiliser dans NGINX qui agit comme reverse-proxy pour d’autres applications (grafana dans mon exemple).

Je n’ai pas abordé la problématique de renouvellement du certificat. Les certificats Let’s Encrypt n’ont une durée de vie que de 90 jours. Cela étant, Azure Container Instances n’est probablement pas fait pour des programmes "permanents", donc 90 jours est largement suffisant.

Il est possible de s’appuyer sur des fichiers YAML pour le déploiement d’Azure Container Instances. Cela s’avère très utile en phase de déploiement mais complétement inutile en phase d’industrialisation. En effet, il n’est pas possible d’utiliser des variables, des paramètres ou faire référence à d’autres ressources comme j’ai pu le faire dans l’ARM template.

Ainsi, dans l’ARM template, l’utilisation de l’image microsoft/azure-cli se fait avec une référence aux clés du compte de stockage:

{
    "name": "AZURE_STORAGE_KEY",
    "value": "[listKeys(parameters('storageAccountName'),'2017-10-01').keys[0].value]"
}

Ou même, la commande de génération de certificat avec l’image neilpang/acme.sh qui utilise une variable de mon ARM template:

"command": [
    "--issue",
    "--standalone",
    "-d",
    "[variables('fqdn')]"
]

La référence ARM est relativement bien documentée. Il ne faut donc pas hésiter à l’utiliser !