diff --git a/.ci/exec_functions.sh b/.ci/exec_functions.sh new file mode 100755 index 0000000..6470b54 --- /dev/null +++ b/.ci/exec_functions.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# shellcheck shell=bash disable=SC1094,SC1090,SC1091,SC2044 + +set -e +set -o pipefail + +scripts_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +for f in $(find "${scripts_path}" -type f -name 'functions_*.sh'); do + source "${f}" +done + +"$@" diff --git a/.ci/functions.sh b/.ci/functions.sh deleted file mode 100644 index fa3e13f..0000000 --- a/.ci/functions.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# shellcheck disable=SC2061,SC2035 - -function format_shell() { - find . -type f -name "*.sh" -exec shfmt -l -w {} + -} - -function format_tofu() { - tofu fmt --recursive -} - -function lint_shell() { - find . -type f -name "*.sh" -exec shfmt -l -d {} + - find . -type f -name "*.sh" -exec shellcheck -x {} + -} - -function lint_tofu() { - local tf_projs - readarray -t tf_projs < <(find ./examples -mindepth 1 -maxdepth 1 -type d) - - for tf_proj in "${tf_projs[@]}"; do - if [[ -n "$(find ./"${tf_proj}" -name *.tf)" ]]; then - pushd "${tf_proj}" >/dev/null || return - tofu init -backend=false >/dev/null 2>/dev/null || (popd >/dev/null || return) - tofu validate || (popd >/dev/null || return) - popd >/dev/null || return - fi - done -} diff --git a/.ci/functions_az.sh b/.ci/functions_az.sh new file mode 100644 index 0000000..8f21703 --- /dev/null +++ b/.ci/functions_az.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# shellcheck shell=bash disable=SC1090,SC1091,SC2015 + +function azure_cli_login() { + local az_client_id="${1}" + local az_client_secret="${2}" + local az_tenant_id="${3}" + + az login \ + --service-principal \ + --username "${az_client_id}" \ + --password "${az_client_secret}" \ + --tenant "${az_tenant_id}" +} + +function get_az_ad_application_name() { + if [[ ! -e ./terraform.auto.tfvars.enc.json ]]; then + echo "Unexpectedly missing terraform.auto.tfvars.enc.json file" >/dev/stderr + exit 1 + fi + jq -r '.azure.az_ad_application_name' /dev/stderr + echo "Ensure that the terraform.auto.tfvars.enc.json file has been decrypted" + exit 1 + fi + jq -r .azure.client_secret_enc >/dev/stderr + exit 1 + fi + + if [[ -z "${subscription}" ]]; then + echo "variable subscription must be set" >>/dev/stderr + exit 1 + fi + + az storage account generate-sas \ + --expiry "$(date -d "+1 days" +%Y-%m-%d)"'T00:00:00Z' \ + --permissions "acdlpruw" \ + --resource-types "co" \ + --services "b" \ + --account-name "${storage_account_name}" \ + --https-only \ + --subscription "${subscription}" \ + -o tsv +} + +function get_sas_token() { + local tf_decrypted_file="${1}" + local subscription_id + + if [[ -z "${tf_decrypted_file}" ]]; then + tf_decrypted_file=terraform.auto.tfvars.json + fi + + subscription_id="$(jq -r .azure.subscription_id <"${tf_decrypted_file}")" + storage_account_name="$(jq -r .terraform_backend.storage_account_name <"${tf_decrypted_file}")" + + generate_sas_key "${subscription_id}" "${storage_account_name}" +} diff --git a/.ci/functions_lint.sh b/.ci/functions_lint.sh new file mode 100644 index 0000000..3b0d310 --- /dev/null +++ b/.ci/functions_lint.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2061,SC2035,SC2044 + +function format_shell() { + for f in $( + find . -type f -name "*.sh" \ + -not \( -path "*/node_modules/*" -prune \) \ + -not \( -path "*/.terraform/*" -prune \) + ); do + shfmt -l -w "${f}" + done +} + +function format_markdown() { + for f in $( + find . -type f -name "*.md" \ + -not \( -path "*/node_modules/*" -prune \) \ + -not \( -path "*/.terraform/*" -prune \) + ); do + yarn prettier --write "${f}" + done +} + +function format_tofu() { + tofu fmt --recursive +} + +function format_all() { + format_markdown + format_shell + format_tofu +} + +function lint_markdown() { + for f in $( + find . -type f -name "*.md" \ + -not \( -path "*/node_modules/**" -prune \) \ + -not \( -path "*/.terraform/**" -prune \) + ); do + yarn markdownlint-cli2 "${f}" "#node_modules" + done + for f in $( + find . -type f -name "*.md" \ + -not \( -path "*/node_modules/**" -prune \) \ + -not \( -path "*/.terraform/**" -prune \) + ); do + yarn prettier --check "${f}" + done +} + +function lint_shell() { + find . -type f -name "*.sh" -exec shfmt -l -d {} + + find . -type f -name "*.sh" -exec shellcheck -x {} + +} + +function lint_tofu() { + local tf_projs + readarray -t tf_projs < <(find ./examples -mindepth 1 -maxdepth 1 -type d) + + for tf_proj in "${tf_projs[@]}"; do + if [[ -n "$(find ./"${tf_proj}" -name *.tf)" ]]; then + pushd "${tf_proj}" >/dev/null || return + if [[ -d .terraform ]]; then rm -rf .terraform; fi + if [[ -e .terraform.lock.hcl ]]; then rm .terraform.lock.hcl; fi + tofu init -backend=false >/dev/null || (popd >/dev/null || return) + tofu validate || (popd >/dev/null || return) + popd >/dev/null || return + fi + done +} diff --git a/.ci/functions_tf.sh b/.ci/functions_tf.sh new file mode 100644 index 0000000..bd004fc --- /dev/null +++ b/.ci/functions_tf.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +# shellcheck shell=bash disable=SC1090,SC1091,SC2015 + +function get_tf_backend_az_resource_group() { + local tf_decrypted_file="${1}" + + if [[ -z "${tf_decrypted_file}" ]]; then + tf_decrypted_file=terraform.auto.tfvars.json + fi + + jq -r .terraform_backend.storage_account_resource_group <"${tf_decrypted_file}" +} + +function get_tf_backend_az_storage_account_name() { + local tf_decrypted_file="${1}" + + if [[ -z "${tf_decrypted_file}" ]]; then + tf_decrypted_file=terraform.auto.tfvars.json + fi + + jq -r .terraform_backend.storage_account_name <"${tf_decrypted_file}" +} + +function get_tf_backend_az_storage_container_name() { + local tf_decrypted_file="${1}" + + if [[ -z "${tf_decrypted_file}" ]]; then + tf_decrypted_file=terraform.auto.tfvars.json + fi + + jq -r .terraform_backend.storage_container_name <"${tf_decrypted_file}" +} + +function get_tf_backend_az_storage_container_key() { + local tf_decrypted_file="${1}" + + if [[ -z "${tf_decrypted_file}" ]]; then + tf_decrypted_file=terraform.auto.tfvars.json + fi + + jq -r .terraform_backend.storage_container_key <"${tf_decrypted_file}" +} + +function terraform_decrypt() { + local kv_key="${1}" + local tf_encrypted_file="${2}" + local tf_decrypted_file="${3}" + + if [[ -z "${tf_encrypted_file}" ]]; then + tf_encrypted_file=terraform.auto.tfvars.enc.json + fi + + if [[ -z "${tf_decrypted_file}" ]]; then + tf_decrypted_file=terraform.auto.tfvars.json + fi + + if [[ -z "${kv_key}" ]]; then + echo "kv_key is required" >/dev/stderr + else + if [[ "${kv_key}" == ".azure.sops_key_vault" ]]; then + kv_key_arg="--azure-kv" + kv_key=.azure.sops_key_vault + else + kv_key_arg="--pgp" + kv_key=.sops_pgp_fingerprint + fi + fi + + if [[ -f "${tf_encrypted_file}" ]] && [[ -s "${tf_encrypted_file}" ]]; then + echo "Decrypting ${tf_encrypted_file}" + sops \ + --verbose \ + --decrypt \ + --input-type json \ + --output-type json \ + --encrypted-suffix _enc \ + "${kv_key_arg}" "$(jq -r "${kv_key}" "${tf_encrypted_file}")" \ + "${tf_encrypted_file}" | jq --sort-keys . >"${tf_decrypted_file}" + + echo "${PWD}/${tf_encrypted_file} has been decrypted to ${PWD}/${tf_decrypted_file}" + else + echo "Skipping decryption, ${tf_encrypted_file} does not exist" + fi + +} + +function terraform_encrypt() { + local kv_key="${1}" + local tf_encrypted_file="${2}" + local tf_decrypted_file="${3}" + local kv_key_arg + + if [[ -z "${tf_encrypted_file}" ]]; then + tf_encrypted_file=terraform.auto.tfvars.enc.json + fi + + if [[ -z "${tf_decrypted_file}" ]]; then + tf_decrypted_file=terraform.auto.tfvars.json + fi + + if [[ -z "${kv_key}" ]]; then + echo "kv_key is required" >/dev/stderr + elif [[ "${kv_key}" == ".azure.sops_key_vault" ]]; then + kv_key_arg="--azure-kv" + elif [[ "${kv_key}" == ".sops_pgp_fingerprint" ]]; then + kv_key_arg="--pgp" + else + echo "Unexpected kv_key variable ${kv_key}" + fi + + # shellcheck disable=SC2094 + if [[ -f "${tf_decrypted_file}" ]] && [[ -s "${tf_decrypted_file}" ]]; then + echo "Encrypting ${tf_decrypted_file}" + sops \ + --verbose \ + --encrypt \ + --input-type json \ + --output-type json \ + --encrypted-suffix "_enc" \ + "${kv_key_arg}" "$(jq -r "${kv_key}" "${tf_decrypted_file}")" \ + "${tf_decrypted_file}" >"${tf_encrypted_file}" + echo "${PWD}/${tf_encrypted_file} has been updated" + else + echo "Skipping encryption, ${tf_decrypted_file} does not exist" + fi +} + +function tf_init_az_backend() { + local storage_account_resource_group="${1}" + local storage_account_name="${2}" + local storage_container_name="${3}" + local storage_container_key="${4}" + local sas_token="${5}" + local additional_args="${6}" + + if [[ -n "${additional_args}" ]]; then + tofu init \ + -force-copy \ + -input=false \ + -reconfigure \ + -backend-config "resource_group_name=${storage_account_resource_group}" \ + -backend-config "storage_account_name=${storage_account_name}" \ + -backend-config "container_name=${storage_container_name}" \ + -backend-config "key=${storage_container_key}" \ + -backend-config "sas_token=${sas_token}" \ + "${additional_args}" + else + tofu init \ + -force-copy \ + -input=false \ + -reconfigure \ + -backend-config "resource_group_name=${storage_account_resource_group}" \ + -backend-config "storage_account_name=${storage_account_name}" \ + -backend-config "container_name=${storage_container_name}" \ + -backend-config "key=${storage_container_key}" \ + -backend-config "sas_token=${sas_token}" + fi +} + +function tf_upgrade_az_backend() { + tf_init_az_backend "${1}" "${2}" "${3}" "${4}" "${5}" "-upgrade=true" +} + +function tf_init_local_backend() { + local storage_account_resource_group="${1}" + local storage_account_name="${2}" + local storage_container_name="${3}" + local storage_container_key="${4}" + local sas_token="${5}" + local additional_args="${6}" + + if [[ -n "${additional_args}" ]]; then + tofu init \ + -force-copy \ + -input=false \ + -reconfigure \ + -backend-config "resource_group_name=${storage_account_resource_group}" \ + -backend-config "storage_account_name=${storage_account_name}" \ + -backend-config "container_name=${storage_container_name}" \ + -backend-config "key=${storage_container_key}" \ + -backend-config "sas_token=${sas_token}" \ + "${additional_args}" + else + tofu init \ + -force-copy \ + -input=false \ + -reconfigure \ + -backend-config "resource_group_name=${storage_account_resource_group}" \ + -backend-config "storage_account_name=${storage_account_name}" \ + -backend-config "container_name=${storage_container_name}" \ + -backend-config "key=${storage_container_key}" \ + -backend-config "sas_token=${sas_token}" + fi +} + +function tf_upgrade_local_backend() { + tf_init_az_backend "${1}" "${2}" "${3}" "${4}" "${5}" "-upgrade=true" +} + +function tf_plan() { + local target_resource + target_resource="${1:-}" + + if [[ -z "${target_resource}" ]]; then + tofu plan + else + tofu plan -target="${target_resource}" + fi +} + +function tf_apply() { + local target_resource + target_resource="${1:-}" + + if [[ -z "${target_resource}" ]]; then + tofu apply --auto-approve + else + tofu apply -target="${target_resource}" --auto-approve + fi +} + +function tf_destroy() { + local target_resource + target_resource="${1:-}" + + if [[ -z "${target_resource}" ]]; then + tofu destroy + else + tofu destroy -target="${target_resource}" + fi +} diff --git a/.ci/source_functions.sh b/.ci/source_functions.sh deleted file mode 100755 index cc130ae..0000000 --- a/.ci/source_functions.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -# shellcheck shell=bash disable=SC1094,SC1090,SC1091 - -set -e -set -o pipefail - -source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/functions.sh" -"$@" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..149aa52 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_size = 2 +indent_style = space +trim_trailing_whitespace = true + +[Dockerfile] +indent_size = 4 + +[*.{sh,Dockerfile}] +indent_size = 4 + +[Makefile] +indent_style = tab +indent_size = 4 + +[*.md] +max_line_length = off diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..60122c1 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,9 @@ +{ + "line-length": false, + "no-trailing-spaces": false, + "MD025": false, + "MD033": { + "allowed_elements": ["source", "video", "br"] + }, + "MD046": false +} diff --git a/Makefile b/Makefile index 5e6abf0..f3d2e0d 100644 --- a/Makefile +++ b/Makefile @@ -5,9 +5,12 @@ SHELL := /bin/bash lint format: - ./.ci/source_functions.sh format_shell - ./.ci/source_functions.sh format_tofu + ./.ci/exec_functions.sh format_markdown + ./.ci/exec_functions.sh format_shell + ./.ci/exec_functions.sh format_tofu lint: - ./.ci/source_functions.sh lint_shell - ./.ci/source_functions.sh lint_tofu + ./.ci/exec_functions.sh lint_markdown + ./.ci/exec_functions.sh lint_shell + ./.ci/exec_functions.sh lint_tofu + pushd examples/az-resource-groups; make init_local_backend; popd diff --git a/README.md b/README.md index c3a0ed6..568f993 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,43 @@ these modules are superficial. Others form a meaningful a group of tofu resources and datasources into a logical and coherent grouping. -## Examples +## Install Dependencies + +Install whichever dependencies are missing for the CI process and using opentofu. +Installation instructions below are targetting ubuntu 22.04. + +```bash +# Install asdf to install npm modules +wget -q https://raw.githubusercontent.com/stephenmoloney/localbox/master/bin/install/asdf.sh +chmod +x ./asdf.sh && ./asdf.sh +export ASDF_DIR="${HOME}/.asdf" && source "${HOME}/.asdf/asdf.sh" +asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git +asdf install nodejs 16.14.2 +asdf global nodejs 16.14.2 +asdf plugin add yarn +asdf install yarn 1.22.18 +asdf global yarn 1.22.18 + +# Install npm modules +yarn install --frozen-lockfile --ignore-optional + +# Install opentofu +asdf plugin add opentofu https://github.com/virtualroot/asdf-opentofu.git +asdf install opentofu latest +asdf global opentofu latest +``` + +## CI Tasks + +| Command | Description | +| ------------- | ------------------- | +| `make format` | Format the codebase | +| `make lint` | Lint the codebase | + +## Examples using the modules See `./examples` ## Licence [MIT](https://choosealicense.com/licenses/mit/) - diff --git a/docs/creating_az_tf_backend.md b/docs/creating_az_tf_backend.md new file mode 100644 index 0000000..bbe9cd8 --- /dev/null +++ b/docs/creating_az_tf_backend.md @@ -0,0 +1,215 @@ +# Creating an Azure Backend for terraform/opentofu + +![Azure Blob Storage Backend](./diagram.png) + +Terraform/Opentofu are leading tools for building and maintaining infrastructure +as code. However, there is a chicken and egg problem, in order to +have a terraform/opentofu backend, some infrastructure is needed. The backend can be an object +storage backend in AWS, GCP, Azure and many other providers. + +This document describes the process for using Azure. + +The resources required in Azure are + + - A storage account + - A container within the storage account + - A blob (object store) within the container + +Blobs on azure storage accounts are quite feature complete for the purposes of a terraform/opentofu +backend perspective. The locking is builtin. The networking for the container is +configurable which has security benefits. In AWS, to get state locking, a dynamodb +table is needed which means creating an s3 bucket and a dynamodb table. That means more +steps - which is why I prefer to use Azure Blob Storage as my preferred terraform/opentofu backend. + +## Create the standalone azure resources + +A standalone resource group and storage container is created first in order +so that the subsequent resources can be deployed using terraform/opentofu and more highly +automated technologies. + +### Login to azure with the correct subscription + +In this instance, login with a user account. For subsequent resources where there +is greater automation, a service principal or managed identity can be used. + +```bash +az login +``` + +### Select the correct subscription + +```bash +az account list --output table +``` + +For these operations, we will use the `your-subscription` subscription. + +```bash +az account set --subscription "your-subscription" +``` + +### Get a list of possible locations + +```bash +az account list-locations -o table +``` + +### Create the resource group in the desired location + +```bash +az group create \ + --verbose \ + --location northeurope \ + --resource-group your-subscription-standalone-rg \ + --tags \ + 'managed-by=az-cli' \ + 'environment=standalone' \ + 'purpose=tf-backend' \ + 'source=https://github.com/eirenauts/tf-modules/tree/main/docs/creating_az_tf_backend.md' +``` + +### Create the azure storage account + +The purpose of the `your-subscription-standalone-rg` resource group is to +have a azure storage as a once off backend for subsequent terraform/opentofu operations +to create more terraform/opentofu backends. + +```bash +az storage account create \ + --verbose \ + --location northeurope \ + --resource-group your-subscription-standalone-rg \ + --name your-tf-storage-account \ + --sku Standard_LRS \ + --kind BlobStorage \ + --access-tier Cool \ + --https-only true \ + --encryption-services blob \ + --min-tls-version TLS1_2 \ + --allow-blob-public-access false \ + --require-infrastructure-encryption false \ + --default-action Deny \ + --tags \ + 'managed-by=az-cli' \ + 'environment=standalone' \ + 'purpose=tf-backend' \ + 'source=https://github.com/eirenauts/tf-modules/tree/main/docs/creating_az_tf_backend.md' +``` + +### Restrict traffic to the azure storage account + +#### Ip exceptions table + +`/27` corresponds to a block of public IP addresses which you will allow access to the storage container. + +The rationale behind this is to restrict access to the storage backend substantially and +therefore greatly increase the security of the backend. + +| IP | +| -------------------- | +| `/27` | +| `/32` | + +```bash +allowed_ips=( /27 ) +for ip in "${allowed_ips[@]}"; do + az storage account network-rule add \ + --verbose \ + --resource-group your-subscription-standalone-rg \ + --account-name your-tf-storage-account \ + --action Allow \ + --ip-address "${ip}" +done +az storage account show \ + --verbose \ + --resource-group your-subscription-standalone-rg \ + --name your-tf-storage-account \ + --query '{"Storage Account Name":name, "Resource Group":resourceGroup, RuleSet:networkRuleSet}' +``` + +### Create storage containers for the terraform/opentofu backends + +```bash +az_storage_acccount_containers=("examples-az-resource-groups") +for az_storage_acccount_container in "${!az_storage_acccount_containers[@]}"; do + az storage container create \ + --verbose \ + --account-name your-tf-storage-account \ + --resource-group your-subscription-standalone-rg \ + --name "${az_storage_acccount_container}" \ + --public-access "off" \ + --fail-on-exist +done +``` + +## Initialize the terraform/opentofu backend + +### Create the `backend.tf` file in the main module + +```hcl-terraform +terraform { + backend "azurerm" { + } +} +``` + +### Initialize the backend + +```bash +tofu init \ + -backend-config "resource_group_name=your-subscription-standalone-rg" \ + -backend-config "storage_account_name=your-tf-storage-account" \ + -backend-config "container_name=your-tf-container-name" \ + -backend-config "key=default.tfstate" \ + -backend-config "sas_token=your-sas-token" +``` + +| Resource | Notes | +| -------------------- | ----------------------------------------------------------------------------------------- | +| resource_group_name | The azure resource group housing the azure storage account | +| storage_account_name | The azure storage account housing the azure container | +| container_name | The azure container name - housing the blob storage | +| key | The key to access the blob storage. Defaults to default.tfstate | +| sas_token | This key is used by terraform/opentofu to obtain permissions to read/write to the backend | + +### Creating a sas token + +One way to create a sas token is described already at `.ci/functions_az.sh`. + +The code is copied below which you can use to generate a new sas token + +```bash +function generate_sas_key() { + local subscription="${1}" + local storage_account_name="${2}" + + if [[ -z "${storage_account_name}" ]]; then + echo "variable storage_account_name must be set" >>/dev/stderr + exit 1 + fi + + if [[ -z "${subscription}" ]]; then + echo "variable subscription must be set" >>/dev/stderr + exit 1 + fi + + az storage account generate-sas \ + --expiry "$(date -d "+1 days" +%Y-%m-%d)"'T00:00:00Z' \ + --permissions "acdlpruw" \ + --resource-types "co" \ + --services "b" \ + --account-name "${storage_account_name}" \ + --https-only \ + --subscription "${subscription}" \ + -o tsv +} +``` + +## Wrap up + +This concludes the steps to create a terraform backend using azure. And this is the +method I use myself to create storage accounts for terraform backends where there is +a lack of existing infrastructure available as alternatives. + +I would recommend you only use a backend that is highly available and +has terraform state locking compatibility to avoid problems later. diff --git a/docs/diagram.png b/docs/diagram.png new file mode 100644 index 0000000..3746c40 Binary files /dev/null and b/docs/diagram.png differ diff --git a/docs/diagram.svg b/docs/diagram.svg new file mode 100644 index 0000000..0b2f537 --- /dev/null +++ b/docs/diagram.svg @@ -0,0 +1,13 @@ + + + + + + + Azure Storage Account + + + Network rules canbe applied + + + \ No newline at end of file diff --git a/examples/az-resource-groups/Makefile b/examples/az-resource-groups/Makefile new file mode 100644 index 0000000..24be2bf --- /dev/null +++ b/examples/az-resource-groups/Makefile @@ -0,0 +1,61 @@ +SHELL := /bin/bash +GIT_ROOT_DIR := $(shell git rev-parse --show-toplevel) +ENCRYPTED_FILE := terraform.auto.tfvars.enc.json +ENCRYPTED_FILE_PATH := $(CURDIR)/$(ENCRYPTED_FILE) +DECRYPTED_FILE := terraform.auto.tfvars.json +DECRYPTED_FILE_PATH := $(CURDIR)/$(DECRYPTED_FILE) + +.PHONY: \ + sops_decrypt \ + sops_encrypt \ + init_local_backend \ + init_az_backend \ + upgrade_local_backend \ + upgrade_az_backend \ + plan \ + apply + +sops_decrypt: + @echo "Commencing decrpytion of $(ENCRYPTED_FILE_PATH)" + @echo $(GIT_ROOT_DIR) + @$(GIT_ROOT_DIR)/.ci/exec_functions.sh terraform_decrypt ".sops_pgp_fingerprint" + +sops_encrypt: + @echo "Commencing encrpytion of $(CURDIR)/$(ENCRYPTED_FILE)" + @echo $(GIT_ROOT_DIR) + @$(GIT_ROOT_DIR)/.ci/exec_functions.sh terraform_encrypt ".sops_pgp_fingerprint" + +init_local_backend: + rm -rf .terraform + tofu init -cloud=false + +init_az_backend: + rm -rf .terraform + tofu init + @$(GIT_ROOT_DIR)/.ci/exec_functions.sh tf_init_az_backend \ + $$($(GIT_ROOT_DIR)/.ci/exec_functions.sh get_tf_backend_az_resource_group) \ + $$($(GIT_ROOT_DIR)/.ci/exec_functions.sh get_tf_backend_az_storage_account_name) \ + $$($(GIT_ROOT_DIR)/.ci/exec_functions.sh get_tf_backend_az_storage_container_name) \ + $$($(GIT_ROOT_DIR)/.ci/exec_functions.sh get_tf_backend_az_storage_container_key) \ + $$($(GIT_ROOT_DIR)/.ci/exec_functions.sh get_sas_token) + +upgrade_local_backend: + rm -rf .terraform + rm .terraform.lock.hcl + tofu init -cloud=false -upgrade + +upgrade_az_backend: + rm -rf .terraform + rm .terraform.lock.hcl + @$(GIT_ROOT_DIR)/.ci/exec_functions.sh tf_upgrade_az_backend \ + $$($(GIT_ROOT_DIR)/.ci/exec_functions.sh get_tf_backend_az_resource_group) \ + $$($(GIT_ROOT_DIR)/.ci/exec_functions.sh get_tf_backend_az_storage_account_name) \ + $$($(GIT_ROOT_DIR)/.ci/exec_functions.sh get_tf_backend_az_storage_container_name) \ + $$($(GIT_ROOT_DIR)/.ci/exec_functions.sh get_tf_backend_az_storage_container_key) \ + $$($(GIT_ROOT_DIR)/.ci/exec_functions.sh get_sas_token) + +plan: + @$(GIT_ROOT_DIR)/.ci/exec_functions.sh tf_plan + +apply: + @$(GIT_ROOT_DIR)/.ci/exec_functions.sh tf_apply diff --git a/examples/az-resource-groups/README.md b/examples/az-resource-groups/README.md new file mode 100644 index 0000000..d49ecd0 --- /dev/null +++ b/examples/az-resource-groups/README.md @@ -0,0 +1,30 @@ +# az-resource-groups example + +The general steps to deploy the module are outlined below. + +## Create the azure storage container as terraform backend + +The first step is to create a opentofu backend. There are many +providers of object storage solutions that can act as a storage +backend. Initialize the backend. + +To use Azure Blob Storage, checkout either of the resources linked: + +- <../../docs/creating_az_tf_backend.md> +- + +```bash +make init_az_backend +``` + +## Plan the resources + +```bash +make plan +``` + +## Create the resources + +```bash +make apply +``` diff --git a/examples/az-resource-groups/backend.tf b/examples/az-resource-groups/backend.tf new file mode 100644 index 0000000..0aec049 --- /dev/null +++ b/examples/az-resource-groups/backend.tf @@ -0,0 +1,4 @@ +terraform { + backend "azurerm" { + } +} diff --git a/examples/az-resource-groups/main.tf b/examples/az-resource-groups/main.tf new file mode 100644 index 0000000..d816b08 --- /dev/null +++ b/examples/az-resource-groups/main.tf @@ -0,0 +1,11 @@ +// Azure Resources +module "az_resource_groups" { + source = "git::https://github.com/eirenauts/tf-modules.git//az-resource-group?ref=main" + // source = "../../az-resource-group" + + for_each = var.az_resource_groups + az_resource_group = merge( + each.value, + { tags = merge(var.global_tags, each.value.tags) } + ) +} diff --git a/examples/az-resource-groups/providers.tf b/examples/az-resource-groups/providers.tf new file mode 100644 index 0000000..516aa4a --- /dev/null +++ b/examples/az-resource-groups/providers.tf @@ -0,0 +1,13 @@ +provider "azurerm" { + subscription_id = var.azure.subscription_id + tenant_id = var.azure.tenant_id + + features { + key_vault { + purge_soft_delete_on_destroy = true + recover_soft_deleted_key_vaults = true + purge_soft_deleted_secrets_on_destroy = true + recover_soft_deleted_secrets = true + } + } +} diff --git a/examples/az-resource-groups/terraform.auto.tfvars.enc.json b/examples/az-resource-groups/terraform.auto.tfvars.enc.json new file mode 100644 index 0000000..4f3d6e1 --- /dev/null +++ b/examples/az-resource-groups/terraform.auto.tfvars.enc.json @@ -0,0 +1,61 @@ +{ + "global_tags": { + "environment": "test", + "managed-by": "terraform", + "triggered-by": "user-principal-stephen-moloney", + "project": "examples-az-resource-groups", + "purpose": "Demonstrating use of module for creating Azure Resource Groups", + "source-dir": "examples/az-resource-groups", + "source-repo": "https://github.com/eirenauts/tf-modules.git" + }, + "az_resource_groups": { + "tf_modules_az_rg_1": { + "location": "North Europe", + "name": "tf-modules-az-rg-1", + "tags": { + "name": "tf-modules-az-rg-1", + "environment": "dev", + "managed-by": "terraform" + } + }, + "tf_modules_az_rg_2": { + "location": "West Europe", + "name": "tf-modules-az-rg-2", + "tags": { + "name": "tf-modules-az-rg-2", + "managed-by": "terraform" + } + } + }, + "azure": { + "az_ad_application_name": "", + "client_secret_enc": "", + "subscription_id": "", + "tenant_id": "" + }, + "sops_pgp_fingerprint": "21583B31C30CB8BE31418483FC6E9A7735295C2B", + "terraform_backend": { + "storage_account_name": "", + "storage_account_resource_group": "", + "storage_container_key": "default.tfstate", + "storage_container_name": "examples-az-resource-groups" + }, + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": null, + "lastmodified": "2024-03-13T20:29:39Z", + "mac": "ENC[AES256_GCM,data:w0Vi8o2AEBR2ynP+BxagKFp0uamsgd0Y1DwYbiFQt7SR+hhxVjW0iDY9R30brf+A7x2TRUQLUAosaShv8baqRFvTDLYgisujHyzKxaDzxckYxHmgmuWRP7B6JWJpCRpZaHvV+ue6TqvHZlIfk55dYmrrM3ovjm4Q5RPDqPDv51k=,iv:UjGDFA4RbjM8TAGFg32v5QDFsQvIjIbGf/1CKoKp4X8=,tag:3Ys1Ecqlah6maRzx3oNhQA==,type:str]", + "pgp": [ + { + "created_at": "2024-03-13T20:29:39Z", + "enc": "-----BEGIN PGP MESSAGE-----\n\nhQIMAx3f3zFZ2uXWAQ/9GCfP21yJU5FRd6CLAZ46KhbrCwyBOkPD966/5sk3+dlQ\nnAtx09RfBhHlWPEbw5hzKBtF+p7yHhFRUR3PC17wbw1J8q4LcAzZocIBrZo9G3Dj\nTVKR0HMXtkKtfg1rM9U2S0RF4bTwm9mVl1qfe9xRk1bvMbBAqGhHTMMll/UDlCVd\nQqJBYmuENncYugBg5BNtGkNd9fUw+pKU9nmprcL9H7x+JCnzont90qYtFpAV+4J9\npm6e/HRmLC5AL0QWl03ymP/ZSTAnrjeGLPApQRTet+oycG6qfxRnOqVg2IZcfG9D\n21SnmiYBEbC2CAbCdWBgT7Sgf0XKAY876Z8R2OWjVf8/52Zl8kydgIELQ3UDjSqB\nDBhLi0KfY5ArQG4FkLbYkkUwpkaaNYb2ixi+QFl9P9bQsEs+mZ13Bk1Xr+UUhlHm\neSZopdXuj1MTcI0ela1h1gNPjNzIqU7aNVYMtG/7M/Xpm17owlJmecliV3gHi5Dg\npmryL7TtrHjfcmgZhw7G1lxhFmXxUEqnQVaoxdT2uDZeAxyUIg9fstD1Rdj7llXS\ngkPWw2Sp9X1izDomcjmN2L6G1QnDypYzIplBJx2wdWuCXtRsWCJ7U8lxxln6r6yO\nqMwDzgMPu+L4RN3MejgDKevWrJagurzcSGs2rVcQLV1r0ZRfEk1WRjyBX+nTaw7S\nXAFe1OflOBL7tgt8WsrytVdQa1aqwwPsCFa/VO5tosVqePrjVZWsU03X0GJEHvVh\nyhkoOT3byPUEmUqn0x7DmfYi2bkkR6YEKlLPqfyswdhCBKYQ3y20PKR/YCNN\n=FWrm\n-----END PGP MESSAGE-----", + "fp": "21583B31C30CB8BE31418483FC6E9A7735295C2B" + } + ], + "encrypted_suffix": "_enc", + "version": "3.8.1" + } +} \ No newline at end of file diff --git a/examples/az-resource-groups/variables.tf b/examples/az-resource-groups/variables.tf new file mode 100644 index 0000000..42614e0 --- /dev/null +++ b/examples/az-resource-groups/variables.tf @@ -0,0 +1,42 @@ +variable "azure" { + type = object({ + subscription_id = string + tenant_id = string + }) + description = "Credentials for the azure provider" +} + +variable "terraform_backend" { + type = object({ + storage_account_name = string + storage_container_name = string + storage_container_key = string + }) + description = "Settings for the tofu backend" +} + +variable "sops_pgp_fingerprint" { + type = string + description = "fingerprint of the sops key" +} + +variable "global_tags" { + type = object({ + environment = string + managed-by = string + triggered-by = string + project = string + purpose = string + source-dir = string + source-repo = string + }) +} + +variable "az_resource_groups" { + type = map(object({ + name = string + location = string + tags = map(string) + })) + description = "Creates resource groups" +} diff --git a/examples/az-resource-groups/version.tf b/examples/az-resource-groups/version.tf new file mode 100644 index 0000000..a886aaf --- /dev/null +++ b/examples/az-resource-groups/version.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.0, < 2.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "3.84.0" + } + azuread = { + source = "hashicorp/azuread" + version = "2.35.0" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..523d1af --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "author": "Stephen Moloney", + "description": "Some npm modules for ci for tf-modules git repo", + "dependencies": {}, + "devDependencies": { + "markdownlint-cli2": "==0.12.1", + "markdown-it": "==12.3.2", + "markdown-it-anchor": "==8.4.1", + "prettier": "==2.6.1", + "rehype-cli": "==11.0.1", + "rehype-format": "==4.0.1", + "rehype-sanitize": "==5.0.1" + }, + "license": "UNLICENSED", + "name": "tf-modules", + "private": true, + "scripts": {}, + "version": "1.0.0" +}