Skip to content

Instantly share code, notes, and snippets.

@aligneddev
Last active January 3, 2025 20:17
Show Gist options
  • Save aligneddev/6595b7c1ad9e6a551193ab5b8f98a346 to your computer and use it in GitHub Desktop.
Save aligneddev/6595b7c1ad9e6a551193ab5b8f98a346 to your computer and use it in GitHub Desktop.
Use F# to run az cli commands to add Azure App Config and Key Vault values
// https://aligneddev.net/blog/2024/fsharp-for-cli-commands/
// If you know how to get the Key Vault commands to work inside of the script, please let me know
// open in VS Code,
// click the Run Script button
// or highlight and Alt+Enter to run
// or in cli in this tools directory `dotnet fsi .\appConfigAndKeyVault.fsx`
// az --version to find the az.exe path
// az login
// az account show to check your current subscription
// if you get `echo Failed to load python executable.`
// python --help get the path
// in VS Code ctrl + shift + open user settings json
// overwrite "terminal.integrated.env.windows": { "PATH": "C:\\Python311\\python.exe;;C:\\Program Files\\Microsoft SDKs\\Azure\\CLI2\\az.cmd;${env:PATH}" }
// that might help for the az cli as well
// restart VS Code (ctrl + shift + p > reload window)
// az keyvault list
// az appconfig list
open System.Diagnostics
open System.Threading
#load "azCommands.fsx"
open AzCommands
let runAzCliCommand command =
let processStartInfo = ProcessStartInfo()
processStartInfo.FileName <- "cmd.exe"
processStartInfo.Arguments <- sprintf "/C %s" command
processStartInfo.RedirectStandardOutput <- true
processStartInfo.RedirectStandardError <- true
processStartInfo.UseShellExecute <- false
processStartInfo.CreateNoWindow <- true
printf "Running %s \n" command
use p = new Process()
p.StartInfo <- processStartInfo
p.Start() |> ignore
let output = p.StandardOutput.ReadToEnd()
let errorOutput = p.StandardError.ReadToEnd()
p.WaitForExit()
if p.ExitCode <> 0 then
sprintf "Error: %s" errorOutput
else
output
let runCommand azCommandValues onlyPrintCommands =
let buildConfigOnlyCommand azCommandValues =
$"az appconfig kv set --name {azCommandValues.AzResource.AppConfigName} --key {azCommandValues.ConfigValues.ConfigKeyName} --value \"{azCommandValues.ConfigValues.ConfigValue}\" --yes"
let buildWithKeyVaultCommand azCommandValues =
let addToKeyVault =
$"az keyvault secret set --vault-name '{azCommandValues.AzResource.VaultName}' --name '{azCommandValues.ConfigValues.SecretName}' --value \"{azCommandValues.ConfigValues.SecretValue}\" --content-type \"{configContentTypeForKvReference}\""
let keyVaultUrl =
$"https://{azCommandValues.AzResource.VaultName}.vault.azure.net/secrets/{azCommandValues.ConfigValues.SecretName}"
let addKeyVaultReferenceToConfig =
$"az appconfig kv set-keyvault --name {azCommandValues.AzResource.AppConfigName} --key {azCommandValues.ConfigValues.ConfigKeyName} --secret-identifier {keyVaultUrl} --yes"
{| addToKeyVault = addToKeyVault
addKeyVaultReferenceToConfig = addKeyVaultReferenceToConfig |}
match azCommandValues.CommandType with
| ConfigOnly ->
(buildConfigOnlyCommand azCommandValues
|> (fun commands ->
if onlyPrintCommands then
commands
else
runAzCliCommand commands))
| KeyVault ->
(buildWithKeyVaultCommand azCommandValues
|> (fun r ->
if onlyPrintCommands then
$"{r.addToKeyVault} && ${r.addKeyVaultReferenceToConfig}"
else
// az login causes a popup, still doesn't work
// https://learn.microsoft.com/en-us/cli/azure/authenticate-azure-cli-interactively
// TODO try az login --tenant $YOUR_TENANT_ID -u $YOUR_USERNAME -p $YOUR_PASSWORD
// let login = "az config set core.login_experience_v2=off && az login --tenant $YOUR_TENANT_ID -u $YOUR_USERNAME -p $YOUR_PASSWORD
// let kv = runAzCliCommand $"{login} && {r.addToKeyVault}"
// Thread.Sleep(1000)
// let kvr = runAzCliCommand r.addKeyVaultReferenceToConfig
// $"{kv} {System.Environment.NewLine} {kvr}"
$"Command to run manually {System.Environment.NewLine}{r.addToKeyVault} && {r.addKeyVaultReferenceToConfig}"
))
// without the az login, it is failing with ERROR: <urllib3.connection.HTTPSConnection object at 0x0000021805AA4AD0>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed
let runCommands azCommandValues onlyPrintCommands =
if onlyPrintCommands then
printf "Only Printing Commands!"
azCommandValues
|> List.map (fun commands ->
let result = runCommand commands onlyPrintCommands
if azCommandValues.Length > 1 then
Thread.Sleep(1000)
result)
|> String.concat $" {System.Environment.NewLine}"
// it's idempotent, so Azure doesn't overwrite existing ones or duplicate them
// TODO read in from a file or event dotnet user-secrets?
let commands =
[]
// |> List.append messageQueueCommands
|> List.append agSyncCommands
// |> List.append agXCommands
// |> List.append agrisCommands
// |> List.append reportingCommands
// helpful to verify setup and paths
//runAzCliCommand "az --version" |> printf "%s"
let onlyPrintCommands = false
// Key Vault entries aren't getting created, so it's just printing the commands out to run manually
// if Error: ERROR: <urllib3.connection.HTTPSConnection object at 0x0000021805AA4AD0>: Failed to establish a new connection: [Errno 11001] getaddrinfo failed
// you may have an error in the cli call. Copy the command and run it in another command prompt
// TODO: improve error catching from the cli call and display it
(runCommands commands onlyPrintCommands)
|> printf "%s"
// could put in new azCommands.fsx file if you want to
// add or remove #load "azCommands.fsx" above
// This is where we define our values to put into Azure. We also have more functions to make building commands more succinct
``` fsharp
type AzResource =
{ AppConfigName: string
VaultName: string }
type AzCommandType =
| ConfigOnly
| KeyVault
// TODO refactor and split out config and KeyVault ?
// using options here would be a good refactor to avoid "" meaning not included
type ConfigValues =
{ SecretName: string //option
SecretValue: string //option
ConfigKeyName: string
ConfigValue: string } //option
type AzCommandValues =
{ CommandType: AzCommandType
AzResource: AzResource
ConfigValues: ConfigValues }
type Environment =
| Engineering
| Production
// vaultUrl="https://$valueName.vault.azure.net"
let configContentTypeForKvReference =
"application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
let buildCommand environment commandType configValues =
let azResource =
{ AppConfigName =
match environment with
| Engineering -> "appcs-common-engi-cus-02"
| Production -> "appcs-common-prod-cus-02"
VaultName =
match environment with
| Engineering -> "kv-common-engi-cus-01"
| Production -> "kv-common-prod-cus-01" }
{ CommandType = commandType
AzResource = azResource
ConfigValues = configValues }
let buildCommandPair commandType configValues prodConfigValue prodSecretValue =
[ buildCommand Engineering commandType configValues
buildCommand
Production
commandType
{ configValues with
ConfigValue = prodConfigValue
SecretValue = prodSecretValue } ]
type ConfigCommandValues =
{ ConfigKeyName: string
ConfigValueEngi: string
ConfigValueProd: string }
let buildConfigCommandPair values =
buildCommandPair
ConfigOnly
{ ConfigKeyName = values.ConfigKeyName
ConfigValue = values.ConfigValueEngi
SecretName = ""
SecretValue = "" }
values.ConfigValueProd
""
type KeyVaultCommandValues =
{ ConfigKeyName: string
SecretValueEngi: string
SecretValueProd: string }
let buildKeyVaultCommandPair values =
buildCommandPair
KeyVault
{ ConfigKeyName = values.ConfigKeyName
ConfigValue = ""
// Class:Key for ConfigKeyName, Class-Key for KeyVault
SecretName = values.ConfigKeyName.Replace(":", "-")
SecretValue = values.SecretValueEngi }
""
values.SecretValueProd
let myCommands =
[]
|> List.append (buildKeyVaultCommandPair
{ ConfigKeyName = "AppSettings:ApiPassword"
SecretValueEngi = "" // fill in, run then clear so you don't add to source control
SecretValueProd = "" })
|> List.append (buildConfigCommandPair
{ ConfigKeyName = "AppSettings:ApiUrl"
ConfigValueEngi = "https://myapi-dev.somewhere"
ConfigValueProd = "https://myapi.somewhere" })
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment