diff --git a/.gitignore b/.gitignore index 3276b3bfe..d7243e768 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ __pycache__/ **/.idea/ **/config_output/ **/exec_output + +# ignore deploy.sh output +**/generated-configurations/* diff --git a/src/build/apply_tf.sh b/src/build/apply_tf.sh index c0db1513c..d174bbe2d 100755 --- a/src/build/apply_tf.sh +++ b/src/build/apply_tf.sh @@ -35,8 +35,10 @@ tier2_vars=$6 display_tf_output=${7:-n} # reference paths -core_path=$(realpath ../core/) -scripts_path=$(realpath ../scripts/) +this_script_path=$(realpath "${BASH_SOURCE%/*}") +src_dir=$(dirname "${this_script_path}") +core_path="${src_dir}/core/" +scripts_path="${src_dir}/scripts/" # apply function apply() { @@ -90,24 +92,24 @@ apply() { while [ $apply_success == "false" ] do - echo "Applying ${name} (${attempts}/${max_attempts})..." + echo "INFO: applying ${name} (${attempts}/${max_attempts})..." if ! eval "$apply_command"; then # if we fail, run terraform destroy and try again - error_log "Failed to apply ${name} (${attempts}/${max_attempts}). Trying some manual clean-up and Terraform destroy..." + error_log "ERROR: failed to apply ${name} (${attempts}/${max_attempts}). Trying some manual clean-up and Terraform destroy..." eval "$destroy_command" ((attempts++)) if [[ $attempts -gt $max_attempts ]]; then - error_log "Failed ${max_attempts} times to apply ${name}. Exiting." + error_log "ERROR: failed ${max_attempts} times to apply ${name}. Exiting." exit 1 fi else # if we succeed meet the base case apply_success="true" - echo "Finished applying ${name}!" + echo "INFO: finished applying ${name}!" fi done } diff --git a/src/build/destroy_tf.sh b/src/build/destroy_tf.sh index 8cbec1861..f185ebbca 100755 --- a/src/build/destroy_tf.sh +++ b/src/build/destroy_tf.sh @@ -35,8 +35,10 @@ tier2_vars=$6 display_tf_output=${7:-n} # reference paths -core_path=$(realpath ../core/) -scripts_path=$(realpath ../scripts/) +this_script_path=$(realpath "${BASH_SOURCE%/*}") +src_dir=$(dirname "${this_script_path}") +core_path="${src_dir}/core/" +scripts_path="${src_dir}/scripts/" # destroy function destroy() { @@ -88,22 +90,22 @@ destroy() { while [ $destroy_success == "false" ] do - echo "Destroying ${name} (${attempts}/${max_attempts})..." + echo "INFO: destroying ${name} (${attempts}/${max_attempts})..." if ! eval "$destroy_command"; then # if we fail, run terraform destroy again until $max_attempts - error_log "Failed to destroy ${name} (${attempts}/${max_attempts})" + error_log "ERROR: failed to destroy ${name} (${attempts}/${max_attempts})" ((attempts++)) if [[ $attempts -gt $max_attempts ]]; then - error_log "Failed ${max_attempts} times to destroy ${name}. Exiting." + error_log "ERROR: failed ${max_attempts} times to destroy ${name}. Exiting." exit 1 fi else destroy_success="true" - echo "Finished destroying ${name}!" + echo "INFO: finished destroying ${name}!" fi done } diff --git a/src/clean.sh b/src/clean.sh new file mode 100755 index 000000000..4e1d37989 --- /dev/null +++ b/src/clean.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# +# shellcheck disable=1090,2154 +# +# remove resources deployed by deploy.sh by mlz env name + +set -e + +error_log() { + echo "${1}" 1>&2; +} + +show_help() { + print_formatted() { + long_name=$1 + char_name=$2 + desc=$3 + printf "%20s %2s %s \n" "$long_name" "$char_name" "$desc" + } + print_formatted "argument" "" "description" + print_formatted "--mlz-env-name" "-z" "[OPTIONAL] Unique name for MLZ environment (defaults to 'mlz' + UNIX timestamp)" +} + +usage() { + echo "clean.sh: remove resources deployed by deploy.sh by mlz env name" + show_help +} + +this_script_path=$(realpath "${BASH_SOURCE%/*}") +configuration_output_path="${this_script_path}/generated-configurations" + +# check for dependencies + +"${this_script_path}/scripts/util/checkforazcli.sh" +"${this_script_path}/scripts/util/checkforterraform.sh" + +# inspect user input +while [ $# -gt 0 ] ; do + case $1 in + -z | --mlz-env-name) mlz_env_name="$2" ;; + esac + shift +done + +# check mandatory parameters +# shellcheck disable=1083 +for i in { $mlz_env_name } +do + if [[ $i == "notset" ]]; then + error_log "ERROR: Missing required arguments. These arguments are mandatory: -z" + usage + exit 1 + fi +done + +# source generated config +mlz_config_file="${configuration_output_path}/${mlz_env_name}.mlzconfig" +. "${mlz_config_file}" + +# generate names for reference +. "${this_script_path}/scripts/config/generate_names.sh" "${mlz_config_file}" + +# source generated terraform vars +tfvars_filename="${mlz_env_name}.tfvars" +tfvars_path="${configuration_output_path}/${tfvars_filename}" + +# login +echo "INFO: setting current subscription to ${mlz_config_subid}..." +az account set \ + --subscription "${mlz_config_subid}" \ + --only-show-errors \ + --output none + +# destroy terraform +echo "INFO: destroying Terraform using ${mlz_config_file} and ${tfvars_path}..." +"${this_script_path}/build/destroy_tf.sh" \ + "${mlz_config_file}" \ + "${tfvars_path}" \ + "${tfvars_path}" \ + "${tfvars_path}" \ + "${tfvars_path}" \ + "${tfvars_path}" \ + "y" + +# clean up MLZ config resources +echo "INFO: cleaning up MLZ resources with tag 'DeploymentName=${mlz_env_name}'..." + +# Create array of unique subscription IDs. The 'sed' command below search thru the source +# variables file looking for all lines that do not have a '#' in the line. If a line with +# a '#' is found, the '#' and ever character after it in the line is ignored. The output +# of what remains from the sed command is then piped to grep to find the words that match +# the pattern. These words are what make up the 'mlz_subs' array. +mlz_sub_pattern="mlz_.*._subid" +mlz_subs=$(< "${mlz_config_file}" sed 's:#.*$::g' | grep -w "${mlz_sub_pattern}") +subs=() + +for mlz_sub in $mlz_subs +do + mlz_sub_id=$(echo "${mlz_sub#*=}" | tr -d '"') + if [[ ! "${subs[*]}" =~ ${mlz_sub_id} ]];then + subs+=("${mlz_sub_id}") + fi +done + +# delete resource groups where deploymentname is mlz_env_name in each subscription +for sub in "${subs[@]}"; +do + rgs_to_delete=$(az group list --subscription ${sub} --tag DeploymentName="${mlz_env_name}" --query [].name -o tsv) + for rg in $rgs_to_delete; + do + echo "INFO: deleting ${rg}..." + + az group delete \ + --name "${rg}" \ + --yes \ + --only-show-errors \ + --output none + done +done + +echo "INFO: deleting service principal ${mlz_sp_name}..." +az ad sp delete --id "http://${mlz_sp_name}" + +echo "INFO: Complete! Resources for ${mlz_env_name} deleted!" diff --git a/src/core/saca-hub/main.tf b/src/core/saca-hub/main.tf index 0cf864f19..0b90edc9c 100644 --- a/src/core/saca-hub/main.tf +++ b/src/core/saca-hub/main.tf @@ -27,6 +27,10 @@ provider "random" { resource "azurerm_resource_group" "hub" { location = var.mlz_location name = var.saca_rgname + + tags = { + DeploymentName = var.deploymentname + } } module "saca-hub-network" { diff --git a/src/core/saca-hub/variables.tf b/src/core/saca-hub/variables.tf index 4b169f903..e867a9d70 100644 --- a/src/core/saca-hub/variables.tf +++ b/src/core/saca-hub/variables.tf @@ -73,14 +73,17 @@ variable "firewall_address_space" { variable "saca_fwname" { description = "Name of the Hub Firewall" + default = "mlzDemoFirewall" } variable "firewall_ipconfig_name" { description = "The name of the Firewall IP Configuration" + default = "mlzDemoFirewallIpConfiguration" } variable "public_ip_name" { description = "The name of the Firewall Public IP" + default = "mlzDemoFirewallPip" } variable "create_network_watcher" { diff --git a/src/core/tier-0/main.tf b/src/core/tier-0/main.tf index adfb9b208..40800adeb 100644 --- a/src/core/tier-0/main.tf +++ b/src/core/tier-0/main.tf @@ -59,6 +59,10 @@ data "azurerm_firewall" "firewall" { resource "azurerm_resource_group" "t0" { location = var.mlz_location name = var.tier0_rgname + + tags = { + DeploymentName = var.deploymentname + } } module "t0-network" { @@ -133,4 +137,4 @@ module "t0-inbound-peering" { tags = { DeploymentName = var.deploymentname } -} \ No newline at end of file +} diff --git a/src/core/tier-0/variables.tf b/src/core/tier-0/variables.tf index 214a8070f..677d69ddd 100644 --- a/src/core/tier-0/variables.tf +++ b/src/core/tier-0/variables.tf @@ -76,6 +76,7 @@ variable "tier0_vnetname" { variable "tier0_vnet_address_space" { description = "Address space prefixes list of strings" type = list(string) + default = ["10.0.110.0/26"] } variable "subnets" { @@ -103,6 +104,44 @@ variable "subnets" { routetable_name = string })) + default = { + "tier0vms" = { + name = "tier0vms" + address_prefixes = ["10.0.110.0/27"] + service_endpoints = ["Microsoft.Storage"] + + enforce_private_link_endpoint_network_policies = false + enforce_private_link_service_network_policies = false + + nsg_name = "tier0vmsnsg" + nsg_rules = { + "allow_ssh" = { + name = "allow_ssh" + priority = "100" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "22" + destination_port_range = "" + source_address_prefix = "*" + destination_address_prefix = "" + }, + "allow_rdp" = { + name = "allow_rdp" + priority = "200" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "3389" + destination_port_range = "" + source_address_prefix = "*" + destination_address_prefix = "" + } + } + + routetable_name = "tier0vmsrt" + } + } } variable "create_network_watcher" { diff --git a/src/core/tier-1/main.tf b/src/core/tier-1/main.tf index 82f4ca74e..8cb009648 100644 --- a/src/core/tier-1/main.tf +++ b/src/core/tier-1/main.tf @@ -59,6 +59,10 @@ data "azurerm_firewall" "firewall" { resource "azurerm_resource_group" "t1" { location = var.mlz_location name = var.tier1_rgname + + tags = { + DeploymentName = var.deploymentname + } } module "t1-network" { diff --git a/src/core/tier-1/variables.tf b/src/core/tier-1/variables.tf index 156270850..3b0464d32 100644 --- a/src/core/tier-1/variables.tf +++ b/src/core/tier-1/variables.tf @@ -76,6 +76,7 @@ variable "tier1_vnetname" { variable "tier1_vnet_address_space" { description = "Address space prefixes for the virtual network" type = list(string) + default = ["10.0.115.0/26"] } variable "subnets" { @@ -103,6 +104,44 @@ variable "subnets" { routetable_name = string })) + default = { + "tier1vms" = { + name = "tier1vms" + address_prefixes = ["10.0.115.0/27"] + service_endpoints = ["Microsoft.Storage"] + + enforce_private_link_endpoint_network_policies = false + enforce_private_link_service_network_policies = false + + nsg_name = "tier1vmsnsg" + nsg_rules = { + "allow_ssh" = { + name = "allow_ssh" + priority = "100" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "22" + destination_port_range = "" + source_address_prefix = "*" + destination_address_prefix = "" + }, + "allow_rdp" = { + name = "allow_rdp" + priority = "200" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "3389" + destination_port_range = "" + source_address_prefix = "*" + destination_address_prefix = "" + } + } + + routetable_name = "tier1vmsrt" + } + } } variable "create_network_watcher" { diff --git a/src/core/tier-2/main.tf b/src/core/tier-2/main.tf index a835446a4..945b1c808 100644 --- a/src/core/tier-2/main.tf +++ b/src/core/tier-2/main.tf @@ -59,6 +59,10 @@ data "azurerm_firewall" "firewall" { resource "azurerm_resource_group" "t2" { location = var.mlz_location name = var.tier2_rgname + + tags = { + DeploymentName = var.deploymentname + } } module "t2-network" { diff --git a/src/core/tier-2/variables.tf b/src/core/tier-2/variables.tf index ccc3af44f..2b05521cc 100644 --- a/src/core/tier-2/variables.tf +++ b/src/core/tier-2/variables.tf @@ -76,6 +76,7 @@ variable "tier2_vnetname" { variable "tier2_vnet_address_space" { description = "Address space prefixes list of strings" type = list(string) + default = ["10.0.120.0/26"] } variable "subnets" { @@ -103,6 +104,44 @@ variable "subnets" { routetable_name = string })) + default = { + "tier2vms" = { + name = "tier2vms" + address_prefixes = ["10.0.120.0/27"] + service_endpoints = ["Microsoft.Storage"] + + enforce_private_link_endpoint_network_policies = false + enforce_private_link_service_network_policies = false + + nsg_name = "tier2vmsnsg" + nsg_rules = { + "allow_ssh" = { + name = "allow_ssh" + priority = "100" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "22" + destination_port_range = "" + source_address_prefix = "*" + destination_address_prefix = "" + }, + "allow_rdp" = { + name = "allow_rdp" + priority = "200" + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "3389" + destination_port_range = "" + source_address_prefix = "*" + destination_address_prefix = "" + } + } + + routetable_name = "tier2vmsrt" + } + } } variable "create_network_watcher" { diff --git a/src/deploy.sh b/src/deploy.sh new file mode 100755 index 000000000..060978a76 --- /dev/null +++ b/src/deploy.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# +# shellcheck disable=1090,2154 +# create all the configuration and deploy Terraform resources with minimal input + +set -e + +error_log() { + echo "${1}" 1>&2; +} + +show_help() { + print_formatted() { + long_name=$1 + char_name=$2 + desc=$3 + printf "%20s %2s %s \n" "$long_name" "$char_name" "$desc" + } + print_formatted "argument" "" "description" + print_formatted "--subscription-id" "-s" "Subscription ID for MissionLZ resources" + print_formatted "--location" "-l" "[OPTIONAL] The location that you're deploying to (defaults to 'eastus')" + print_formatted "--tf-environment" "-e" "[OPTIONAL] Terraform azurerm environment (defaults to 'public') see: https://www.terraform.io/docs/language/settings/backends/azurerm.html#environment" + print_formatted "--mlz-env-name" "-z" "[OPTIONAL] Unique name for MLZ environment (defaults to 'mlz' + UNIX timestamp)" + print_formatted "--hub-sub-id" "-h" "[OPTIONAL] subscription ID for the hub network and resources (defaults to the value provided for -s --subscription-id)" + print_formatted "--tier0-sub-id" "-0" "[OPTIONAL] subscription ID for tier 0 network and resources (defaults to the value provided for -s --subscription-id)" + print_formatted "--tier1-sub-id" "-1" "[OPTIONAL] subscription ID for tier 1 network and resources (defaults to the value provided for -s --subscription-id)" + print_formatted "--tier2-sub-id" "-2" "[OPTIONAL] subscription ID for tier 2 network and resources (defaults to the value provided for -s --subscription-id)" +} + +usage() { + echo "deploy.sh: create all the configuration and deploy Terraform resources with minimal input" + show_help +} + +this_script_path=$(realpath "${BASH_SOURCE%/*}") +configuration_output_path="${this_script_path}/generated-configurations" +timestamp=$(date +%s) + +##### check for dependencies ##### + +"${this_script_path}/scripts/util/checkforazcli.sh" +"${this_script_path}/scripts/util/checkforterraform.sh" + +##### generate an MLZ config file ##### + +# set helpful defaults that can be overridden or 'notset' for mandatory input +mlz_config_subid="notset" +mlz_config_location="eastus" +tf_environment="public" +mlz_env_name="mlz${timestamp}" +subs_args=() + +# inspect user input +while [ $# -gt 0 ] ; do + case $1 in + -s | --subscription-id) mlz_config_subid="$2" ;; + -l | --location) mlz_config_location="$2" ;; + -e | --tf-environment) tf_environment="$2" ;; + -z | --mlz-env-name) mlz_env_name="$2" ;; + -h | --hub-sub-id) subs_args+=("-h ${2}") ;; + -0 | --tier0-sub-id) subs_args+=("-0 ${2}") ;; + -1 | --tier1-sub-id) subs_args+=("-1 ${2}") ;; + -2 | --tier2-sub-id) subs_args+=("-2 ${2}") ;; + esac + shift +done + +# check mandatory parameters +# shellcheck disable=1083 +for i in { $mlz_config_subid } +do + if [[ $i == "notset" ]]; then + error_log "ERROR: Missing required arguments. These arguments are mandatory: -s" + usage + exit 1 + fi +done + +# switch to the MLZ subscription +echo "INFO: setting current subscription to ${mlz_config_subid}..." +az account set \ + --subscription "${mlz_config_subid}" \ + --only-show-errors \ + --output none + +# retrieve tenant ID for the MLZ subscription +mlz_tenantid=$(az account show \ + --query "tenantId" \ + --output tsv) + +# create MLZ configuration file based on user input +mlz_config_file="${configuration_output_path}/${mlz_env_name}.mlzconfig" +echo "INFO: creating an MLZ config file at ${mlz_config_file}..." + +# derive args from user input +gen_config_args=() +gen_config_args+=("-f ${mlz_config_file}") +gen_config_args+=("-e ${tf_environment}") +gen_config_args+=("-z ${mlz_env_name}") +gen_config_args+=("-l ${mlz_config_location}") +gen_config_args+=("-s ${mlz_config_subid}") +gen_config_args+=("-t ${mlz_tenantid}") + +# add hubs and spokes input, if present +for j in "${subs_args[@]}" +do + gen_config_args+=("$j") +done + +# expand array into a string of space separated arguments +gen_config_args_str=$(printf '%s ' "${gen_config_args[*]}") + +# create the file +# do not quote args $gen_config_args_str, we intend to split +# ignoring shellcheck for word splitting because that is the desired behavior +# shellcheck disable=SC2086 +"${this_script_path}/scripts/config/generate_config_file.sh" $gen_config_args_str + +##### create global terraform variables based on the MLZ config ##### + +tfvars_filename="${mlz_env_name}.tfvars" +tfvars_path="${configuration_output_path}/${tfvars_filename}" +echo "INFO: creating terraform variables at $tfvars_path..." +"${this_script_path}/scripts/terraform/create_globals_from_config.sh" "${tfvars_path}" "${mlz_config_file}" + +##### create MLZ resources ##### +echo "INFO: creating MLZ resources using ${mlz_config_file}..." +"${this_script_path}/scripts/mlz_tf_setup.sh" "${mlz_config_file}" + +# generate names for reference +. "${this_script_path}/scripts/config/generate_names.sh" "${mlz_config_file}" + +##### apply terraform using MLZ resources ##### +echo "INFO: applying Terraform using ${mlz_config_file} and ${tfvars_path}..." +"${this_script_path}/build/apply_tf.sh" \ + "${mlz_config_file}" \ + "${tfvars_path}" \ + "${tfvars_path}" \ + "${tfvars_path}" \ + "${tfvars_path}" \ + "${tfvars_path}" \ + "y" + +echo "INFO: Complete!" +echo "INFO: All finished? Want to clean up?" +echo "INFO: Try this command:" +echo "INFO: ${this_script_path}/clean.sh -z ${mlz_env_name}" diff --git a/src/docs/command-line-deployment.md b/src/docs/command-line-deployment.md index b4957f21b..ab8eac5f8 100644 --- a/src/docs/command-line-deployment.md +++ b/src/docs/command-line-deployment.md @@ -8,10 +8,90 @@ az login ``` +1. [Quickstart](#Quickstart) 1. [Configure the Terraform Backend](#Configure-the-Terraform-Backend) 1. [Set Terraform Configuration Variables](#Set-Terraform-Configuration-Variables) 1. [Deploy Terraform Configuration](#Deploy-Terraform-Configuration) +### Quickstart + +#### Quickstart Deploy + +Interested in just getting started and seeing what this does? Login to Azure CLI and try this command to deploy Mission LZ with some default configuration: + +```bash +src/deploy.sh -s {your_subscription_id} +``` + +> **NOTE** This implies some software pre-requisites. We highly [recommend using the .devcontainer](https://github.com/Azure/missionlz/blob/main/src/docs/getting-started.md#use-the-development-container-for-command-line-deployments) described in this repository to make thing easier. However, deploying Mission LZ via BASH shell is possible with these minimum requirements: +> +> - An Azure Subscription where you have ['Owner' RBAC permissions]() +> - The current version of Azure CLI (try `az cli -v` or see ) +> - Terraform CLI version > v0.13.4 (try `terraform -v` or see ) + +The `deploy.sh` command deploys all of the MLZ and Terraform resources, and by default, into a single subscription in Azure Commercial EastUS with a timestamped name. + +If you needed to deploy into another cloud, say Azure Government, you would [override the default region](https://azure.microsoft.com/en-us/global-infrastructure/geographies/#overview) and [default azurerm terraform environment](https://www.terraform.io/docs/language/settings/backends/azurerm.html#environment) like: + +```bash +az cloud set -n AzureUSGovernment +az login +src/deploy.sh -s {your_subscription_id} \ + --location usgovvirginia \ + --tf-environment usgovernment +``` + +For a complete list of arguments see [Quickstart Arguments](#Quickstart-Arguments). + +#### Quickstart Clean + +Once the deployment is complete, you'll be presented with a command that will clean up all of the resources that were deployed: + +```plaintext +INFO: Complete! +INFO: All finished? Want to clean up? +INFO: Try this command: +INFO: src/clean.sh -z mymlzenv +``` + +Which you can then execute like: + +```bash +src/clean.sh -z mymlzenv +``` + +The `clean.sh` command will call Terraform destroy for all the resources Terraform created and delete the MLZ resources and service principal. + +#### Quickstart Arguments + +If you don't wish to use those defaults, you can customize this command to target multiple subscriptions, different regions, and using different Terraform environments and azurerm configurations with the full set of arguments: + +```plaintext +deploy.sh: create all the configuration and deploy Terraform resources with minimal input + argument description + --subscription-id -s Subscription ID for MissionLZ resources + --location -l [OPTIONAL] The location that you're deploying to (defaults to 'eastus') + --tf-environment -e [OPTIONAL] Terraform azurerm environment (defaults to 'public') see: https://www.terraform.io/docs/language/settings/backends/azurerm.html#environment + --mlz-env-name -z [OPTIONAL] Unique name for MLZ environment (defaults to 'mlz' + UNIX timestamp) + --hub-sub-id -h [OPTIONAL] subscription ID for the hub network and resources (defaults to the value provided for -s --subscription-id) + --tier0-sub-id -0 [OPTIONAL] subscription ID for tier 0 network and resources (defaults to the value provided for -s --subscription-id) + --tier1-sub-id -1 [OPTIONAL] subscription ID for tier 1 network and resources (defaults to the value provided for -s --subscription-id) + --tier2-sub-id -2 [OPTIONAL] subscription ID for tier 2 network and resources (defaults to the value provided for -s --subscription-id) +``` + +For example, if I wanted to deploy into four subscriptions (one for each network) and provide my own name for created resources, I could do so like: + +```bash +src/deploy.sh -s {my_mlz_configuration_subscription_id} \ + -h {my_hub_network_subscription_id} \ + -0 {my_identity_network_subscription_id} \ + -1 {my_operations_network_subscription_id} \ + -2 {my_shared_services_network_subscription_id} \ + -z {my_mlz_environment_name} +``` + +Need further customization? The rest of this documentation covers in detail how to customize this deployment to your needs. + ### Configure the Terraform Backend The MLZ deployment architecture uses a single Service Principal whose credentials are stored in a central "config" Key Vault. Terraform state storage is distributed into a separate storage account for each tier. When deploying the MLZ architecture, all tiers can be deployed into a single subscription or each tier can be deployed into its own subscription. @@ -81,7 +161,6 @@ The script `destroy_terraform.sh` at [src/scripts/destroy_terraform.sh](/src/scr 1. The Global variables file 1. The directory that contains the main.tf and *.tfvars variables file of the configuration to apply - The hub network must be deployed first. See [Networking](https://github.com/Azure/missionlz#networking) for a description of the hub and spoke and what each network is used for. For saca-hub, run the following command to apply the terraform configuration from the root of this repository. diff --git a/src/mlz_tf_cfg.var.sample b/src/mlz_tf_cfg.var.sample index 9092fb5e7..d9a6652bb 100644 --- a/src/mlz_tf_cfg.var.sample +++ b/src/mlz_tf_cfg.var.sample @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. tf_environment="{TF_ENVIRONMENT}" # https://www.terraform.io/docs/language/settings/backends/azurerm.html#environment +mlz_metadatahost="{MLZ_METADATAHOST}" mlz_env_name="{MLZ_ENV_NAME}" # Unique name for MLZ environment mlz_config_subid="{MLZ_CONFIG_SUBID}" # Subscription ID for MissionLZ configuration resources mlz_config_location="{MLZ_CONFIG_LOCATION}" # Azure Region for deploying Mission LZ configuration resources diff --git a/src/scripts/apply_terraform.sh b/src/scripts/apply_terraform.sh index f28f81f1c..8d3dda7c2 100755 --- a/src/scripts/apply_terraform.sh +++ b/src/scripts/apply_terraform.sh @@ -13,8 +13,6 @@ set -e -PGM=$(basename "${0}") - if [[ "$#" -lt 2 ]]; then echo "apply_terraform.sh: initializes Terraform for a given directory using given a .env file for backend configuration" echo "usage: apply_terraform.sh " @@ -55,6 +53,7 @@ key="${mlz_env_name}${tf_name}" # initialize terraform in the configuration directory cd "${tf_dir}" || exit terraform init \ + -backend-config "metadata_host=${metadata_host}" \ -backend-config "key=${key}" \ -backend-config "resource_group_name=${tf_be_rg_name}" \ -backend-config "storage_account_name=${tf_be_sa_name}" \ diff --git a/src/scripts/config/append_prereq_endpoints.sh b/src/scripts/config/append_prereq_endpoints.sh index 1c42330ca..0bd97992c 100755 --- a/src/scripts/config/append_prereq_endpoints.sh +++ b/src/scripts/config/append_prereq_endpoints.sh @@ -24,13 +24,9 @@ fi file_to_append=$1 cloudEndpoints=($(az cloud show \ - --query '[endpoints.resourceManager, suffixes.acrLoginServerEndpoint, suffixes.keyvaultDns]' \ + --query '[endpoints.resourceManager, suffixes.acrLoginServerEndpoint, suffixes.keyvaultDns, name]' \ --output tsv)) -resourceManager=${cloudEndpoints[0]} -acrLoginServerEndpoint=${cloudEndpoints[1]} -keyvaultDns=${cloudEndpoints[2]} - append_if_not_empty() { key_name=$1 key_value=$2 @@ -40,6 +36,7 @@ append_if_not_empty() { fi } -append_if_not_empty "metadatahost" ${cloudEndpoints[0]} ${file_to_append} -append_if_not_empty "acrLoginServerEndpoint" ${cloudEndpoints[1]} ${file_to_append} -append_if_not_empty "keyvaultDns" ${cloudEndpoints[2]} ${file_to_append} +append_if_not_empty "mlz_metadatahost" ${cloudEndpoints[0]} ${file_to_append} +append_if_not_empty "mlz_acrLoginServerEndpoint" ${cloudEndpoints[1]} ${file_to_append} +append_if_not_empty "mlz_keyvaultDns" ${cloudEndpoints[2]} ${file_to_append} +append_if_not_empty "mlz_cloudname" ${cloudEndpoints[3]} ${file_to_append} diff --git a/src/scripts/config/config_create.sh b/src/scripts/config/config_create.sh index ab04313fc..df5123f4a 100755 --- a/src/scripts/config/config_create.sh +++ b/src/scripts/config/config_create.sh @@ -38,30 +38,32 @@ tf_name=$(basename "${tf_dir}") # generate names . "${BASH_SOURCE%/*}/generate_names.sh" "${mlz_tf_cfg}" "${tf_sub_id}" "${tf_name}" +echo "INFO: creating resources for ${tf_name} Terraform state..." + # create TF Resource Group and Storage Account for Terraform State files -echo "Validating Resource Group for Terraform state..." +echo "INFO: sourcing resource group ${tf_rg_name} for Terraform state..." rg_exists="az group show \ --name ${tf_rg_name} \ --subscription ${tf_sub_id}" if ! $rg_exists &> /dev/null; then - echo "Resource Group does not exist...creating resource group ${tf_rg_name}" + echo "INFO: creating resource group ${tf_rg_name}..." az group create \ --subscription "${tf_sub_id}" \ --location "${mlz_config_location}" \ --name "${tf_rg_name}" \ + --tags "DeploymentName=${mlz_env_name}" \ --output none -else - echo "Resource Group already exists...getting resource group" + echo "INFO: resource group ${tf_rg_name} created!" fi -echo "Validating Storage Account for Terraform state..." +echo "INFO: sourcing storage account ${tf_sa_name} for Terraform state..." sa_exists="az storage account show \ --name ${tf_sa_name} \ --subscription ${tf_sub_id}" if ! $sa_exists &> /dev/null; then - echo "Storage Account does not exist...creating storage account ${tf_sa_name}" + echo "INFO: creating storage account ${tf_sa_name}..." az storage account create \ --name "${tf_sa_name}" \ --subscription "${tf_sub_id}" \ @@ -84,9 +86,7 @@ if ! $sa_exists &> /dev/null; then --account-name "${tf_sa_name}" \ --account-key "${sa_key}" \ --output none - echo "Storage account and container for Terraform state created!" -else - echo "Storage Account already exists" + echo "INFO: storage account ${tf_sa_name} and container ${container_name} created!" fi # generate a config.vars file @@ -95,3 +95,5 @@ fi "${tf_sub_id}" \ "${tf_name}" \ "${tf_dir}" + +echo "INFO: Terraform state resources for ${tf_name} created!" diff --git a/src/scripts/config/generate_config_file.sh b/src/scripts/config/generate_config_file.sh index 1fd0edb85..491b4f558 100755 --- a/src/scripts/config/generate_config_file.sh +++ b/src/scripts/config/generate_config_file.sh @@ -3,6 +3,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. # +# shellcheck disable=1083,1090,2154 +# # Generate a configuration file for MLZ prerequisites and optional SACA and T0-T2 subscriptions. set -e @@ -21,7 +23,6 @@ show_help() { print_formatted "argument" "" "description" print_formatted "--file" "-f" "the destination file path and name (e.g. 'src/mlz_tf_cfg.var')" print_formatted "--tf-env" "-e" "Terraform azurerm environment (e.g. 'public') see: https://www.terraform.io/docs/language/settings/backends/azurerm.html#environment" - print_formatted "--metadatahost" "-m" "Azure Metadata Service endpoint. (e.g 'management.azure.com' or 'management.usgovcloudapi.net')" print_formatted "--mlz-env-name" "-z" "Unique name for MLZ environment" print_formatted "--location" "-l" "The location that you're deploying to (e.g. 'eastus')" print_formatted "--config-sub-id" "-s" "Subscription ID for MissionLZ configuration resources" @@ -40,7 +41,6 @@ usage() { # stage required parameters as not set dest_file="notset" tf_environment="notset" -metadatahost="notset" mlz_env_name="notset" mlz_config_location="notset" mlz_config_subid="notset" @@ -51,7 +51,6 @@ while [ $# -gt 0 ] ; do case $1 in -f | --file) dest_file="$2" ;; -e | --tf-env) tf_environment="$2" ;; - -m | --metadatahost) metadatahost="$2" ;; -z | --mlz-env-name) mlz_env_name="$2" ;; -l | --location) mlz_config_location="$2" ;; -s | --config-sub-id) mlz_config_subid="$2" ;; @@ -65,10 +64,10 @@ while [ $# -gt 0 ] ; do done # check mandatory parameters -for i in { $dest_file $tf_environment $metadatahost $mlz_env_name $mlz_config_location $mlz_config_subid $mlz_tenant_id } +for i in { $dest_file $tf_environment $mlz_env_name $mlz_config_location $mlz_config_subid $mlz_tenant_id } do if [[ $i == "notset" ]]; then - error_log "ERROR: Missing required arguments. These arguments are mandatory: -f, -e, -m, -z, -l, -s, -t" + error_log "ERROR: Missing required arguments. These arguments are mandatory: -f, -e, -z, -l, -s, -t" usage exit 1 fi @@ -76,10 +75,11 @@ done # write the file to the desired path rm -f "$dest_file" +dest_file_dir=$(dirname "${dest_file}") +mkdir -p "${dest_file_dir}" touch "$dest_file" { echo "tf_environment=${tf_environment}" - echo "mlz_metadatahost=${metadatahost}" echo "mlz_env_name=${mlz_env_name}" echo "mlz_config_location=${mlz_config_location}" echo "mlz_config_subid=${mlz_config_subid}" @@ -93,9 +93,9 @@ append_optional_args() { default_value=$3 file_to_append=$4 if [[ $key_value ]]; then - printf "${key_name}=${key_value}\n" >> "${file_to_append}" + printf "%s=%s\n" "${key_name}" "${key_value}" >> "${file_to_append}" else - printf "${key_name}=${default_value}\n" >> "${file_to_append}" + printf "%s=%s\n" "${key_name}" "${default_value}" >> "${file_to_append}" fi } append_optional_args "mlz_saca_subid" "${mlz_saca_subid}" "${mlz_config_subid}" "${dest_file}" @@ -105,4 +105,4 @@ append_optional_args "mlz_tier2_subid" "${mlz_tier2_subid}" "${mlz_config_subid} # append cloud specific endpoints this_script_path=$(realpath "${BASH_SOURCE%/*}") -. "${this_script_path}/append_prereq_endpoints.sh" ${dest_file} +. "${this_script_path}/append_prereq_endpoints.sh" "${dest_file}" diff --git a/src/scripts/config/generate_vars.sh b/src/scripts/config/generate_vars.sh index 80bdd236d..ec9b72e0c 100755 --- a/src/scripts/config/generate_vars.sh +++ b/src/scripts/config/generate_vars.sh @@ -30,6 +30,9 @@ tf_sub_id=${2} tf_name=${3} tf_dir=$(realpath "${4}") +# source mlz config +. "${mlz_tf_cfg}" + # generate names . "${BASH_SOURCE%/*}/generate_names.sh" "${mlz_tf_cfg}" "${tf_sub_id}" "${tf_name}" @@ -38,6 +41,7 @@ config_vars="${tf_dir}/config.vars" rm -f "$config_vars" touch "$config_vars" { + echo "metadata_host=${mlz_metadatahost}" echo "tenant_id=${mlz_tenantid}" echo "mlz_env_name=${mlz_env_name}" echo "mlz_cfg_sub_id=${mlz_config_subid}" diff --git a/src/scripts/config/mlz_config_create.sh b/src/scripts/config/mlz_config_create.sh index eb7a28e64..6ad605a92 100755 --- a/src/scripts/config/mlz_config_create.sh +++ b/src/scripts/config/mlz_config_create.sh @@ -36,20 +36,20 @@ sp_exists () { max_wait_in_seconds=180 max_retries=$((max_wait_in_seconds/sleep_time_in_seconds)) - echo "Maximum time to wait in seconds = ${max_wait_in_seconds}" - echo "Maximum number of retries = ${max_retries}" - + echo "INFO: maximum time to wait in seconds = ${max_wait_in_seconds}" + echo "INFO: maximum number of retries = ${max_retries}" + count=1 while ! $sp_query &> /dev/null do - echo "Waiting for Service Principal ${sp_property} to complete provisioning (${count}/${max_retries})" - echo "Trying again in ${sleep_time_in_seconds} seconds..." + echo "INFO: waiting for service principal ${sp_name} to populate property ${sp_property} (${count}/${max_retries})" + echo "INFO: trying again in ${sleep_time_in_seconds} seconds..." sleep "${sleep_time_in_seconds}" if [[ ${count} -eq max_retries ]]; then - echo "Provisioning the Service Principal ${sp_property} has exceeded ${max_wait_in_minutes} minutes. Investigate and re-run script." + error_log "ERROR: unable to determine ${sp_property} for the service principal ${sp_property} in ${max_wait_in_minutes} minutes. Investigate and re-run script." exit 1 fi @@ -82,6 +82,8 @@ subs=() # generate MLZ configuration names . "${BASH_SOURCE%/*}/generate_names.sh" "${mlz_tf_cfg}" +echo "INFO: creating MLZ resources for ${mlz_env_name}..." + for mlz_sub in $mlz_subs do # Grab value of variable @@ -93,9 +95,9 @@ done # Create Azure AD application registration and Service Principal # TODO: Lift the subscription scoping out of here and move into conditional -echo "Verifying Service Principal is unique (${mlz_sp_name})" +echo "INFO: verifying service principal ${mlz_sp_name} is unique..." if [[ -z $(az ad sp list --filter "displayName eq '${mlz_sp_name}'" --query "[].displayName" -o tsv) ]];then - echo "Service Principal does not exist...creating" + echo "INFO: creating service principal ${mlz_sp_name}..." sp_pwd=$(az ad sp create-for-rbac \ --name "http://${mlz_sp_name}" \ --skip-assignment true \ @@ -128,7 +130,7 @@ if [[ -z $(az ad sp list --filter "displayName eq '${mlz_sp_name}'" --query "[]. # Assign Contributor role to Service Principal for sub in "${subs[@]}" do - echo "Setting Contributor role assignment for ${mlz_sp_name} on subscription ID: ${sub}" + echo "INFO: setting Contributor role assignment for ${mlz_sp_name} on subscription ${sub}..." az role assignment create \ --role Contributor \ --assignee-object-id "${sp_objid}" \ @@ -137,7 +139,7 @@ if [[ -z $(az ad sp list --filter "displayName eq '${mlz_sp_name}'" --query "[]. --output none done else - error_log "Service Principal named ${mlz_sp_name} already exists. This must be a unique Service Principal for your use only. Try again with a new enclave name. Exiting script." + error_log "ERROR: A service principal named ${mlz_sp_name} already exists. This must be a unique service principal for your use only. Try again with a new mlz-env-name. Exiting script." exit 1 fi @@ -146,15 +148,15 @@ rg_exists="az group show \ --name ${mlz_rg_name} \ --subscription ${mlz_config_subid}" +echo "INFO: sourcing resource group ${mlz_rg_name} for MLZ resources..." if ! $rg_exists &> /dev/null; then - echo "Resource Group does not exist...creating resource group ${mlz_rg_name}" + echo "INFO: creating resource group ${mlz_rg_name} for MLZ resources..." az group create \ --subscription "${mlz_config_subid}" \ --location "${mlz_config_location}" \ --name "${mlz_rg_name}" \ + --tags "DeploymentName=${mlz_env_name}" \ --output none -else - echo "Resource Group already exists...getting resource group" fi # Create Key Vault @@ -162,19 +164,19 @@ kv_exists="az keyvault show \ --name ${mlz_kv_name} \ --subscription ${mlz_config_subid}" +echo "INFO: sourcing keyvault ${mlz_kv_name} for MLZ resources..." if ! $kv_exists &> /dev/null; then - echo "Key Vault ${mlz_kv_name} does not exist...creating Key Vault" + echo "INFO: creating keyvault ${mlz_kv_name} for MLZ resources..." az keyvault create \ --name "${mlz_kv_name}" \ --subscription "${mlz_config_subid}" \ --resource-group "${mlz_rg_name}" \ --location "${mlz_config_location}" \ --output none - echo "Key Vault ${mlz_kv_name} created!" fi # Create Key Vault Access Policy for Service Principal -echo "Setting Access Policy for Service Principal..." +echo "INFO: setting access policy on ${mlz_kv_name} for service principal ${mlz_sp_name}..." az keyvault set-policy \ --name "${mlz_kv_name}" \ --subscription "${mlz_config_subid}" \ @@ -182,10 +184,9 @@ az keyvault set-policy \ --object-id "${sp_objid}" \ --secret-permissions get list set \ --output none -echo "Access Policy for Service Principal set!" # Set Key Vault Secrets -echo "Updating KeyVault with Service Principal secrets..." +echo "INFO: setting secrets in ${mlz_kv_name} for service principal ${mlz_sp_name}..." az keyvault secret set \ --name "${mlz_sp_kv_password}" \ --subscription "${mlz_config_subid}" \ @@ -199,4 +200,5 @@ az keyvault secret set \ --vault-name "${mlz_kv_name}" \ --value "${sp_clientid}" \ --output none -echo "KeyVault updated with Service Principal secrets!" + +echo "INFO: MLZ resources for ${mlz_env_name} created!" diff --git a/src/scripts/destroy_terraform.sh b/src/scripts/destroy_terraform.sh index e3a568e65..f92b7541a 100755 --- a/src/scripts/destroy_terraform.sh +++ b/src/scripts/destroy_terraform.sh @@ -13,8 +13,6 @@ set -e -PGM=$(basename "${0}") - if [[ "$#" -lt 2 ]]; then echo "destroy_terraform.sh: initializes Terraform for a given directory using given a .env file for backend configuration" echo "usage: destroy_terraform.sh " @@ -55,6 +53,7 @@ key="${mlz_env_name}${tf_name}" # initialize terraform in the configuration directory cd "${tf_dir}" || exit terraform init \ + -backend-config "metadata_host=${metadata_host}" \ -backend-config "key=${key}" \ -backend-config "resource_group_name=${tf_be_rg_name}" \ -backend-config "storage_account_name=${tf_be_sa_name}" \ diff --git a/src/scripts/init_terraform.sh b/src/scripts/init_terraform.sh index 2abbba346..b6f6e0391 100755 --- a/src/scripts/init_terraform.sh +++ b/src/scripts/init_terraform.sh @@ -48,6 +48,7 @@ key="${mlz_env_name}${tf_name}" # Initialize terraform in the configuration directory cd "${tf_dir}" || exit terraform init \ + -backend-config "metadata_host=${metadata_host}" \ -backend-config "key=${key}" \ -backend-config "resource_group_name=${tf_be_rg_name}" \ -backend-config "storage_account_name=${tf_be_sa_name}" \ diff --git a/src/scripts/terraform/create_globals_from_config.sh b/src/scripts/terraform/create_globals_from_config.sh new file mode 100755 index 000000000..c2e4332c8 --- /dev/null +++ b/src/scripts/terraform/create_globals_from_config.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +# +# shellcheck disable=1090,2154 +# SC1090: Can't follow non-constant source. This file is input. +# SC2154: "var is referenced but not assigned". These values come from an external file. +# +# generate a terraform globals tfvars file given an MLZ config and a desired tfvars file name + +set -e + +error_log() { + echo "${1}" 1>&2; +} + +usage() { + echo "create_globals_from_config.sh: generate a terraform tfvars file given an MLZ config and a desired tfvars file name" + echo "create_globals_from_config.sh: " + show_help +} + +if [[ "$#" -lt 2 ]]; then + usage + exit 1 +fi + +file_to_create=$1 +mlz_config=$2 + +# source config +. "${mlz_config}" + +# given a key and value write a key="value" new line to a file +append_kvp() { + key=$1 + value=$2 + printf "%s=\"%s\"\n" "${key}" "${value}" >> "${file_to_create}" +} + +# write the file to the desired path +rm -f "$file_to_create" +dest_file_dir=$(dirname "${file_to_create}") +mkdir -p "${dest_file_dir}" +touch "$file_to_create" + +append_kvp "deploymentname" "${mlz_env_name}" + +append_kvp "tf_environment" "${tf_environment}" + +append_kvp "mlz_cloud" "${mlz_cloudname}" +append_kvp "mlz_tenantid" "${mlz_tenantid}" +append_kvp "mlz_location" "${mlz_config_location}" +append_kvp "mlz_metadatahost" "${mlz_metadatahost}" + +append_kvp "tier0_subid" "${mlz_tier0_subid}" +append_kvp "tier0_rgname" "rg-t0-${mlz_env_name}" +append_kvp "tier0_vnetname" "vn-t0-${mlz_env_name}" + +append_kvp "tier1_subid" "${mlz_tier1_subid}" +append_kvp "tier1_rgname" "rg-t1-${mlz_env_name}" +append_kvp "tier1_vnetname" "vn-t1-${mlz_env_name}" + +append_kvp "tier2_subid" "${mlz_tier2_subid}" +append_kvp "tier2_rgname" "rg-t2-${mlz_env_name}" +append_kvp "tier2_vnetname" "vn-t2-${mlz_env_name}" + +append_kvp "saca_subid" "${mlz_saca_subid}" +append_kvp "saca_rgname" "rg-saca-${mlz_env_name}" +append_kvp "saca_vnetname" "vn-saca-${mlz_env_name}" +append_kvp "saca_fwname" "Firewall${mlz_env_name}" +append_kvp "saca_lawsname" "laws-${mlz_env_name}"