/ BLOGGING

Infrastructure as Code Patterns

I’ve been using a few different patterns for Infrastructure as Code (IaC) and wanted to document them.

Domains

I think about a automating deployments for infrastructure as code in domains. This helps me decide where to define parameters.

Domain Description
Human Computer Interaction (HCI) The UI that a person uses to change parameters for a deployment.
Automation Plane Tool used to take the UI parameters and execute a deployment.
Deployment Tool use to deploy resources to an environment.
Target The environment that the resources are deployed to.

The deploymnent domain usually has an orchestration configuration which uses resuable components to deploy resources. For example it could be a orchestration arm template that calls child templates.

Tools

Tool HCI Automation Plane Orchestration Modules Target
GitHub UI X        
GitHub Actions   X      
Azure DevOps   X      
Bamboo   X      
shell script   X X X  
shell script ./iac/orchestration.sh     X    
shell script ./script/common.sh       X  
Azure CLI         X
Arm Templates     X X  
Terraform     X X  
bicept     X X  

Example - GitHub Actions with Azure CLI

I use this as my starting point but I’m starting to look more at bicept.

Get Parameters from user in GitHub Actions step.

# Get params
target_env=${{github.event.inputs.environment}}
rg_region=${{github.event.inputs.location}}

echo create provision_connectivity in $location
./script/devops.sh provision_connectivity --location "$location"

The devops.sh is a single place to pass all the control through. It just forwards on the a file like ./iac/<specific>_orchestration.sh script

Update Parameters Step. The <specific>_orchestration.sh updates the default parameters with values from the user.

# default values
rg_name="rg_${name}_${location}"
kv_name="kv-common-$randomIdentifier"
log_name="log-common-$randomIdentifier"

# Parse arguments
echo "Parsing arguments"
while [[ "$#" -gt 0 ]]; do
  case "$1" in
    --parameters)
        shift
        # Assume it's key-value pairs passed directly
        while [ -n "$1" ] && [ "${1:0:1}" != "-" ]; do
            key="${1%%=*}"
            value="${1#*=}"
            local "$key=$value"
            shift
        done
        ;;
    *)
        echo "Unknown option: $1"
        exit 1
        ;;
  esac
done

# Create resources
# az ...

Example - GitHub Actions with Arm Templates

Here is an example of what is in my GitHub Actions workflow for one of the steps using powershell to update the arm template parameter file with values from the user.

Update Parameters Step

# Point to the right orchestration templates and parameter files
$templateFilePath = "./pipelines/artifacts/deploy.json"
$parameterFilePath = "./pipelines/artifacts/parameters.json"
$parameterUpdatedFilePath = "./pipelines/parameters.updated.json"

# Read the parameter file and sanitize it
$paramsRaw = Get-Content $parameterFilePath -Raw
$paramsSanitized = $paramsRaw -replace '(?m)(?<=^([^"]|"[^"]*")*)//.*' -replace '(?ms)/\*.*?\*/'
$json = ConvertFrom-Json $paramsSanitized -AsHashTable

# Replace with Pipeline parameters
$json.parameters.componentStorageAccountId.value = "${{secrets.COMPONENT_STORAGE_ACCOUNT_ID}}"
$json.parameters.componentsStorageContainerName.value = "${{env.componentsStorageContainerName}}"
$json.parameters.location.value = "${{ env.location }}"
$json.parameters.resourcegroupname.value = "${{ github.event.inputs.resourceGroupName }}"
$json.parameters.soCWvdPrincipalIds.value = "${{ env.soc_service_principal_ids }}"

# Save the updated parameter file
New-Item -Path $parameterUpdatedFilePath -Force
ConvertTo-Json $json -depth 10 | Out-File $parameterUpdatedFilePath

# Save Param file so next step can pick up
echo "Parameters updated path:" $parameterUpdatedFilePath
echo ::set-output name=parameterUpdatedFilePath::$parameterUpdatedFilePath

Validate Deployment

Write-Verbose 'Validate deployment' -Verbose

$ValidationErrors = $null
$deployment_name = "${{github.event.inputs.resourceGroupName}}-${{github.run_id}}-${{github.run_id}}-validate"
$templateFilePath = "${{ env.orchestrationPath }}/${{ env.rgFolder }}/deploy.json"
$parameterUpdatedFilePath = "${{ env.orchestrationPath }}/${{ env.rgFolder }}/Parameters/parameters.updated.json"

# Validate deployment
az deployment sub validate --name $deployment_name --location "${{ env.location }}" --template-file $templateFilePath --parameters $parameterUpdatedFilePath

if ($ValidationErrors) {
  Write-Error "Template is not valid."
}

Deployment Step

 Write-Verbose 'Handling subscription level deployment' -Verbose

$ValidationErrors = $null

$deployment_name = "${{github.event.inputs.resourceGroupName}}-${{github.run_id}}-${{github.run_id}}-validate"
$templateFilePath = "${{ env.orchestrationPath }}/${{ env.rgFolder }}/deploy.json"
$parameterUpdatedFilePath = "${{ env.orchestrationPath }}/${{ env.rgFolder }}/Parameters/parameters.updated.json"

# Create deployment
az deployment sub create --name $deployment_name --location "${{ env.location }}" --template-file $templateFilePath --parameters $parameterUpdatedFilePath

if ($ValidationErrors) {
  Write-Error "Template is not valid."
}

Example - Bash Script and bicept

I’m starting to look more at bicept. Here is an example of what is in my GitHub Actions workflow for one of the steps using powershell to update the arm template parameter file with values from the user.

Get Parameters from user

# Get params
target_env=${{github.event.inputs.environment}}
rg_region=${{github.event.inputs.location}}

echo create provision_connectivity in $location
./script/devops.sh provision_connectivity --location "$location"

Update Parameters Step

local env_file="${PROJ_ROOT_PATH}/.env"
local deployment_name="${app_name}.Provisioning-${run_date}"

# Pass additional parameters to overide the defaults defined in the parameters file
additional_parameters=("applicationName=$app_name")
if [ -n "$ENV_NAME" ]
then
    additional_parameters+=("environmentName=$ENV_NAME")
fi

if [ -n "$AZURE_LOCATION" ]
then
    additional_parameters+=("location=$AZURE_LOCATION")
fi

echo "Deploying ${deployment_name} in $location with ${additional_parameters[*]}"
az deployment sub create \
    --name "${deployment_name}" \
    --location "$location" \
    --template-file "${PROJ_ROOT_PATH}/infra/main.bicep" \
    --parameters "${PROJ_ROOT_PATH}/infra/main.parameters.json" \
    --parameters "${additional_parameters[@]}"

# Get the output variables from the deployment
output_variables=$(az deployment sub show -n "${deployment_name}" --query 'properties.outputs' --output json)
echo "Save deployment $deployment_name output variables to ${env_file}"
{
    echo ""
    echo "# Deployment output variables"
    echo "# Generated on ${ISO_DATE_UTC}"
    echo "$output_variables" | jq -r 'to_entries[] | "\(.key | ascii_upcase )=\(.value.value)"' 
}>> "$env_file"