-
Notifications
You must be signed in to change notification settings - Fork 53
/
build.sh
executable file
·500 lines (439 loc) · 17.3 KB
/
build.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
#!/bin/bash
#set -x
# Build the HPC infrastructure with Terraform.
# build.sh -a [plan, apply, destroy] -v <vars file> -f <folder>
#
set -e
THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
AZHOP_CONFIG=config.yml
BICEP_ROOT=$THIS_DIR/bicep
ARM_ROOT=$THIS_DIR/marketplace/solution
AZCLI_VERSION_MAJOR=2
AZCLI_VERSION_MINOR=37
AZCLI_VERSION_PATCH=0
AZCLI_VERSION="$AZCLI_VERSION_MAJOR.$AZCLI_VERSION_MINOR.$AZCLI_VERSION_PATCH"
AZHOP_ROOT=$THIS_DIR
AZHOP_FROM=local
#TFVARS_FILE=""
TF_FOLDER=$THIS_DIR/tf
TF_TARGET=
VALIDATE_CONFIG=true
VERBOSE=0
function usage()
{
echo "Usage build.sh "
echo " Required arguments:"
echo " -a|--action [plan, apply, destroy] - Destroy will not applied with Bicep"
echo " "
echo " Optional arguments:"
echo " -f|--folder <relative path> - relative folder name containing the terraform files, default is ./tf"
echo " -l|--language <tf, bicep, arm> - deployment language to use, default is tf. arm template will be generated from bicep"
echo " --no-validate - skip validation of config.yml"
}
#######################################################################################################################
#
# Terraform Helpers
#
#######################################################################################################################
function terraform_init()
{
export TF_CLI_ARGS_apply="-auto-approve"
#export TF_LOG=INFO
#export TF_LOG_PATH=.terraform/terraform.log
# As we don't managed TF state in blob don't call this
#get_arm_access_key
# If an existing terraform state exists for a different resource group, then delete it.
if [ -e $THIS_DIR/tf/terraform.tfstate ]; then
rg_in_state=$(jq -r '.resources[] | select(.type=="azurerm_resource_group") | .instances[] | .attributes.name' $THIS_DIR/tf/terraform.tfstate)
rg_in_config=$(yq eval '.resource_group' $AZHOP_CONFIG)
set +e
echo $rg_in_state | grep -w -q $rg_in_config
if [ $? -eq 1 ]; then
echo "Deleting existing terraform state for resource group $rg_in_state"
rm -rf $THIS_DIR/tf/terraform.tfstate
else
echo "Keep existing terraform state for resource group $rg_in_state"
fi
set -e
fi
terraform -chdir=$TF_FOLDER init -upgrade
if [ -e $THIS_DIR/tf/terraform.tfstate ] && [ $TF_FOLDER != $THIS_DIR/tf ]; then
cp -u -f $THIS_DIR/tf/terraform.tfstate $TF_FOLDER
fi
}
function terraform_run()
{
# -parallelism=30
set +e
export TF_LOG="INFO"
export TF_LOG_PATH="$THIS_DIR/tf/terraform.log"
rm -f $TF_LOG_PATH
echo "terraform -chdir=$TF_FOLDER $TF_COMMAND $PARAMS"
terraform -chdir=$TF_FOLDER $TF_COMMAND $PARAMS
export EXIT_CODE=$?
if [ -e $TF_FOLDER/terraform.tfstate ] && [ $TF_FOLDER != $THIS_DIR/tf ]; then
cp -u -f $TF_FOLDER/terraform.tfstate $THIS_DIR/tf/terraform.tfstate
fi
}
# Set the ARM_ACCESS_KEY env variable to be used by TF to manage states in azure blobs backend
# The ./tf/backend.tf file contains the values of the storage ressource group, account and container to be used
function get_arm_access_key {
TF_BACKEND_FILE=$TF_FOLDER/backend.tf
if [ ! -e $TF_BACKEND_FILE ]; then
echo "No backend specified, using local state"
else
RESOURCE_GROUP_NAME=$(grep "resource_group_name" $TF_BACKEND_FILE | cut -d'=' -f 2 | xargs)
STORAGE_ACCOUNT_NAME=$(grep "storage_account_name" $TF_BACKEND_FILE | cut -d'=' -f 2 | xargs)
ACCOUNT_KEY=$(az storage account keys list --resource-group $RESOURCE_GROUP_NAME --account-name $STORAGE_ACCOUNT_NAME --query '[0].value' -o tsv)
if [ "$ACCOUNT_KEY" == "" ]; then
echo "unable to retrieve the account key for accessing terraform blob backend"
exit 1
fi
echo "Using backend stored in $RESOURCE_GROUP_NAME/$STORAGE_ACCOUNT_NAME"
export ARM_ACCESS_KEY=$ACCOUNT_KEY
fi
}
#######################################################################################################################
#
# Azure Functions
#
#######################################################################################################################
function get_azure_context()
{
# Inspired from https://github.com/aztfmod/rover/blob/4098ce32e46f854445ac85839125f21410b439fc/scripts/functions.sh#L807
# Retrieve under which identity we run
user_type=$(az account show --query user.type -o tsv)
export TF_VAR_tenant_id=$(az account show -o json | jq -r .tenantId)
subscription_id=$(az account show --query id -o tsv)
if [ ${user_type} == "user" ]; then
unset ARM_TENANT_ID
unset ARM_SUBSCRIPTION_ID
unset ARM_CLIENT_ID
unset ARM_CLIENT_SECRET
unset ARM_USE_MSI
export TF_VAR_logged_user_objectId=$(az ad signed-in-user show --query id -o tsv)
logged_user_upn=$(az ad signed-in-user show --query userPrincipalName -o tsv)
echo " - logged in Azure with User ${logged_user_upn}"
else
unset TF_VAR_logged_user_objectId
export clientId=$(az account show --query user.name -o tsv)
case "${clientId}" in
"systemAssignedIdentity")
mds=$(curl -s --noproxy "*" -H Metadata:true "http://169.254.169.254/metadata/instance?api-version=2019-08-15")
vmname=$(echo $mds | jq -r '.compute.name')
rgname=$(echo $mds | jq -r '.compute.resourceGroupName')
echo " - logged in Azure with System Assigned Identity from ${vmname}/${rgname}"
export TF_VAR_logged_user_objectId=$(az resource list -n $vmname -g $rgname --query [*].identity.principalId --out tsv)
if [ "$TF_VAR_logged_user_objectId" == "" ]; then
export TF_VAR_logged_user_objectId=$(az resource list -n $vmname -g $rgname --query [*].identity.userAssignedIdentities.*.principalId --out tsv)
fi
export ARM_TENANT_ID=${TF_VAR_tenant_id}
export ARM_SUBSCRIPTION_ID=${subscription_id}
export ARM_USE_MSI=true
logged_user_upn="${TF_VAR_logged_user_objectId} from ${vmname}"
;;
"userAssignedIdentity")
mds=$(curl -s --noproxy "*" -H Metadata:true "http://169.254.169.254/metadata/instance?api-version=2019-08-15")
vmname=$(echo $mds | jq -r '.compute.name')
rgname=$(echo $mds | jq -r '.compute.resourceGroupName')
echo " - logged in Azure with User Assigned Identity from ${vmname}/${rgname}"
export TF_VAR_logged_user_objectId=$(az resource list -n $vmname -g $rgname --query [*].identity.userAssignedIdentities.*.principalId --out tsv)
client_id=$(az vm identity show -g $rgname --n $vmname --query userAssignedIdentities.*.clientId -o tsv)
export ARM_TENANT_ID=${TF_VAR_tenant_id}
export ARM_CLIENT_ID=${client_id}
export ARM_SUBSCRIPTION_ID=${subscription_id}
export ARM_USE_MSI=true
logged_user_upn="${TF_VAR_logged_user_objectId} from ${vmname}"
;;
*)
export TF_VAR_logged_user_objectId=$(az ad sp show --id ${clientId} --query id -o tsv)
logged_user_upn=$(az ad sp show --id ${clientId} --query displayName -o tsv)
echo " - logged in Azure with Service Principal Name ${logged_user_upn}"
export ARM_TENANT_ID=${TF_VAR_tenant_id}
export ARM_SUBSCRIPTION_ID=${subscription_id}
;;
esac
fi
export TF_VAR_CreatedBy=${logged_user_upn}
}
function get_storage_id {
# get the storage account ID to use
id=$(az storage account list -g $RESOURCE_GROUP --query "[?contains(@.name, 'storage')==\`true\`].id" | jq -r '.[0]')
if [ ${id} == null ]; then
echo "no storage account found to store the TF state"
exit 1
fi
}
function check_azcli_version {
version=$(az --version | grep azure-cli | xargs | cut -d' ' -f 2)
major=$(echo $version | cut -d'.' -f 1)
minor=$(echo $version | cut -d'.' -f 2)
patch=$(echo $version | cut -d'.' -f 3)
if [ $major -lt $AZCLI_VERSION_MAJOR ]; then
echo "azure-cli version $AZCLI_VERSION or higher is required"
exit 1
else
if [ $minor -lt $AZCLI_VERSION_MINOR ]; then
echo "azure-cli version $AZCLI_VERSION or higher is required"
exit 1
else
if [ $patch -lt $AZCLI_VERSION_PATCH ]; then
echo "azure-cli version $AZCLI_VERSION or higher is required"
exit 1
fi
fi
fi
}
function get_azure_cloud_env()
{
# Retrieve on which cloud environment we run on
# env details from here: https://github.com/hashicorp/terraform-provider-azurerm/blob/main/vendor/github.com/Azure/go-autorest/autorest/azure/environments.go
# TODO: check az environmentName for AzureChina and AzureGerman
cloud_env="Public"
account_env=$(az account show --output json| jq '.environmentName' -r)
case "$account_env" in
# AzureChinaCloud)
# export TF_VAR_AzureEnvironment=AZURECHINACLOUD
# export TF_VAR_KeyVaultSuffix=vault.azure.cn
# export TF_VAR_BlobStorageSuffix=blob.core.chinacloudapi.cn
# ;;
# AzureGermanCloud)
# export TF_VAR_AzureEnvironment=AZUREGERMANCLOUD
# export TF_VAR_KeyVaultSuffix=vault.microsoftazure.de
# export TF_VAR_BlobStorageSuffix=blob.core.cloudapi.de
# ;;
AzureCloud)
export TF_VAR_AzureEnvironment=AZUREPUBLICCLOUD
# export TF_VAR_KeyVaultSuffix=vault.azure.net
# export TF_VAR_BlobStorageSuffix=blob.core.windows.net
;;
AzureUSGovernment)
export TF_VAR_AzureEnvironment=AZUREUSGOVERNMENTCLOUD
# export TF_VAR_KeyVaultSuffix=vault.usgovcloudapi.net
# export TF_VAR_BlobStorageSuffix=blob.core.usgovcloudapi.net
;;
*)
echo "ERROR: Unknown Azure environment ${account_env}"
exit 1
;;
esac
}
#######################################################################################################################
#
# BICEP Helpers
#
#######################################################################################################################
function set_bicep_param_value()
{
local Param=$1
local Value=$2
case $Value in
"true"|"True")
eval_str=". | $Param=true"
;;
"false"|"False")
eval_str=". | $Param=false"
;;
*)
eval_str=". | $Param=\$param"
;;
esac
jq "$eval_str" --arg param "$Value" $BICEP_PARAMS > $TMP_PARAMS
cp $TMP_PARAMS $BICEP_PARAMS
}
function set_bicep_azhopconfig()
{
jq '. | .parameters.azhopConfig.value=$json' --argjson json "$(yq $AZHOP_CONFIG -o json | jq '.' -c)" $BICEP_PARAMS > $TMP_PARAMS
cp $TMP_PARAMS $BICEP_PARAMS
}
function arm_init()
{
pushd $ARM_ROOT
./build.sh local
popd
}
function bicep_init()
{
# Build the parameters.json file based on the config file content
location=$(yq '.location' $AZHOP_CONFIG)
resource_group=$(yq '.resource_group' $AZHOP_CONFIG)
adminuser=$(yq '.admin_user' $AZHOP_CONFIG)
BICEP_PARAMS=$resource_group.parameters.json
TMP_PARAMS=tmp.parameters.json
AZHOP_DEPLOYMENT_OUTPUT=$resource_group.outputs.json
# Set default param template file if it doesn't exists for the resource group
if [ ! -e $BICEP_PARAMS ]; then
cp $BICEP_ROOT/param.json $BICEP_PARAMS
fi
if [ "$AZHOP_FROM" == "local" ]; then
set_bicep_param_value ".parameters.loggedUserObjectId.value" "$TF_VAR_logged_user_objectId"
fi
set_bicep_param_value ".parameters.autogenerateSecrets.value" "false"
set_bicep_param_value ".parameters.branchName.value" "$(git branch | grep "*" | cut -d' ' -f 2)"
set_bicep_azhopconfig
if [ "$AZHOP_FROM" == "deployer" ]; then
# Change the jumpbox name to deployer if present
sed -i 's/jumpbox/deployer/g' $BICEP_PARAMS
# Add the logged user as a reader of the keyvault
set_bicep_param_value ".parameters.azhopConfig.value.key_vault_readers" "$TF_VAR_logged_user_objectId"
fi
# Read secrets from the parameter file as we don't know the keyvault name until a proper deployment has been successful
adminPassword=$(jq -r '.parameters.adminPassword.value' $BICEP_PARAMS)
if [ "$adminPassword" == "null" ]; then
set_bicep_param_value ".parameters.adminPassword.value" "$(openssl rand -base64 24)"
fi
databaseAdminPassword=$(jq -r '.parameters.databaseAdminPassword.value' $BICEP_PARAMS)
if [ "$adminPassword" == "null" ]; then
set_bicep_param_value ".parameters.databaseAdminPassword.value" "$(openssl rand -base64 24)"
fi
adminSshPublicKey=$(jq -r '.parameters.adminSshPublicKey.value' $BICEP_PARAMS)
if [ "$adminSshPublicKey" == "null" ]; then
# generate a new key if it doesn't exists
if [ ! -e $AZHOP_ROOT/${adminuser}_id_rsa ]; then
ssh-keygen -f $AZHOP_ROOT/${adminuser}_id_rsa -N ""
fi
set_bicep_param_value ".parameters.adminSshPublicKey.value" "$(cat $AZHOP_ROOT/${adminuser}_id_rsa.pub)"
set_bicep_param_value ".parameters.adminSshPrivateKey.value" "$(cat $AZHOP_ROOT/${adminuser}_id_rsa)"
fi
}
function bicep_run()
{
timestamp=$(date -u +"%Y.%m%d.%H%M%S")
deployment_name=azhop_${timestamp}
deployment_op="create"
if [ "$TF_COMMAND" == "plan" ]; then
deployment_op="what-if"
fi
if [ "$VERBOSE" == "1" ]; then
az bicep build --file $BICEP_ROOT/mainTemplate.bicep --outfile mainTemplate.json
cat mainTemplate.json
az deployment sub validate --template-file $BICEP_ROOT/mainTemplate.bicep --location $location --parameters @$BICEP_PARAMS
fi
case $DEPLOY_LANGUAGE in
"bicep")
echo "* Deploying using Bicep"
TEMPLATE_FILE=$BICEP_ROOT/mainTemplate.bicep
;;
"arm")
TEMPLATE_FILE=$ARM_ROOT/build_local/mainTemplate.json
echo "* Deploying using ARM"
;;
esac
az deployment sub $deployment_op --template-file $TEMPLATE_FILE --location $location -n $deployment_name --parameters @$BICEP_PARAMS
if [ "$TF_COMMAND" == "plan" ]; then
exit
fi
echo "* Getting deployment output"
az deployment group show \
-g $resource_group \
-n azhop \
--query properties.outputs \
> $AZHOP_DEPLOYMENT_OUTPUT
# if [ "$AZHOP_FROM" == "local" ]; then
# Update config path
jq '. | .azhopGlobalConfig.value.global_config_file=$param' --arg param $AZHOP_ROOT/config.yml $AZHOP_DEPLOYMENT_OUTPUT > $TMP_PARAMS
cp $TMP_PARAMS $AZHOP_DEPLOYMENT_OUTPUT
kv=$(jq -r .keyvaultName.value $AZHOP_DEPLOYMENT_OUTPUT)
echo "* Getting keys from keyvault"
az keyvault secret show --vault-name $kv -n ${adminuser}-pubkey --query "value" -o tsv > $AZHOP_ROOT/${adminuser}_id_rsa.pub
az keyvault secret show --vault-name $kv -n ${adminuser}-privkey --query "value" -o tsv > $AZHOP_ROOT/${adminuser}_id_rsa
chmod 600 $AZHOP_ROOT/${adminuser}_id_rsa*
mkdir -p $AZHOP_ROOT/bin
jq -r .azhopGetSecretScript.value $AZHOP_DEPLOYMENT_OUTPUT > $AZHOP_ROOT/bin/get_secret
chmod +x $AZHOP_ROOT/bin/get_secret
jq -r .azhopConnectScript.value $AZHOP_DEPLOYMENT_OUTPUT > $AZHOP_ROOT/bin/connect
chmod +x $AZHOP_ROOT/bin/connect
mkdir -p $AZHOP_ROOT/playbooks/group_vars
jq .azhopGlobalConfig.value $AZHOP_DEPLOYMENT_OUTPUT | yq -P > $AZHOP_ROOT/playbooks/group_vars/all.yml
jq '.azhopInventory.value' $AZHOP_DEPLOYMENT_OUTPUT | yq -P > $AZHOP_ROOT/playbooks/inventory
# substitute passwords into the file
# - __ADMIN_PASSWORD__
admin_pass="$(az keyvault secret show --vault-name $kv -n ${adminuser}-password --query "value" -o tsv)"
sed -i "s/__ADMIN_PASSWORD__/$(sed 's/[&/\]/\\&/g' <<< $admin_pass)/g" $AZHOP_ROOT/playbooks/inventory
jq .azhopPackerOptions.value $AZHOP_DEPLOYMENT_OUTPUT > $AZHOP_ROOT/packer/options.json
# fi
}
if [ $# -eq 0 ]; then
usage
exit 1
fi
unset PARAMS
DEPLOY_LANGUAGE=tf
while (( "$#" )); do
case "${1}" in
-a|--action)
TF_COMMAND=${2}
# verify that the action is either plan, apply or destroy
if [ "$TF_COMMAND" != "plan" ] && [ "$TF_COMMAND" != "apply" ] && [ "$TF_COMMAND" != "destroy" ] && [ "$TF_COMMAND" != "import" ] && [ "$TF_COMMAND" != "state" ]; then
echo "Invalid action $TF_COMMAND. Valid values are plan, apply or destroy"
exit 1
fi
shift 2
;;
-l|--language)
DEPLOY_LANGUAGE=${2}
# verify that the language is either tf or bicep
if [ "$DEPLOY_LANGUAGE" != "tf" ] && [ "$DEPLOY_LANGUAGE" != "bicep" ] && [ "$DEPLOY_LANGUAGE" != "arm" ]; then
echo "Invalid language $DEPLOY_LANGUAGE. Valid values are tf, bicep or arm"
exit 1
fi
shift 2
;;
-f|--folder)
TF_FOLDER=${THIS_DIR}/${2}
shift 2
;;
--no-validate)
VALIDATE_CONFIG=false
shift 1
;;
*)
PARAMS+="${1} "
shift
;;
esac
done
# Check that action is set
if [ -z "$TF_COMMAND" ]; then
echo "Action is required"
usage
exit 1
fi
if [ -d ${THIS_DIR}/miniconda ]; then
echo "Activating conda environment"
source ${THIS_DIR}/miniconda/bin/activate
fi
# Validate config against schema
if [ "$VALIDATE_CONFIG" = true ]; then
$THIS_DIR/validate_config.sh $AZHOP_CONFIG
fi
check_azcli_version
case $DEPLOY_LANGUAGE in
tf)
get_azure_context
get_azure_cloud_env
terraform_init
terraform_run
;;
bicep)
# If a Jumpbox VM is defined in the config file then assume it's a local deployment, otherwise it's done from the deployer VM
jumpbox=$(yq '.jumpbox' $AZHOP_CONFIG)
if [ "$jumpbox" == "null" ]; then
AZHOP_FROM="deployer"
else
AZHOP_FROM="local"
fi
get_azure_context
bicep_init
bicep_run
;;
arm)
# ARM deployment is always using a deplpyed VM
AZHOP_FROM="deployer"
arm_init
get_azure_context
bicep_init
bicep_run
;;
esac
exit $EXIT_CODE