Last active
January 3, 2025 20:17
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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