Skip to content

Instantly share code, notes, and snippets.

@lawrencegripper
Last active March 17, 2021 16:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lawrencegripper/c06239f37ace287ce44e4bf36dd6ee2f to your computer and use it in GitHub Desktop.
Save lawrencegripper/c06239f37ace287ce44e4bf36dd6ee2f to your computer and use it in GitHub Desktop.
Azure Windows Container in VNET Powershell DSC
<#
.SYNOPSIS
Uses PowerShell DSC to configure the machine to run the container
.PARAMETER Image
Docker Image to run complete with tag
.PARAMETER Command
Command to run in the docker image
.PARAMETER RegistryUrl
Azure container registry url
.PARAMETER RegistryUsername
.PARAMETER RegistryPassword
.PARAMETER EnvironmentVariables
A base64 encoded string of a .env file to use when running the container
.PARAMETER InstanceName
The name to give the running docker container
.PARAMETER DockerConfigLocation
[Optional defaults to '"C:\ProgramData\Docker\config\daemon.json"'] The location on disk of the docker daemon.json config file
.PARAMETER DockerDataDir
[Optional defaults to 'D:\\'] Location on disk for docker to store volumes and images
#>
Configuration DockerImageStart {
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$Image,
[Parameter(Mandatory = $true)]
[String]
$Command,
[Parameter(Mandatory = $false)]
[String]
$ContainerName = "dscManagedContainerInstance",
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[String]
$RegistryUrl,
[Parameter(Mandatory = $true)]
[String]
$RegistryUsername,
[Parameter(Mandatory = $true)]
[string]
$RegistryPassword,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$EnvironmentVariables,
[Parameter(Mandatory = $false)]
[string]
$DockerConfigLocation = "C:\ProgramData\Docker\config\daemon.json",
[Parameter(Mandatory = $false)]
[string]
$DockerDataDir = "D:\\"
)
Import-DscResource -ModuleName 'PSDesiredStateConfiguration'
Node localhost
{
# Have the machine check every 15 mins that config is good.
# See details here: https://devblogs.microsoft.com/powershell/understanding-meta-configuration-in-windows-powershell-desired-state-configuration/
LocalConfigurationManager {
ConfigurationMode = "ApplyAndAutoCorrect"
RefreshFrequencyMins = 30
ConfigurationModeFrequencyMins = 15
RefreshMode = "PUSH"
RebootNodeIfNeeded = $true
}
# Docs: Each 'Script' resource ensures a configruation is setup correctly on the VM
# Get, test and set: https://docs.microsoft.com/en-us/powershell/scripting/dsc/resources/get-test-set?view=powershell-7.1
# Script module: https://docs.microsoft.com/en-us/powershell/scripting/dsc/reference/resources/windows/scriptResource?view=powershell-7.1
# Responsible for configuring the storage to use the ephemeral locally attached disk for docker storage
Script DockerStorageLocation {
SetScript = {
Set-Content -Path $using:DockerConfigLocation -Value "{ `"data-root`": `"$using:DockerDataDir`" }"
# Restart the Daemon so it picks up the new config
Restart-Service -Force Docker
}
TestScript = {
if (!(Test-Path $using:DockerConfigLocation)) {
return $false
}
$dataroot = (Get-Content $using:DockerConfigLocation | ConvertFrom-Json)."data-root"
if ($dataroot -ne $using:DockerDataDir) {
return $false
}
if ((Get-Service Docker).Status -ne "Running") {
return $false
}
return $true
}
GetScript = {
# Return the ID of the current container
@{ Result = (Get-Content $using:DockerConfigLocation | ConvertFrom-Json)."data-root" }
}
}
# Responsible for ensuring that docker is logged into the ACR
Script AzureContainerRepositoryLogin {
DependsOn = "[Script]DockerStorageLocation"
SetScript = {
# Handle running the paramaterised docker commands: https://stackoverflow.com/questions/6338015/how-do-you-execute-an-arbitrary-native-command-from-a-string
function Invoke-Login($command) {
Write-Verbose "Running command $command"
$output = $using:RegistryPassword | & 'docker' $command.Split(" ") 2>&1
if (!$? -and -not ($output -like "Login Succeeded"))
{
throw "Docker command failed, err: $output"
}
Write-Output $output
}
# Login to the ACR
try {
$output = Invoke-Login "login -u $using:RegistryUsername --password-stdin $using:RegistryUrl"
}
catch {
Write-Error "Failed running login command $_ $output"
}
}
TestScript = {
# Check we're logged into the ACR
if (!(Test-Path ~/.docker/config.json))
{
Write-Verbose "No docker config file found so can't be logged in"
return $false
}
$ConfigSettings = Get-Content ~/.docker/config.json | ConvertFrom-Json
$LoginDetailsBase64 = $ConfigSettings.auth."$using:RegistryUrl"
if (!$LoginDetailsBase64) {
Write-Verbose "Didn't find login details for the repo $using:RegistryUrl"
return $false
}
$LoginDetailsRaw = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($LoginDetailsBase64))
if ($LoginDetailsRaw -ne "$($using:RegistryUsername):$using:RegistryPassword") {
Write-Verbose "Current credentials don't match expected creds for $using:RegistryUrl"
return $false
}
return $true
}
GetScript = {
@{ Result = (Get-Content ~/.docker/config.json | ConvertFrom-Json).auth."$using:RegistryUrl" }
}
}
# Responsible for starting the container and keeping it running
Script ContainerInstance {
DependsOn = '[Script]AzureContainerRepositoryLogin', '[Script]DockerStorageLocation'
SetScript = {
# Handle running the paramaterised docker commands: https://stackoverflow.com/questions/6338015/how-do-you-execute-an-arbitrary-native-command-from-a-string
function Invoke-Executable($command) {
Write-Verbose "Running command $command"
$output = & 'docker' $command.Split(" ") 2>&1
if (!$?)
{
throw "Docker command failed, err: $output"
}
Write-Output $output
}
# Attempt to remove the container if it exists
try {
Write-Verbose "Attempting to remove existing container"
$output = Invoke-Executable "container rm -f $using:ContainerName"
}
catch {
Write-Warning "Failed to remove existing container Error: $_ $output"
# This is allowed to fail, for example the container might not be present
}
# Pull image
try {
$output = Invoke-Executable "pull $using:Image"
}
catch {
Write-Error "An error occurred pulling image: $_ stdOut: $output"
}
# Start the container
try {
$EnvFileContent = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($using:EnvironmentVariables))
$EnvFileLocation = "C:\$($using:ContainerName).env"
Set-Content -Path $EnvFileLocation -Value $EnvFileContent
$EnvFileHash = (Get-FileHash $EnvFileLocation).Hash
$output = Invoke-Executable "container run -d --restart=always --name=$using:ContainerName --env-file=$EnvFileLocation --label EnvFileHash=$EnvFileHash $using:Image $using:Command"
}
catch {
Write-Error "An error occurred starting the container: $_ stdOut: $output"
}
}
TestScript = {
# Track errors from external commands: https://stackoverflow.com/questions/12359427/try-catch-on-executable-exe-in-powershell
$ErrorActionPreference = 'Stop'
# Retrieve all running conatiners
$RunningContainers = iex 'docker ps --format "{{json . }}"' | ConvertFrom-Json | Where-Object { $_.Names -eq $using:ContainerName }
# Write a "-check" version of the env file provided
$EnvFileContent = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($using:EnvironmentVariables))
$EnvFileLocation = "C:\$($using:ContainerName)-check.env"
Set-Content -Path $EnvFileLocation -Value $EnvFileContent
# Get the files hash
$EnvFileHash = (Get-FileHash $EnvFileLocation).Hash
if (
($RunningContainers | Measure-Object).Count -eq 1 `
-and $RunningContainers[0].Image -eq $using:Image `
-and $RunningContainers[0].Status -like "up *" `
)
{
# Cool so it's running, is it running the right version of the environment variables?
# Compare the hash to the currently set 'envhash' label on the running instance
$DockerInspecResult = iex "docker inspect $($RunningContainers[0].ID)" | ConvertFrom-Json
Write-Verbose "Env hash $($DockerInspecResult.Config.Labels.EnvFileHash)"
if ($DockerInspecResult.Config.Labels.EnvFileHash -eq $EnvFileHash) {
Write-Verbose "Container is in running state with correct image and env file"
return $true
}
}
Write-Verbose "Container does not exist, is not in a running state or has the incorrect image or env file"
return $false
}
GetScript = {
# Return the ID of the current container
@{ Result = (iex 'docker ps --format "{{json . }}"' | ConvertFrom-Json | Where-Object { $_.Names -eq $using:ContainerName }).ID}
}
}
}
}
%{ for config_key, config_value in config ~}
${config_key}=${config_value}
%{ endfor ~}
$output = az vm run-command invoke --ids <machine_id_here> --command-id RunPowershellScript --scripts "docker inspect dscManagedContainerInstance" | ConvertFrom-Json
Write-Host $output.value[0].message
variable shared_env {
type = any
}
variable subnet_id {
type = string
}
variable name {
type = string
}
variable image {
type = string
}
variable command {
type = string
}
variable environment_variables {
description = "The environment variables to be set in the container"
default = {}
}
variable docker_registry_url {}
variable docker_registry_username {}
variable docker_registry_password {}
variable releases_storage_account_name {
type = string
}
variable releases_storage_account_key {
type = string
}
variable releases_container_name {
type = string
}
variable releases_storage_sas {
type = string
}
locals {
env_file = base64encode(templatefile(
"${path.module}/env.tpl",
{
config = var.environment_variables
}
))
script_name = "dsc_config.ps1"
script_zip = "dsc_config.zip"
script_hash = filemd5("${path.module}/dsc_config.ps1")
}
resource "random_string" "random" {
length = 5
special = false
upper = false
number = false
}
resource "random_string" "adminpw" {
length = 18
special = true
upper = true
number = true
}
resource "azurerm_network_interface" "nic" {
name = "${var.name}${random_string.random.result}"
resource_group_name = var.shared_env.rg.name
location = var.shared_env.rg.location
ip_configuration {
name = "internal"
subnet_id = var.subnet_id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_windows_virtual_machine" "vm" {
name = "${var.name}${random_string.random.result}-vm"
resource_group_name = var.shared_env.rg.name
location = var.shared_env.rg.location
computer_name = "relimporter"
// 8 cores, 16GB ram and 128GB temp disk for import data to live on
size = "Standard_F8"
admin_username = "adminuser"
admin_password = random_string.adminpw.result
network_interface_ids = [
azurerm_network_interface.nic.id,
]
os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2019-Datacenter-Core-with-Containers"
version = "latest"
}
patch_mode = "AutomaticByOS"
}
data "archive_file" "script_zip" {
type = "zip"
source_file = "${path.module}/${local.script_name}"
output_path = "${path.module}/${local.script_zip}"
}
resource "azurerm_storage_blob" "dscps1" {
name = "dsc${local.script_hash}.zip"
storage_account_name = var.releases_storage_account_name
storage_container_name = var.releases_container_name
type = "Block"
source = "${path.module}/${local.script_zip}"
depends_on = [data.archive_file.script_zip]
}
// Using this https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/dsc-windows
// to run a DSC configuration with will make sure the VM is running the container and
// periodically check for any issue and correct them.
// See: https://docs.microsoft.com/en-us/powershell/scripting/dsc/overview/overview?view=powershell-7.1
resource "azurerm_virtual_machine_extension" "dscconfig" {
name = "dscconfig${local.script_hash}"
virtual_machine_id = azurerm_windows_virtual_machine.vm.id
publisher = "Microsoft.Powershell"
type = "DSC"
type_handler_version = "2.77"
auto_upgrade_minor_version = true
depends_on = [azurerm_storage_blob.dscps1]
settings = jsonencode(jsondecode(<<SETTINGS
{
"configuration": {
"url": "https://${var.releases_storage_account_name}.blob.core.windows.net/${var.releases_container_name}/dsc${local.script_hash}.zip",
"script": "dsc_config.ps1",
"function": "DockerImageStart"
}
}
SETTINGS
))
protected_settings = jsonencode(jsondecode(<<JSON
{
"configurationArguments": {
"Image": "${var.image}",
"Command": "${var.command}",
"RegistryUrl": "${var.docker_registry_url}",
"RegistryUsername": "${var.docker_registry_username}",
"RegistryPassword": "${var.docker_registry_password}",
"EnvironmentVariables": "${local.env_file}"
},
"configurationUrlSasToken": "${var.releases_storage_sas}"
}
JSON
))
}
# An example of using the above module
module "container_vm" {
source = "./docker_vm"
shared_env = local.shared_env
subnet_id = var.subnet_id
docker_registry_username = var.docker_registry_username
docker_registry_password = var.docker_registry_password
docker_registry_url = var.docker_registry_url
releases_storage_account_name = module.core.releases_storage_account_name
releases_storage_account_key = module.core.releases_storage_account_key
releases_storage_sas = module.core.releases_account_sas
releases_container_name = module.core.releases_container_name
name = "consolecontainer"
image = "myregistry.azurecr.io/thingy:imagetag"
command = "myConsoleApp.exe"
environment_variables = {
COSMOS_KEY = module.core.cosmos_account_key,
COSMOS_ENDPOINT = module.core.cosmos_account_endpoint,
COSMOS_DB_NAME = module.core.cosmos_db_name,
COSMOS_CONTAINER_NAME = module.core.cosmos_container_name,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment