Author : MD TAREQ HASSAN | Updated : 2021/10/18
Stack Name When Using Blob Container as Backend
- The file state backend (local disk, S3, Azure blob store, etc.) only has a single namespace for all stacks - just the stack name
- That’s why you get that error that
xxx
already exists - See: Troubleshooting - error: stack names may not contain slashes
Required Credentials and Environment Variables
- For Pulumi Backend
- Blob container as stack state backend (storing and retrieving state files)
- Pulumi will use storage account key or SAS token
- Set follwoing environment variables
AZURE_STORAGE_ACCOUNT
(required)AZURE_STORAGE_KEY
(orAZURE_STORAGE_SAS_TOKEN
) (required)AZURE_STORAGE_CONTAINER
(Not required but just storing complete information about account & container)
- Azure KeyVault key for Pulumi secrets (encrypt/decrypt)
- By default Pulumi (Go SDK) uses “Environment-based Authentication”
- Follwoing environment variables will be set so that Pulumi can get service principal credential and use it to access Azure KeyVault
AZURE_TENANT_ID
AZURE_CLIENT_ID
AZURE_CLIENT_SECRET
- Use RBAC for vault access policy and assign role “Key Vault Crypto User” to service principal so that Pulumi can read key from KeyVault
- Assign role “Key Vault Administrator” to yourself, otherwise you would not be able to create key (RSA, 2048) in KeyVault
- Blob container as stack state backend (storing and retrieving state files)
- Pulumi Deployment to target Azure Subscription (ARM API)
- Pulumi can authenticate to Azure using a Service Principal or the Azure CLI
- Use Azure CLI (for local)
- Simply login to the Azure CLI and Pulumi will automatically use your credentials
az login --use-device-code
- Use Service Principal (DevOps pipeline)
- Set following environment variables
ARM_CLIENT_ID
ARM_CLIENT_SECRET
ARM_TENANT_ID
ARM_SUBSCRIPTION_ID
- Alternatively use
pulumi config set ...
(details: https://www.pulumi.com/registry/packages/azure-native/installation-configuration/#option-2-use-a-service-principal)
- Set following environment variables
Create Service Principal
- Create service principal (Azure portal or PowerShell)
- Save service principal information (credential)
Create new App Registration in Azure AD
Copy ObjectId & ApplicationId
Copy TenantId
Add Client Secret
Gathered information (service_principal_credential.json
)
{
"AZURE_CLIENT_SECRET": "xxx",
"AZURE_TENANT_ID": "xxx",
"AZURE_CLIENT_ID": "xxx",
"AZURE_SERVICE_PRINCIPAL_OBJECT_ID": "xxx",
"AZURE_SERVICE_PRINCIPAL_DISPLAY_NAME": "pulumi-demo-sp"
}
Create Resource Group and Blob Container
- Create resource group “pulumi-backend-rg” : https://portal.azure.com/#create/Microsoft.ResourceGroup
- Create Storage Account “pulumibackendsa” : https://portal.azure.com/#create/Microsoft.StorageAccount-ARM
- Create blob container “pulumi-backend-container”
- Copy Stoarge Account Key (Security warning: account key is used in this article, you should use SAS token)
Copy Storage Account Key
Gathered information (storage_container_credential.json
)
{
"AZURE_STORAGE_ACCOUNT": "pulumibackendsa",
"AZURE_STORAGE_KEY": "xxx==",
"AZURE_STORAGE_CONTAINER": "pulumi-backend-container"
}
Create KeyVault and Key
- Create KeyVault “pulumi-backend-kv” :
- https://portal.azure.com/#create/Microsoft.KeyVault
- Access policies (Permission model) : Azure role-based access control (RBAC)
- Create Key (RSA, 2048)
- Warning: if KayVault or Key is in ‘deleted but recoverable’ state, you have to purge it first, otherwise would not be able to create (KeyVault or Key) with same name
KayVault (RSA) key for Pulumi secrets
Assign Role to Service Principal
Pulumi will use service principal to deploy stack to the target subscription. Assign “Contributor” role to the service principal at subscription scope.
Assign yourself “Key Vault Administrator” role (otherwise you would not be able to create key) and assign service principal “Key Vault Crypto User” (so that pulumi can read key).
Environment Variables
Pulumi Credential (from service principal credential) (pulumi_credential.json
)
{
"ARM_SUBSCRIPTION_ID": "xxx",
"ARM_TENANT_ID": "xxx",
"ARM_CLIENT_ID": "xxx",
"ARM_CLIENT_SECRET": "xxx",
}
Now, set required environment variables
Create Pulumi Project and Deploy to Target Subscription
#
# Set variables for pulumi command
#
$blobContainerName = 'pulumi-backend-container'
$resourceGroupLocation = 'Japan East'
$kvName = 'pulumi-backend-kv'
$kvKeyName = 'pulumi-secret-key'
$solutionName = 'demo-iac-pulumi' # VS solution
$pulumiProjectName = 'demo-azure-infra' # Pulumi project -> VS project (will be added to VS solution)
$pulumiProjectDescription = 'A demo pulumi IaC project'
$pulumiStackName = 'dev'
#
# Create solution directory and VS solution
#
if (Test-Path .\$solutionName) {
# Clean up folder
Remove-Item .\$solutionName\* -Force -Recurse | Out-Null
} else {
# Create folder
New-Item -Path .\$solutionName -ItemType Directory | Out-Null
}
Set-Location $solutionName
dotnet new sln --name $solutionName
#
# Pulumi login to Azure (blob container as stack state backend)
#
pulumi login azblob://$blobContainerName
#
# Pulumi new project (Azure KeyVault key for pulumi secret)
#
pulumi new azure-csharp `
--stack="$pulumiStackName" `
--secrets-provider="azurekeyvault://$kvName.vault.azure.net/keys/$kvKeyName" `
--name="$pulumiProjectName" `
--description="$pulumiProjectDescription" `
--dir="$pulumiProjectName" `
--config="azure-native:location=$resourceGroupLocation" `
--force
#
# Add pulumi project to VS solution
#
dotnet sln add ./$pulumiProjectName/$pulumiProjectName.csproj
#
# Deploy pulumi stack to Azure (target subscription)
#
# Prevent Pulumi from using Azure CLI token -> logout from all accounts
az account clear
az logout
Set-Location $pulumiProjectName
pulumi up #--yes
Automating the Whole Process with PowerShell Script
Service principal information (credential)
service_principal_credential.json
- If service principal is created in advance, then the script will expect you to provide service principal information in this file
- If service principal does not exist, the script will create service principal and store credential to this file
- Make sure that you have enough privilege to create service principal
service_principal_credential.json
(this file is expected in current folder if service principal is created in advance)
{
"AZURE_CLIENT_SECRET": "xxx",
"AZURE_TENANT_ID": "xxx",
"AZURE_CLIENT_ID": "xxx",
"AZURE_SERVICE_PRINCIPAL_OBJECT_ID": "xxx",
"AZURE_SERVICE_PRINCIPAL_DISPLAY_NAME": "pulumi-demo-sp"
}
Storage Account and blob container information (credential)
- The script will create storage account and blob container if does not exist
storage_container_credential.json
- If this file is present in the current folder, the script will read infromation from it and verify information is correct
- If the file is not present, the script will create it and save storage account information in it
storage_container_credential.json
(this file is expected in current folder if storage account and container are created in advance)
{
"AZURE_STORAGE_ACCOUNT": "pulumibackendsa",
"AZURE_STORAGE_KEY": "xxx==",
"AZURE_STORAGE_CONTAINER": "pulumi-backend-container"
}
KeyVault and Key
- KeyVault and Key will be created if do not exist
- Key will be used for Pulumi secrets
- Required roles will be assigned to KeyVault
Environment Variables
- Service principal credential will be saved to environment variables
- Storage account credential will be save to environment variables
- Required environments variables will be set so that Pulumi can deploy to the target subscription
Pulumi commands generation
- The script will generate “
pulumi_login_and_new_project_cmd.ps1
” - Run “
pulumi_login_and_new_project_cmd.ps1
”, it will create pulumi ‘azure-csharp’ project with:- Azure blob container to save stack state file
- Azure KeyVault key for Pulumi secrets
- Make sure that you restart PowerShell/VS Code before executing “
pulumi_login_and_new_project_cmd.ps1
“
utility-functions.ps1
#
# ----------------------------------------------------------------------------------------------------
# Functions - Login / Connect to Azure
# ----------------------------------------------------------------------------------------------------
#
function Connect-AzAccountTargetSubscription {
param (
[Parameter(Mandatory)]
[string] $SubscriptionName
)
# In Azure CloudShell, by default managed identity is used to connect to Azure Account (account will be some thing like "MSxxx")
#
$loggedInAccount = (Get-AzContext).Account
if (!( "$loggedInAccount".Contains("@") )) {
Write-Host "Login is required `r`n"
Connect-AzAccount -UseDeviceAuthentication
Set-AzContext -Subscription (Get-AzSubscription -SubscriptionName $SubscriptionName).Id
Start-Sleep -Seconds 2
$currentSubscriptionName = (Get-AzContext).Subscription.Name
if ($currentSubscriptionName -ne $SubscriptionName) {
throw "Context (subscription) mis-match"
} else {
Write-Host "Subscription '$SubscriptionName' has been set as context"
}
}
else {
Write-Host "User is already logged in `r`n"
}
}
#
# ----------------------------------------------------------------------------------------------------
# Functions - Service Principal
# ----------------------------------------------------------------------------------------------------
#
function ConvertTo-String {
param(
[Parameter(Mandatory)]
[Security.SecureString] $SecureString
)
try {
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
[Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
}
finally {
if ( $bstr -ne [IntPtr]::Zero ) {
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
}
}
}
function New-ServicePrincipal {
param (
[Parameter(Mandatory)]
[string] $ServicePrincipalDisplayName
)
$spCred = @{}
try {
$newSp = (New-AzADServicePrincipal -DisplayName $ServicePrincipalDisplayName)
Start-Sleep -Seconds 3 # cool down
$spSecretAsStringObj = ConvertTo-String -SecureString $newSp.Secret
$spCred['AZURE_SERVICE_PRINCIPAL_DISPLAY_NAME'] = $ServicePrincipalDisplayName
$spCred['AZURE_SERVICE_PRINCIPAL_OBJECT_ID'] = $newSp.Id
$spCred['AZURE_TENANT_ID'] = (Get-AzTenant).Id
$spCred['AZURE_CLIENT_ID'] = $newSp.ApplicationId
$spCred['AZURE_CLIENT_SECRET'] = $spSecretAsStringObj
<# $envVarKeySpDisplayName = "AZURE_SERVICE_PRINCIPAL_DISPLAY_NAME"
$envVarKeySpObjectId = "AZURE_SERVICE_PRINCIPAL_OBJECT_ID"
$envVarKeyTenantId = "AZURE_TENANT_ID"
$envVarKeyClientId = "AZURE_CLIENT_ID"
$envVarKeyClientSecret = "AZURE_CLIENT_SECRET" #>
}
catch {
Write-Warning $Error[0]
}
return $spCred
}
#
# ----------------------------------------------------------------------------------------------------
# Functions - File Operations
# ----------------------------------------------------------------------------------------------------
#
function Read-JsonFileAsHashTable {
param (
[Parameter(Mandatory)]
[string] $JsonFileUriWithExtension
)
$ht = @{}
try {
$ht = Get-Content -Raw -Path $JsonFileUriWithExtension | ConvertFrom-Json -AsHashtable
}
catch {
Write-Warning $Error[0]
}
return $ht
}
function WriteTo-File {
param (
[Parameter(Mandatory)]
[string] $Content,
[Parameter(Mandatory)]
[string] $FileUriWithExtension
)
try {
Write-Output $Content > $FileUriWithExtension
}
catch {
Write-Warning $Error[0]
}
}
#
# ----------------------------------------------------------------------------------------------------
# Functions - Storage Account & Blob Container
# ----------------------------------------------------------------------------------------------------
#
function Get-StorageAccountKey {
param (
[Parameter(Mandatory)]
[string] $ResourceGroupName,
[Parameter(Mandatory)]
[string] $StorageAccountName
)
$saKeys = Get-AzStorageAccountKey -ResourceGroupName $ResourceGroupName -Name $StorageAccountName
$key1 = $saKeys[0].Value
return $key1
}
#
# ----------------------------------------------------------------------------------------------------
# Functions - User
# ----------------------------------------------------------------------------------------------------
#
function Get-LoggedInUserObjectId {
(Get-AzADUser -UserPrincipalName (Get-AzContext).Account).Id
}
#
# ----------------------------------------------------------------------------------------------------
# Functions - Role & Role Assignment
# ----------------------------------------------------------------------------------------------------
#
function New-RoleAssignement {
param (
[Parameter(Mandatory)]
[string] $RoleDefinitionName,
[Parameter(Mandatory)]
[string] $ObjectId,
[Parameter(Mandatory)]
[string] $Scope
)
#
# Before trying to assign role,
# Check that is given role is already assigned or not
#
$existingRoleAssignment = (Get-AzRoleAssignment -RoleDefinitionName $RoleDefinitionName -ObjectId $ObjectId -Scope $Scope)
if ($null -eq $existingRoleAssignment) {
Write-Host "[Role '$($RoleDefinitionName)' at Scope '$($Scope)'] does not exist, creating new role assignment... `r`n"
$raSplat = @{
RoleDefinitionName = $RoleDefinitionName
ObjectId = $ObjectId
Scope = $Scope
}
New-AzRoleAssignment @raSplat | Out-Null
Write-Host "Role '$($RoleDefinitionName)' has been assigned at scope '$($Scope)' `r`n"
}
else {
Write-Host "[Role '$($RoleDefinitionName)' at Scope '$($Scope)'] already exists `r`n"
}
}
prepare_pulumi_backend.ps1
#
# ----------------------------------------------------------------------------------------------------
# Required variables
# ----------------------------------------------------------------------------------------------------
#
#
# Subscription
#
$subscriptionName = 'Pay-As-You-Go Dev/Test'
$subscriptionId = "" # will be set later
$subscriptionRoleServicePrincipal = 'Contributor' # the role that service principal should have at subscription scope
#
# Service Principal
#
$spDisplayName = 'pulumi-demo-sp'
$spCredJsonFile = 'service_principal_credential.json'
$envVarKeySpObjectId = 'AZURE_SERVICE_PRINCIPAL_OBJECT_ID'
$envVarKeySpTenantId = 'AZURE_TENANT_ID'
$envVarKeySpClientId = 'AZURE_CLIENT_ID'
$envVarKeySpClientSecret = 'AZURE_CLIENT_SECRET'
#
# Resource Group
#
$rgName = 'pulumi-backend-rg'
$rgLocation = 'Japan East'
#
# Storage Account & Blob Container
#
$saName = 'pulumibackendsa'
$saSku = 'Standard_ZRS'
$saKind = 'StorageV2'
$blobContainerName = 'pulumi-backend-container' # Pulumi will use this to save state file
$blobContainerCredJsonFile = 'storage_container_credential.json'
$envVarKeyStorageAccountName = 'AZURE_STORAGE_ACCOUNT'
$envVarKeyBlobContainerName = 'AZURE_STORAGE_CONTAINER'
$envVarKeyStorageAccountKey = 'AZURE_STORAGE_KEY' #"AZURE_STORAGE_SAS_TOKEN"
#
# KeyVault & Key
#
$kvName = 'pulumi-backend-kv'
$kvKeyName = 'pulumi-secret-key' # Pulumi will use this key to encrypt secrets
$kvRoleLoggedInUser = 'Key Vault Administrator' # so that current user can create key in the KeyVault
$kvRoleServicePrincipal = 'Key Vault Crypto User' # so that service principal can access key (RBAC -> vault access policy)
#
# Credential for Pulumi to deploy to the target subscription
#
$envVarKeyArmSubscriptionId = 'ARM_SUBSCRIPTION_ID'
$envVarKeyArmTetantId = 'ARM_TENANT_ID'
$envVarKeyArmClientId = 'ARM_CLIENT_ID'
$envVarKeyArmClientSecret = 'ARM_CLIENT_SECRET'
#
# To generate Pulumi commands
#
$solutionName = 'demo-iac-pulumi' # VS solution
$pulumiProjectName = 'demo-azure-infra' # Pulumi project & VS project
$pulumiProjectDescription = "A demo pulumi IaC project"
$pulumiStackName = 'dev'
$pulumiCmdPowerShellFile = "pulumi_login_and_new_project_cmd.ps1"
#
# ----------------------------------------------------------------------------------------------------
# Import functions (dot sourcing)
# ----------------------------------------------------------------------------------------------------
#
. .\utility-functions.ps1
#
# ----------------------------------------------------------------------------------------------------
# Login/Connect to Azure
# ----------------------------------------------------------------------------------------------------
#
try {
Connect-AzAccountTargetSubscription -SubscriptionName $subscriptionName
}
catch {
Write-Error $Error[0]
return
}
#
# ----------------------------------------------------------------------------------------------------
# Service Principal
# ----------------------------------------------------------------------------------------------------
#
$sp = $null
try {
$sp = Get-AzADServicePrincipal -DisplayName $spDisplayName -ErrorAction Stop
}
catch {
Write-Warning $Error[0]
}
$spCredential = @{}
if ($null -eq $sp) {
Write-Host "Service principal '$($spDisplayName)' does not exist. Creating new... `r`n"
$spCredential = New-ServicePrincipal -ServicePrincipalDisplayName $spDisplayName
#Start-Sleep -Seconds 2
#
# Since service principal secret is only available at creation time, we need to save to file
#
Write-Host "Saving service principal information to '$($spCredJsonFile)' `r`n"
$spFileSplat = @{
Content = $($spCredential | ConvertTo-Json)
FileUriWithExtension = $spCredJsonFile
}
WriteTo-File @spFileSplat
Write-Host "Service principal information has been saved `r`n"
}
else {
Write-Warning "Service principal '$($spDisplayName)' already exists, therefore '$($spCredJsonFile)' must be present in the current folder`r`n"
if (Test-Path $spCredJsonFile) {
Write-Host "Fetching service principal information from '$($spCredJsonFile)' `r`n"
$spCredential = Read-JsonFileAsHashTable -JsonFileUriWithExtension $spCredJsonFile
}
else {
Write-Error "Service principal '$($spDisplayName)' exists but '$($spCredJsonFile)' is not present in the current folder (you must provide service principal information in this file) `r`n"
$spCredential = @{}
}
}
if ($spCredential.Count -lt 5) {
# displayName, objectId, tenantId, clientId, clientSecret
Write-Error "One or more service principal information is missing, terminating script execution `r`n"
return
}
#
# Assign role to service principal at subscription scope
#
$subscriptionId = (Get-AzSubscription -SubscriptionName $subscriptionName).Id
$subscriptionResourceId = "/subscriptions/$subscriptionId"
$spObjectId = $spCredential[$envVarKeySpObjectId]
$subscriptionRaSpSplat = @{
RoleDefinitionName = $subscriptionRoleServicePrincipal
ObjectId = $spObjectId
Scope = $subscriptionResourceId
}
New-RoleAssignement @subscriptionRaSpSplat # if service principal is created by this script, by default 'Contributor' role will already be assigned at subscription scope
#
# ----------------------------------------------------------------------------------------------------
# Pulumi credential (environment variables) -> for Pulumi deployment to target subscription using Service principal
# ----------------------------------------------------------------------------------------------------
#
$pulumiCredential = @{}
$pulumiCredential[$envVarKeyArmSubscriptionId] = $subscriptionId
$pulumiCredential[$envVarKeyArmTetantId] = $spCredential[$envVarKeySpTenantId]
$pulumiCredential[$envVarKeyArmClientId] = $spCredential[$envVarKeySpClientId]
$pulumiCredential[$envVarKeyArmClientSecret] = $spCredential[$envVarKeySpClientSecret]
#
# ----------------------------------------------------------------------------------------------------
# Resource group
# ----------------------------------------------------------------------------------------------------
#
$rgSplat = @{
Name = $rgName
Location = $rgLocation
}
$rg = $null
try {
$rg = Get-AzResourceGroup @rgSplat -ErrorAction Stop
}
catch {
Write-Warning $Error[0]
}
if ($null -eq $rg) {
Write-Host "Resource group '$($rgName)' does not exist, creating new... `r`n"
$rg = (New-AzResourceGroup @rgSplat)
Start-Sleep -Seconds 3
}
else {
Write-Host "Resource group '$($rgName)' already exists `r`n"
}
#
# Make sure resource group is ready
# (there could be some delay due to some problems in Azure)
#
$rg = (Get-AzResourceGroup @rgSplat)
while ($null -eq $rg) {
Write-Host "Resource group not found, trying to re-fetch..."
Start-Sleep -Secomds 2
$rg = (Get-AzResourceGroup @rgSplat)
}
#$rgResourceId = $rg.ResourceId
#
# ----------------------------------------------------------------------------------------------------
# Storage account & blob container
# ----------------------------------------------------------------------------------------------------
#
#
# Storage account
#
$getSaSplat = @{
Name = $saName
ResourceGroupName = $rgName
}
$sa = $null
try {
$sa = Get-AzStorageAccount @getSaSplat -ErrorAction Stop
}
catch {
Write-Warning $Error[0]
}
if ($null -eq $sa) {
Write-Host "Stoarge Account '$($saName)' does not exist, creating new...`r`n"
Write-Host "`r`n-------------------------------------------------------------------------------------------`r`n"
$newSaSplat = $getSaSplat + @{
Location = $rgLocation
SkuName = $saSku
Kind = $saKind
}
New-AzStorageAccount @newSaSplat -AsJob | Out-Null
Start-Sleep -Seconds 2
#Get-Job
$provisioningState = (Get-AzStorageAccount @getSaSplat).ProvisioningState
while ('Succeeded' -ne $provisioningState) {
Write-Host "[$(Get-Date -DisplayHint Time)] Storage account is not created yet -> ProvisioningState: $provisioningState `r`n"
Start-Sleep -Seconds 3
$provisioningState = (Get-AzStorageAccount @getSaSplat).ProvisioningState
}
Write-Host "`r`n-------------------------------------------------------------------------------------------`r`n"
Write-Host "Stoarge Account '$($saName)' has been created `r`n"
}
else {
Write-Host "Stoarge Account '$($saName)' already exists `r`n"
}
#$saResourceId = $sa.ResourceId
#
# Storage account key (for Pulumi to access blob container)
#
$saKeySplat = @{
ResourceGroupName = $rgName
StorageAccountName = $saName
}
$saKey = Get-StorageAccountKey @saKeySplat
#Write-Host $saKey
#
# Storage account context
#
# To create blob container, storage account context is needed
# There are multiple ways to create storage account context
# Account key will be used in this case
#
$saContextSplat = @{
StorageAccountName = $saName
StorageAccountKey = $saKey
}
$saContext = New-AzStorageContext @saContextSplat
#
# Blob container
#
$blobContainer = $null
Try {
$blobContainer = Get-AzStorageContainer -Name $blobContainerName -Context $saContext -ErrorAction Stop
}
catch {
Write-Warning $Error[0]
}
if ($null -eq $blobContainer) {
Write-Host "Container '$($blobContainerName)' does not exist, creating new... `r`n"
# Create blob container
#
$blobContainerSplat = @{
Name = $blobContainerName
Context = $saContext
Permission = 'Off'
}
$blobContainer = New-AzStorageContainer @blobContainerSplat
}
else {
Write-Host "Container '$($blobContainerName)' already exists `r`n"
}
#$cbc = $blobContainer.CloudBlobContainer
#Write-Host $cbc.Uri
#
# Storage container credential (for Pulumi to access blob container)
#
$blobContainerCredential = @{}
if (Test-Path $blobContainerCredJsonFile) {
Write-Host "'$($blobContainerCredJsonFile)' is present in current folder, storage container information will be fetched from it `r`n"
$blobContainerCredential = Read-JsonFileAsHashTable -JsonFileUriWithExtension $blobContainerCredJsonFile
$saNameFromFile = $blobContainerCredential[$envVarKeyStorageAccountName]
$bcNameFromFile = $blobContainerCredential[$envVarKeyBlobContainerName]
$saKeyFromFile = $blobContainerCredential[$envVarKeyStorageAccountKey]
if ( ($saName -ne $saNameFromFile) -or ($blobContainerName -ne $bcNameFromFile) -or ($saKeyFromFile -ne $saKey) ) {
Write-Error "Storage account information mismatch"
$blobContainerCredential = @{}
}
}
else {
Write-Host "'$($blobContainerCredJsonFile)' is not present in the script folder. Creating container credential `r`n"
$blobContainerCredential[$envVarKeyStorageAccountKey] = $saKey
$blobContainerCredential[$envVarKeyStorageAccountName] = $saName
$blobContainerCredential[$envVarKeyBlobContainerName] = $blobContainerName
# Save container information to file
#
$bccSplat = @{
Content = ($blobContainerCredential | ConvertTo-Json)
FileUriWithExtension = $blobContainerCredJsonFile
}
WriteTo-File @bccSplat
Write-Host "Storage container credential has been saved to '$($blobContainerCredJsonFile)' `r`n"
}
if ($blobContainerCredential.Count -lt 3) {
Write-Error "One or more storage container information is missing, terminating script execution"
return
}
#
# ----------------------------------------------------------------------------------------------------
# KeyVault and Key
# ----------------------------------------------------------------------------------------------------
#
#
# Create KeyVault if does not exist
#
$kvSplat = @{
VaultName = $kvName
ResourceGroupName = $rgName
}
$kv = $null
try {
$kv = Get-AzKeyVault @kvSplat -ErrorAction Stop
}
catch {
Write-Warning $Error[0]
}
if ($null -eq $kv) {
#
# Check if KeyVault is soft deleted
# if soft deleted -> purge it and create new KeyVault with same name
#
$softDeleted = $false
$softDeletedVaults = Get-AzKeyVault -InRemovedState # fetch all deleted vaults in the current subscription
if ($softDeletedVaults.Count -gt 0) {
$softDeletedVault = $softDeletedVaults.Where{ $_.Name -like $kvName }
$softDeleted = $null -ne $softDeletedVault
}
if ($softDeleted) {
Write-Host "Vault '$($kvName)' is currently in 'deleted but recoverable' state `r`n"
$kvRmSplat = @{
VaultName = $kvName
Location = $rgLocation
}
try {
#
# Purge soft deleted vault
#
Remove-AzKeyVault @kvRmSplat -InRemovedState -Force | Out-Null
Start-Sleep -Seconds 2
Write-Host "Vault '$($kvName)' was purged `r`n"
}
catch {
Write-Warning $Error[0]
}
}
else {
Write-Host "Vault '$($kvName)' does not exist `r`n"
}
#
# Create vault
#
$kvNewSplat = @{
Name = $kvName
ResourceGroupName = $rgName
Location = $rgLocation
Sku = 'Standard'
SoftDeleteRetentionInDays = 7
}
Write-Host "Creating new vault... `r`n"
$kv = New-AzKeyVault @kvNewSplat -EnableRbacAuthorization
Write-Host "KeyVault '$($kvName)' has been created `r`n"
}
else {
Write-Host "KeyVault '$($kvName)' already exists `r`n"
}
$kvResourceId = $kv.ResourceId # will be used as scope to assign role
#
# Assign role to current user so that current user can create key
#
$userObjectId = Get-LoggedInUserObjectId
$userRaSplat = @{
RoleDefinitionName = $kvRoleLoggedInUser
ObjectId = $userObjectId
Scope = $kvResourceId
}
New-RoleAssignement @userRaSplat
#
# Assign role to service principal so that service principal can access key (RBAC -> vault access policy)
#
$spRaSplat = @{
RoleDefinitionName = $kvRoleServicePrincipal
ObjectId = $spObjectId
Scope = $kvResourceId
}
New-RoleAssignement @spRaSplat
#
# Create key (this key will be used to encrypt/decrypt Pulumi secrets)
#
$kvKey = $null
try {
$kvKey = Get-AzKeyVaultKey -VaultName $kvName -Name $kvKeyName -ErrorAction Stop
}
catch {
Write-Warning $Error[0]
}
if ($null -eq $kvKey) {
$softDeleted = $false
$softDeletedKeys = Get-AzKeyVaultKey -VaultName $kvName -InRemovedState
if ($softDeletedKeys.Count -gt 0) {
$softDeletedKey = $softDeletedKeys.Where{ $_.Name -like $kvKeyName }
$softDeleted = $null -ne $softDeletedKey
}
if ($softDeleted) {
Write-Host "Key '$($kvKeyName)' is currently in 'deleted but recoverable' state `r`n"
try {
$rmKeySplat = @{
Name = $kvKeyName
VaultName = $kvName
}
Remove-AzKeyVaultKey @rmKeySplat -InRemovedState -Force
Write-Host "'$($kvKeyName)' was purged. Creating new key with same name... `r`n"
Start-Sleep -Seconds 3
$kvKey = (Add-AzKeyVaultKey -VaultName $kvName -Name $kvKeyName -Destination "Software")
Write-Host "New key '$($kvKeyName)' has been created `r`n"
}
catch {
Write-Warning $Error[0]
}
}
else {
Write-Host "Key '$($kvKeyName)' does not exist, creating new... `r`n"
$kvKey = (Add-AzKeyVaultKey -VaultName $kvName -Name $kvKeyName -Destination "Software")
Write-Host "Key '$($kvKeyName)' has been created `r`n"
}
}
else {
Write-Host "Key '$($kvKeyName)' already exists `r`n"
}
if ($null -eq $kvKey) {
Write-Error "Failed to create key '$($kvKeyName)'. Terninating script execution"
return
}
#$kvKeyId = $kvKey.Id
#
# ----------------------------------------------------------------------------------------------------
# Environment variables for Pulumi
# ----------------------------------------------------------------------------------------------------
#
$envVars = $spCredential + $pulumiCredential + $blobContainerCredential
Write-Host "Setting environment variables..."
foreach ($key in $envVars.keys) {
[Environment]::SetEnvironmentVariable($key, $envVars[$key], "User")
}
Write-Host "All environment variables has been set `r`n"
#
# ----------------------------------------------------------------------------------------------------
# Generate Pulumi commands
# ----------------------------------------------------------------------------------------------------
#
#
# Now, create Pulumi commands for using Custom Backend (Azure KeyVault and Storage Container)
#
#
# START: pulumi commands multi-line string ----------------------------------------------------------------------
$pulumiCmdStr = @"
#
# Restart PowerShell / VS Code
# (Make sure that Pulumi fetches latest Environments Variables)
#
# Set-Location `$PSScriptRoot
#./$pulumiCmdPowerShellFile
# pulumi stack rm --stack=$pulumiStackName --yes #--preserve-config
#
#
# Set variables for pulumi command
#
`$blobContainerName = '$blobContainerName'
`$resourceGroupLocation = '$rgLocation'
`$kvName = '$kvName'
`$kvKeyName = '$kvKeyName'
`$solutionName = '$solutionName' # VS solution
`$pulumiProjectName = '$pulumiProjectName' # Pulumi project -> VS project (will be added to VS solution)
`$pulumiProjectDescription = '$pulumiProjectDescription'
`$pulumiStackName = '$pulumiStackName'
#
# Create solution directory and VS solution
#
if (Test-Path .\`$solutionName) {
# Clean up folder
Remove-Item .\`$solutionName\* -Force -Recurse | Out-Null
} else {
# Create folder
New-Item -Path .\`$solutionName -ItemType Directory | Out-Null
}
Set-Location `$solutionName
dotnet new sln --name `$solutionName
#
# Pulumi login to Azure (blob container as stack state backend)
#
pulumi login azblob://`$blobContainerName
#
# Pulumi new project (Azure KeyVault key for pulumi secret)
#
pulumi new azure-csharp ``
--stack="`$pulumiStackName" ``
--secrets-provider="azurekeyvault://`$kvName.vault.azure.net/keys/`$kvKeyName" ``
--name="`$pulumiProjectName" ``
--description="`$pulumiProjectDescription" ``
--dir="`$pulumiProjectName" ``
--config="azure-native:location=`$resourceGroupLocation" ``
--force
#
# Add pulumi project to VS solution
#
dotnet sln add ./`$pulumiProjectName/`$pulumiProjectName.csproj
#
# Deploy pulumi stack to Azure (target subscription)
#
# Prevent Pulumi from using Azure CLI token -> logout from all accounts
az account clear
az logout
Set-Location `$pulumiProjectName
pulumi up #--yes
"@
# END: pulumi commands multi-line string -----------------------------------------------------------------------
#
#
# Save Pulumi commands to .ps1 file
#
Write-Host "Saving Pulumi commands to '$($pulumiCmdPowerShellFile)' `r`n"
$pulumiCmdSplat = @{
Content = $pulumiCmdStr
FileUriWithExtension = $pulumiCmdPowerShellFile
}
WriteTo-File @pulumiCmdSplat
#
# Restart PowerShell / VS Code
# (Make sure that Pulumi fetches latest Environments Variables)
#
Next
- Preparing DevOps project and repository for Pulumi
- Creating service connection for Pulumi stack deployment pipeline