From a2d20e7e0b1b80618cbc273f5cd3b60ece539511 Mon Sep 17 00:00:00 2001 From: Kevin Pedersen Date: Tue, 9 Apr 2024 16:44:46 -0300 Subject: [PATCH] initial framework, converted for AWS --- Makefile | 57 +++++++ README.md | 51 ++++++ aws/aws.yaml | 2 + aws/aws_gitlab_terragrunt.hcl | 40 +++++ aws/empty.yaml | 1 + aws/gitlab/env.yaml | 23 +++ aws/gitlab/reg-primary/region.yaml | 4 + aws/gitlab/reg-secondary/region.yaml | 4 + aws/gitlab/scripts/configure.sh | 82 +++++++++ aws/gitlab/scripts/prune_caches.sh | 32 ++++ aws/gitlab/templates/env.tpl | 12 ++ aws/gitlab/templates/region.tpl | 4 + aws/gitlab/test/aws_gitlab_test.go | 241 +++++++++++++++++++++++++++ aws/gitlab/versions.yaml | 9 + docs/decisions/adr_0001.md | 15 ++ docs/decisions/adr_0002.md | 18 ++ docs/decisions/adr_template.md | 15 ++ docs/decisions/index.md | 6 + modules/.gitkeep | 0 scripts/install_terraform.sh | 72 ++++++++ scripts/install_terragrunt.sh | 72 ++++++++ scripts/prune_terragrunt_cache.sh | 32 ++++ 22 files changed, 792 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 aws/aws.yaml create mode 100644 aws/aws_gitlab_terragrunt.hcl create mode 100644 aws/empty.yaml create mode 100644 aws/gitlab/env.yaml create mode 100644 aws/gitlab/reg-primary/region.yaml create mode 100644 aws/gitlab/reg-secondary/region.yaml create mode 100755 aws/gitlab/scripts/configure.sh create mode 100755 aws/gitlab/scripts/prune_caches.sh create mode 100644 aws/gitlab/templates/env.tpl create mode 100644 aws/gitlab/templates/region.tpl create mode 100644 aws/gitlab/test/aws_gitlab_test.go create mode 100644 aws/gitlab/versions.yaml create mode 100644 docs/decisions/adr_0001.md create mode 100644 docs/decisions/adr_0002.md create mode 100644 docs/decisions/adr_template.md create mode 100644 docs/decisions/index.md create mode 100644 modules/.gitkeep create mode 100755 scripts/install_terraform.sh create mode 100755 scripts/install_terragrunt.sh create mode 100755 scripts/prune_terragrunt_cache.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2a507de --- /dev/null +++ b/Makefile @@ -0,0 +1,57 @@ +.PHONY: help +help: + @echo 'make ' + @echo '' + @echo 'Targets:' + @echo ' help Show this help' + @echo ' pre-commit Run pre-commit checks' + @echo '' + @echo ' aws_gitlab_clean Clean up state files' + @echo ' aws_gitlab_configure Configure the deployment' + @echo ' aws_gitlab_deploy Deploy configured resources' + @echo ' aws_gitlab_init Initialize modules, providers' + @echo ' aws_gitlab_install Install Terraform, Terragrunt' + @echo ' aws_gitlab_lint Run Go linters' + @echo ' aws_gitlab_plan Show deployment plan' + @echo ' aws_gitlab_test Run deployment tests' + @echo '' + +.PHONY: pre-commit +pre-commit: + @pre-commit run -a + +.PHONY: aws_gitlab_clean +aws_gitlab_clean: + @cd aws/gitlab && chmod +x ./scripts/prune_caches.sh && ./scripts/prune_caches.sh . + @cd aws/gitlab/test && rm -f go.mod go.sum + +.PHONY: aws_gitlab_configure +aws_gitlab_configure: + @cd aws/gitlab && ./scripts/configure.sh -e dev -o kpeder -p us-east-1 -s us-west-1 -t devops + +.PHONY: aws_gitlab_deploy +aws_gitlab_deploy: aws_gitlab_configure aws_gitlab_init + @cd aws/gitlab/test && go test -v + +.PHONY: aws_gitlab_init +aws_gitlab_init: aws_gitlab_configure + @cd aws/gitlab && terragrunt run-all init + @cd aws/gitlab/test && go mod init aws_gitlab_test.go; go mod tidy + +.PHONY: aws_gitlab_install +aws_gitlab_install: + @chmod +x ./scripts/*.sh + @sudo ./scripts/install_terraform.sh -v ./aws/gitlab/versions.yaml + @sudo ./scripts/install_terragrunt.sh -v ./aws/gitlab/versions.yaml + +.PHONY: aws_gitlab_lint +aws_gitlab_lint: aws_gitlab_configure aws_gitlab_init + @cd aws/gitlab/test && golangci-lint run --print-linter-name --verbose aws_gitlab_test.go + +.PHONY: aws_gitlab_plan +aws_gitlab_plan: aws_gitlab_configure aws_gitlab_init + @cd aws/gitlab && terragrunt run-all plan + +.PHONY: aws_gitlab_test +aws_gitlab_test: aws_gitlab_configure aws_gitlab_lint + @cd aws/gitlab/test && go test -v -destroy diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc54624 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +## Terragrunt Deployment Example +Example Terragrunt deployment that includes Go test routines using Terratest. + +### Decision Records +This repository uses architecture decision records to record design decisions about important elements of the solution. + +The ADR index is available [here](./docs/decisions/index.md). + +### Requirements +Tested on Go version 1.21 on Ubuntu Linux. + +Uses installed packages: +``` +awscli +golangci-lint +make +pre-commit +terraform +terragrunt +``` + +### Configuration +1. Install the packages listed above. +1. Make a copy of the aws/aws.yaml file, named local.aws.yaml, and fill in the fields with configuration values for the target platform. +1. Configure an AWS access key for Terraform provider to use to access the platform: + ``` + $ aws configure + ``` +1. It's recommended to deploy a build project and folder first, and to use this project in the configuration for additional deployments. The build project can be deployed and managed using this framework with a couple of additional steps to create a local state configuration and then to migrate state to a remote state configuration after the project and bucket are created. ALternatively, the build resources can be pre-created and then imported into the framework using the import command. These considerations are not addressed in this example. + +### Deployment +Automated installation configuration, and deployment steps are managed using Makefile targets. Use ```make help``` for a list of configured targets: +``` +$ make help +make + +Targets: + help Show this help + pre-commit Run pre-commit checks + + aws_gitlab_clean Clean up state files + aws_gitlab_configure Configure the deployment + aws_gitlab_deploy Deploy configured resources + aws_gitlab_init Initialize modules, providers + aws_gitlab_install Install Terraform, Terragrunt + aws_gitlab_lint Run Go linters + aws_gitlab_plan Show deployment plan + aws_gitlab_test Run deployment tests +``` + +Note that additional targets can be added in order to configure multiple environments, for example to create development and production environments. diff --git a/aws/aws.yaml b/aws/aws.yaml new file mode 100644 index 0000000..459d474 --- /dev/null +++ b/aws/aws.yaml @@ -0,0 +1,2 @@ +--- +prefix: "" # prefix to use to uniquely identify and name resources managed under this structure, should be 3-5 characters in length diff --git a/aws/aws_gitlab_terragrunt.hcl b/aws/aws_gitlab_terragrunt.hcl new file mode 100644 index 0000000..123b909 --- /dev/null +++ b/aws/aws_gitlab_terragrunt.hcl @@ -0,0 +1,40 @@ +# configure gcs bucket dynamically. +remote_state { + backend = "gcs" + config = { + bucket = format("%s-%s-terraform-state", local.platform.prefix, format("%s-environment", local.environment.environment)) + prefix = path_relative_to_include() + location = local.multiregion.region + project = local.platform.build_project + skip_bucket_creation = false + skip_bucket_versioning = false + } +} + +locals { + default_yaml_path = find_in_parent_folders("empty.yaml") # terragrunt function for input search (not implemented). + platform = fileexists(find_in_parent_folders("local.gcp.yaml")) ? yamldecode(file(find_in_parent_folders("local.gcp.yaml"))) : yamldecode(file(find_in_parent_folders("gcp.yaml"))) + environment = yamldecode(file(find_in_parent_folders("env.yaml"))) + multiregion = yamldecode(file(find_in_parent_folders("reg-multi/region.yaml"))) + versions = yamldecode(file(find_in_parent_folders("versions.yaml"))) +} + +terragrunt_version_constraint = "${format("~> %s.0", local.versions.terragrunt_binary_version)}" +terraform_version_constraint = "${format("~> %s.0", local.versions.terraform_binary_version)}" + +generate "provider" { + path = "provider_override.tf" + if_exists = "overwrite" + contents = < %s.0", local.versions.google_provider_version)}" + } + google-beta = { + version = "${format("~> %s.0", local.versions.google_provider_version)}" + } + } +} +EOF +} diff --git a/aws/empty.yaml b/aws/empty.yaml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/aws/empty.yaml @@ -0,0 +1 @@ +--- diff --git a/aws/gitlab/env.yaml b/aws/gitlab/env.yaml new file mode 100644 index 0000000..77c1781 --- /dev/null +++ b/aws/gitlab/env.yaml @@ -0,0 +1,23 @@ +--- +environment: dev +labels: + deployment: kped + environment: dev + owner: kpeder + team: devops +locations: + multiregion: MREGION + primary: PREGION + secondary: SREGION + +dependencies: + example_folder_dependency_path: "global/folders/example" + example_folder_mock_outputs: + id: "123456789012" + name: "kped-dev-example-folder" + + example_project_dependency_path: "global/projects/example" + example_project_mock_outputs: + project_id: "kped-dev-example" + project_name: "kped-dev-example" + project_number: "123456789012" diff --git a/aws/gitlab/reg-primary/region.yaml b/aws/gitlab/reg-primary/region.yaml new file mode 100644 index 0000000..9809e0d --- /dev/null +++ b/aws/gitlab/reg-primary/region.yaml @@ -0,0 +1,4 @@ +--- +region: "us-east-2" +location: "us-east-2" +zone_preference: "a" diff --git a/aws/gitlab/reg-secondary/region.yaml b/aws/gitlab/reg-secondary/region.yaml new file mode 100644 index 0000000..ae5a853 --- /dev/null +++ b/aws/gitlab/reg-secondary/region.yaml @@ -0,0 +1,4 @@ +--- +region: "us-west-2" +location: "us-west-2" +zone_preference: "a" diff --git a/aws/gitlab/scripts/configure.sh b/aws/gitlab/scripts/configure.sh new file mode 100755 index 0000000..bfb2f76 --- /dev/null +++ b/aws/gitlab/scripts/configure.sh @@ -0,0 +1,82 @@ +#!/bin/bash + +set -e + +function exit_with_msg { + echo "${1}" + exit 1 +} + +while [ $# -gt 0 ]; do + case "${1}" in + -e|--environment) + ENVIRONMENT="${2}" + shift + ;; + -h|--help) + echo "Usage:" + echo "$0 \\" + echo " -e|--environment " + echo " [-h|--help]" + echo " -o|--owner " + echo " -p|--primaryregion " + echo " -s|--secondaryregion " + echo " -t|--team " + exit 0 + ;; + -o|--owner) + OWNER="${2}" + shift + ;; + -p|--primaryregion) + PREGION="${2}" + shift + ;; + -s|--secondaryregion) + SREGION="${2}" + shift + ;; + -t|--team) + TEAM="${2}" + shift + ;; + *) + exit_with_msg "Error: Invalid argument '${1}'." + esac + shift +done + +if [[ -f ../local.aws.yaml ]]; then + PREFIX=$(cat ../local.aws.yaml | grep ^prefix | awk -F '[: #"]+' '{print $2}') +else + PREFIX=$(cat ../aws.yaml | grep ^prefix | awk -F '[: #"]+' '{print $2}') +fi + +[[ -z ${PREFIX} ]] && exit_with_msg "Can't locate deployment prefix. Exiting." +[[ ${#PREFIX} > 5 ]] && exit_with_msg "Prefix '${PREFIX}' is too long. Exiting." + +[[ -z ${ENVIRONMENT} ]] && exit_with_msg "-e|--environment is a required parameter. Exiting." +[[ -z ${OWNER} ]] && exit_with_msg "-o|--owner is a required parameter. Exiting." +[[ -z ${PREGION} ]] && exit_with_msg "-p|--primaryregion is a required parameter. Exiting." +[[ -z ${SREGION} ]] && exit_with_msg "-s|--secondaryregion is a required parameter. Exiting." +[[ -z ${TEAM} ]] && exit_with_msg "-t|--team is a required parameter. Exiting." + +echo "Deployment Owner: ${OWNER}" +echo "Environment: ${ENVIRONMENT}" +echo "Name Prefix: ${PREFIX}" +echo "Primary Region: ${PREGION}" +echo "Secondary Region: ${SREGION}" +echo "Support Team: ${TEAM}" + +cp templates/env.tpl env.yaml +cp templates/region.tpl reg-primary/region.yaml +cp templates/region.tpl reg-secondary/region.yaml + +sed -i -e "s:ENVIRONMENT:${ENVIRONMENT}:g" env.yaml +sed -i -e "s:OWNER:${OWNER}:g" env.yaml +sed -i -e "s:PREFIX:${PREFIX}:g" env.yaml +sed -i -e "s:REGION:${PREGION}:g" reg-primary/region.yaml +sed -i -e "s:ZONE:a:g" reg-primary/region.yaml +sed -i -e "s:REGION:${SREGION}:g" reg-secondary/region.yaml +sed -i -e "s:ZONE:a:g" reg-secondary/region.yaml +sed -i -e "s:TEAM:${TEAM}:g" env.yaml diff --git a/aws/gitlab/scripts/prune_caches.sh b/aws/gitlab/scripts/prune_caches.sh new file mode 100755 index 0000000..f6440fe --- /dev/null +++ b/aws/gitlab/scripts/prune_caches.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e + +function exit_with_msg { + echo "${1}" + exit 1 +} + +while [ $# -gt 0 ]; do + case "${1}" in + -h|--help) + echo "Usage:" + echo "$0 \\" + echo " [-h : --help]" + echo " []" + exit 0 + ;; + *) + [[ -d ${1} ]] || exit_with_msg "directory ${1} not found" + TARGET=${1} + esac + shift +done + +if [[ -z ${TARGET} ]]; then + TARGET=. + echo "Directory not specified; using current directory." +fi + +find ${TARGET} -type d -name ".terragrunt-cache" -prune -exec rm -rf {} \; +find ${TARGET} -type f -name ".terraform.lock.hcl" -prune -exec rm -rf {} \; diff --git a/aws/gitlab/templates/env.tpl b/aws/gitlab/templates/env.tpl new file mode 100644 index 0000000..c2f6f38 --- /dev/null +++ b/aws/gitlab/templates/env.tpl @@ -0,0 +1,12 @@ +--- +environment: ENVIRONMENT +labels: + deployment: PREFIX + environment: ENVIRONMENT + owner: OWNER + team: TEAM +locations: + primary: PREGION + secondary: SREGION + +dependencies: diff --git a/aws/gitlab/templates/region.tpl b/aws/gitlab/templates/region.tpl new file mode 100644 index 0000000..db08243 --- /dev/null +++ b/aws/gitlab/templates/region.tpl @@ -0,0 +1,4 @@ +--- +region: "REGION" +location: "REGION" +zone_preference: "ZONE" diff --git a/aws/gitlab/test/aws_gitlab_test.go b/aws/gitlab/test/aws_gitlab_test.go new file mode 100644 index 0000000..698bbc7 --- /dev/null +++ b/aws/gitlab/test/aws_gitlab_test.go @@ -0,0 +1,241 @@ +package test + +import ( + "flag" + "fmt" + "os" + + //regexp" + "sort" + //"strings" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" + + //"github.com/stretchr/testify/require" + //"github.com/thedevsaddam/gojsonq/v2" + "gopkg.in/yaml.v3" +) + +// Flag to destroy the target environment after tests +var destroy = flag.Bool("destroy", false, "destroy environment after tests") + +func TestTerragruntDeployment(t *testing.T) { + + // Terraform options + binary := "terragrunt" + rootdir := "../." + moddirs := make(map[string]string) + + // Non-local vars to evaluate state between modules + + // Reusable vars for unmarshalling YAML files + var err error + var yfile []byte + + // Define the deployment root + terraformDeploymentOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: rootdir, + TerraformBinary: binary, + }) + + // Check for standard global configuration files + if !fileExists(terraformDeploymentOptions.TerraformDir + "/env.yaml") { + t.Fail() + } + if !fileExists(terraformDeploymentOptions.TerraformDir + "/../local.aws.yaml") { + if !fileExists(terraformDeploymentOptions.TerraformDir + "/../aws.yaml") { + t.Fail() + } + } + if !fileExists(terraformDeploymentOptions.TerraformDir + "/reg-primary/region.yaml") { + t.Fail() + } + if !fileExists(terraformDeploymentOptions.TerraformDir + "/reg-secondary/region.yaml") { + t.Fail() + } + if !fileExists(terraformDeploymentOptions.TerraformDir + "/versions.yaml") { + t.Fail() + } + + // Define modules + moddirs["0-example"] = "../global/example" + + // Maps are unsorted, so sort the keys to process the modules in order + modkeys := make([]string, 0, len(moddirs)) + for k := range moddirs { + modkeys = append(modkeys, k) + } + sort.Strings(modkeys) + + for _, module := range modkeys { + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: moddirs[module], + TerraformBinary: binary, + }) + + fmt.Println("Validating module:", module) + + // Sanity test + terraform.Validate(t, terraformOptions) + + // Check for standard files + if !fileExists(terraformOptions.TerraformDir + "/inputs.yaml") { + t.Fail() + } + if !fileExists(terraformOptions.TerraformDir + "/remotestate.tf") { + t.Fail() + } + if !fileExists(terraformOptions.TerraformDir + "/terragrunt.hcl") { + t.Fail() + } + } + + // Read and store the env.yaml + yfile, err = os.ReadFile(terraformDeploymentOptions.TerraformDir + "/env.yaml") + if err != nil { + t.Fail() + } + + env := make(map[string]interface{}) + err = yaml.Unmarshal(yfile, &env) + if err != nil { + t.Fail() + } + + // Read and store the gcp.yaml + if fileExists(terraformDeploymentOptions.TerraformDir + "/../local.aws.yaml") { + yfile, err = os.ReadFile(terraformDeploymentOptions.TerraformDir + "/../local.aws.yaml") + if err != nil { + t.Fail() + } + } else { + yfile, err = os.ReadFile(terraformDeploymentOptions.TerraformDir + "/../aws.yaml") + if err != nil { + t.Fail() + } + } + + platform := make(map[string]interface{}) + err = yaml.Unmarshal(yfile, &platform) + if err != nil { + t.Fail() + } + + // Read and store the reg-primary/region.yaml + yfile, err = os.ReadFile(terraformDeploymentOptions.TerraformDir + "/reg-primary/region.yaml") + if err != nil { + t.Fail() + } + + pregion := make(map[string]interface{}) + err = yaml.Unmarshal(yfile, &pregion) + if err != nil { + t.Fail() + } + + // Read and store the reg-secondary/region.yaml + yfile, err = os.ReadFile(terraformDeploymentOptions.TerraformDir + "/reg-secondary/region.yaml") + if err != nil { + t.Fail() + } + + sregion := make(map[string]interface{}) + err = yaml.Unmarshal(yfile, &sregion) + if err != nil { + t.Fail() + } + + // Read and store the versions.yaml + yfile, err = os.ReadFile(terraformDeploymentOptions.TerraformDir + "/versions.yaml") + if err != nil { + t.Fail() + } + + versions := make(map[string]interface{}) + err = yaml.Unmarshal(yfile, &versions) + if err != nil { + t.Fail() + } + + // Clean up after ourselves if flag is set + if *destroy { + defer terraform.TgDestroyAll(t, terraformDeploymentOptions) + } + // Deploy the composition + terraform.TgApplyAll(t, terraformDeploymentOptions) + + for _, module := range modkeys { + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: moddirs[module], + TerraformBinary: binary, + }) + + fmt.Println("Testing module:", module) + + // Read the provider output and verify configured version + providers := terraform.RunTerraformCommand(t, terraformOptions, terraform.FormatArgs(terraformOptions, "providers")...) + assert.Contains(t, providers, "provider[registry.terraform.io/hashicorp/aws] ~> "+versions["aws_provider_version"].(string)) + + // Read the inputs.yaml + yfile, err := os.ReadFile(terraformOptions.TerraformDir + "/inputs.yaml") + if err != nil { + t.Fail() + } + + inputs := make(map[string]interface{}) + err = yaml.Unmarshal(yfile, &inputs) + if err != nil { + t.Fail() + } + + // Read the terragrunt.hcl + hclfile, err := os.ReadFile(terraformOptions.TerraformDir + "/terragrunt.hcl") + if err != nil { + t.Fail() + } + + hclstring := string(hclfile) + + // Make sure the path referes to the correct parent hcl file + assert.Contains(t, hclstring, "path = find_in_parent_folders(\"aws_gitlab_terragrunt.hcl\")") + + // Collect the outputs + outputs := terraform.OutputAll(t, terraformOptions) + + assert.NotEmpty(t, outputs) + + // Add module-specific tests below + // Remember that we're in a loop, so group tests by module name (modules range keys) + // The following collections are available for tests: + // platform, env, mregion, pregion, sregion, versions, inputs, outputs + // Two key patterns are available. + // 1. Reference the output map returned by terraform.OutputAll (ie. the output of "terragrunt output") + // require.Equal(t, pregion["location"], outputs["location"]) + // 2. Query the json string representing state returned by terraform.Show (ie. the output of "terragrunt show -json") + // modulejson := gojsonq.New().JSONString(terraform.Show(t, terraformOptions)).From("values.root_module.resources"). + // Where("address", "eq", "azurerm_resource_group.main"). + // Select("values") + // // Execute the above query; since it modifies the pointer we can only do this once, so we add it to a variable + // values := modulejson.Get() + + // Module-specific tests + switch module { + + // Example folder module + case "0-example": + // Make sure that prevent_destroy is set to false + assert.Contains(t, hclstring, "prevent_destroy = false") + + } + } +} + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} diff --git a/aws/gitlab/versions.yaml b/aws/gitlab/versions.yaml new file mode 100644 index 0000000..b78533c --- /dev/null +++ b/aws/gitlab/versions.yaml @@ -0,0 +1,9 @@ +--- +golang_runtime_version: "1.21" +aws_provider_version: "5.44.0" + +terraform_binary_version: "1.7" +terraform_install_version: "1.7.4" + +terragrunt_binary_version: "0.55" +terragrunt_install_version: "0.55.16" diff --git a/docs/decisions/adr_0001.md b/docs/decisions/adr_0001.md new file mode 100644 index 0000000..fddd16c --- /dev/null +++ b/docs/decisions/adr_0001.md @@ -0,0 +1,15 @@ +## 1. Record architecture decisions + +Date: YYYY-MM-DD + +### Status +Proposed + +### Context +Architectural decisions made on this project should be consistently recorded, for reference, rigor and consistency. + +### Decision +Architecture Decision Records will be used, as described in this [article](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions). + +### Consequences +See Michael Nygard's article, linked above. diff --git a/docs/decisions/adr_0002.md b/docs/decisions/adr_0002.md new file mode 100644 index 0000000..6bb5dd9 --- /dev/null +++ b/docs/decisions/adr_0002.md @@ -0,0 +1,18 @@ +## 2. All features include tests + +Date: YYYY-MM-DD + +### Status +Proposed + +### Context +Features included in the product should be tested to ensure that functionality meets expectations and regressions are not introduced over time. This increases the quality of the product and provides a consistent benchmark for its expected operation. + +This approach provides a foundation for the practice of Test Driven Development (TDD). + +### Decision +New features must include functional tests. + +### Consequences +1. All tests will need to pass before any Pull Request is merged. +1. Test updates will need to be included with all Pull Requests introducing new features. diff --git a/docs/decisions/adr_template.md b/docs/decisions/adr_template.md new file mode 100644 index 0000000..ab6a702 --- /dev/null +++ b/docs/decisions/adr_template.md @@ -0,0 +1,15 @@ +## Number. Title + +Date: YYYY-MM-DD + +### Status +Proposed|Accepted|Superceded|Deprecated + +### Context +The issues motivating this decision, and any context that influences or constrains the decision. + +### Decision +The proposed decision, succinctly stated. + +### Consequences +The impact of this decision, and any risks introduced by the change that will need to be addressed. diff --git a/docs/decisions/index.md b/docs/decisions/index.md new file mode 100644 index 0000000..8955d98 --- /dev/null +++ b/docs/decisions/index.md @@ -0,0 +1,6 @@ +## Architecture Decision Record Index + +[ADR Template](./adr_template.md) + +1. [Record architecture decisions](./adr_0001.md) +1. [All features include tests](./adr_0002.md) diff --git a/modules/.gitkeep b/modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/install_terraform.sh b/scripts/install_terraform.sh new file mode 100755 index 0000000..5e1735a --- /dev/null +++ b/scripts/install_terraform.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -euo pipefail + +function exit_with_msg { + echo "${1}" + exit 1 +} + +while [ $# -gt 0 ]; do + case "${1}" in + -h|--help) + echo "Usage:" + echo "$0 \\" + echo " [-h|--help]" + echo " -v|--version-file " + exit 0 + ;; + -v|--version-file) + VERSIONS_FILE="${2}" + shift + ;; + *) + exit_with_msg "Error: Invalid argument '${1}'." + esac + shift +done + +readonly TERRAFORM_INSTALL_DIR="/usr/local/bin" +mkdir -p "$TERRAFORM_INSTALL_DIR" + +# Make sure we have write permissions to target directory before downloading +if [ ! -w "$TERRAFORM_INSTALL_DIR" ] ; then + >&2 echo "User does not have write permission to folder: ${TERRAFORM_INSTALL_DIR}" + exit 1 +fi + +# Get the directory where the script is located +readonly SCRIPT_DIR="$(dirname $0)" + +# Get the operating system identifier. +# May be one of "linux", "darwin", "freebsd" or "openbsd". +OS_IDENTIFIER="${1:-}" +if [[ -z "$OS_IDENTIFIER" ]]; then + # POSIX compliant OS detection + OS_IDENTIFIER=$(uname -s | tr '[:upper:]' '[:lower:]') + >&2 echo "Detected OS Identifier: ${OS_IDENTIFIER}" +fi +readonly OS_IDENTIFIER + +# Determine the version of terraform to install +if [[ -z VERSIONS_FILE ]]; then + VERSIONS_FILE="${SCRIPT_DIR}/../versions.yaml" +fi +readonly VERSIONS_FILE +>&2 echo "Reading $VERSIONS_FILE" + +readonly TERRAFORM_VERSION="$(cat $VERSIONS_FILE | grep '^terraform_install_version: ' | awk -F':' '{gsub(/^[[:space:]]*["]*|["]*[[:space:]]*$/,"",$2); print $2}')" +if [[ -z "$TERRAFORM_VERSION" ]]; then + >&2 echo 'Unable to find version number' + exit 1 +fi + +# Install terraform +readonly TERRAFORM_BIN="${TERRAFORM_INSTALL_DIR}/terraform" +cd "$(mktemp -d)" +wget "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_${OS_IDENTIFIER}_amd64.zip" -O terraform.zip +rm -f "${TERRAFORM_BIN}" || echo "Terraform is not installed." +unzip terraform.zip -d "$TERRAFORM_INSTALL_DIR" +chmod +x "${TERRAFORM_BIN}" + +# Cleanup +rm terraform.zip diff --git a/scripts/install_terragrunt.sh b/scripts/install_terragrunt.sh new file mode 100755 index 0000000..b74a87e --- /dev/null +++ b/scripts/install_terragrunt.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -euo pipefail + +function exit_with_msg { + echo "${1}" + exit 1 +} + +while [ $# -gt 0 ]; do + case "${1}" in + -h|--help) + echo "Usage:" + echo "$0 \\" + echo " [-h|--help]" + echo " -v|--version-file " + exit 0 + ;; + -v|--version-file) + VERSIONS_FILE="${2}" + shift + ;; + *) + exit_with_msg "Error: Invalid argument '${1}'." + esac + shift +done + +readonly TERRAGRUNT_INSTALL_DIR="/usr/local/bin" +mkdir -p "$TERRAGRUNT_INSTALL_DIR" + +# Make sure we have write permissions to target directory before downloading +if [ ! -w "$TERRAGRUNT_INSTALL_DIR" ] ; then + >&2 echo "User does not have write permission to folder: ${TERRAGRUNT_INSTALL_DIR}" + exit 1 +fi + +# Get the directory where the script is located +readonly SCRIPT_DIR="$(dirname $0)" + +# Get the operating system identifier. +# May be one of "linux", "darwin", "freebsd" or "openbsd". +OS_IDENTIFIER="${1:-}" +if [[ -z "$OS_IDENTIFIER" ]]; then + # POSIX compliant OS detection + OS_IDENTIFIER=$(uname -s | tr '[:upper:]' '[:lower:]') + >&2 echo "Detected OS Identifier: ${OS_IDENTIFIER}" +fi +readonly OS_IDENTIFIER + +# Determine the version of terragrunt to install +if [[ -z VERSIONS_FILE ]]; then + VERSIONS_FILE="${SCRIPT_DIR}/../versions.yaml" +fi +readonly VERSIONS_FILE +>&2 echo "Reading $VERSIONS_FILE" + +readonly TERRAGRUNT_VERSION="$(cat $VERSIONS_FILE | grep '^terragrunt_install_version: ' | awk -F':' '{gsub(/^[[:space:]]*["]*|["]*[[:space:]]*$/,"",$2); print $2}')" +if [[ -z "$TERRAGRUNT_VERSION" ]]; then + >&2 echo 'Unable to find version number' + exit 1 +fi + +# Install terragrunt +readonly TERRAGRUNT_BIN="$TERRAGRUNT_INSTALL_DIR/terragrunt" +cd "$(mktemp -d)" +wget "https://github.com/gruntwork-io/terragrunt/releases/download/v${TERRAGRUNT_VERSION}/terragrunt_${OS_IDENTIFIER}_amd64" -O terragrunt +rm -f "$TERRAGRUNT_BIN" || echo "Terragrunt is not installed." +cp terragrunt "$TERRAGRUNT_BIN" +chmod +x "$TERRAGRUNT_BIN" + +# Cleanup +rm terragrunt diff --git a/scripts/prune_terragrunt_cache.sh b/scripts/prune_terragrunt_cache.sh new file mode 100755 index 0000000..f6440fe --- /dev/null +++ b/scripts/prune_terragrunt_cache.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +set -e + +function exit_with_msg { + echo "${1}" + exit 1 +} + +while [ $# -gt 0 ]; do + case "${1}" in + -h|--help) + echo "Usage:" + echo "$0 \\" + echo " [-h : --help]" + echo " []" + exit 0 + ;; + *) + [[ -d ${1} ]] || exit_with_msg "directory ${1} not found" + TARGET=${1} + esac + shift +done + +if [[ -z ${TARGET} ]]; then + TARGET=. + echo "Directory not specified; using current directory." +fi + +find ${TARGET} -type d -name ".terragrunt-cache" -prune -exec rm -rf {} \; +find ${TARGET} -type f -name ".terraform.lock.hcl" -prune -exec rm -rf {} \;