Skip to content

Commit

Permalink
v2.0.0 - Destroying Bootstraps FTW
Browse files Browse the repository at this point in the history
  • Loading branch information
Zordrak committed Dec 3, 2024
1 parent fdfacd9 commit 4d496e7
Show file tree
Hide file tree
Showing 55 changed files with 1,010 additions and 118 deletions.
29 changes: 26 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
### Terraform ###

# Transient backends
components/**/backend_tfscaffold.tf

# Compiled files
**/*.tfstate
**/*.tfplan
**/*.tfstate.backup
**/.terraform
**/.terraform.lock.hcl
**/.terraform/*

# Terraform Output
**/.terraform.output.json
**/build/*
**/work/*
**/*tfstate.lock.info
**/*.tfplan.json

# Scaffold Plugin Cache
plugin-cache/*
Expand Down Expand Up @@ -37,3 +43,20 @@ Icon
Network Trash Folder
Temporary Items
.apdisk

# Security scan reports
*.sarif

# Checkov module directory
.external_modules

# Node modules
node_modules/

*.swp
.nyc_output

# Python
**/.venv
**/__pycache__
**/.pytest_cache
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
## 2.0.0 (02/12/2024)

BREAKING CHANGES:

* It is now possible, with a validation check, to destroy the bootstrap.
* Bootstrap and the example component have been completely rewritten.
* `.terraform` and `backend_tfscaffold.tf` no longer removed during bootstrap cleanup
* Bootstrap and examples now configured to use terraform >= 1.10.0.
* Bootstrap and examples now require terraform > 1.0.0.
* Bootstrap and examples now require AWS Provider ~> 5.79.0.
* tfscaffold tagging default have changed to prefix tag keys with tfscaffold:

FEATURES:

* Bootstraps can now be fully and cleanly destroyed, however only interactively,
requiring a manual text input to confirm.
* A DynamoDB lock table has been added to Bootstrap.
* Bootstrap now uses AWS Provider v4+ S3 Bucket property resources instead
of declaring all configuration in a single bucket resource.
* The example component has been rewritten to reflect modern naming standards,
and idempotency structures.
* An example module has been added, which is called from the example component.
* Example region changed to eu-west-2.
* `bin/docs.sh` has been added to recursively apply terraform-docs
(https://github.com/terraform-docs/terraform-docs) to all directories beneath
the project root that contain a variables.tf file.
* Remove unnecessary compatability log entry for auto-approve.
* Remove unnecessary non-current version transitions from the bootstrap bucket.

BUG FIXES:

* Do not write `.terraform.output.json` after a destroy.
* `-compact-warnings` is no longer passed to `terraform init`

CHORES:

* `.gitignore` updated

## 1.10.2 (02/12/2024)

BUG FIXES:
Expand Down
7 changes: 7 additions & 0 deletions bin/docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env bash

set -euo pipefail

declare script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )";
declare base_path="${script_path%%\/bin}";
find "${base_path}" -name 'variables.tf' -exec sh -c 'terraform-docs markdown table --output-file README.md $(dirname "$1")' sh {} \;
21 changes: 13 additions & 8 deletions bin/terraform.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
##
# Set Script Version
##
readonly script_ver='1.10.2';
readonly script_ver='2.0.0';

##
# Standardised failure function
Expand Down Expand Up @@ -438,7 +438,11 @@ declare tf_var_params;

if [ "${bootstrap}" == 'true' ]; then
if [ "${action}" == "destroy" ]; then
error_and_die 'You cannot destroy a bootstrap bucket using tfscaffold, it's just too dangerous. If you're absolutely certain that you want to delete the bucket and all contents, including any possible state files environments and components within this project, then you will need to do it from the AWS Console. Note you cannot do this from the CLI because the bootstrap bucket is versioned, and even the --force CLI parameter will not empty the bucket of versions';
echo -en "\n#####################\n# ALERT ALERT ALERT #\n#####################\n\nDo you *really* want to destroy this bootstrap?\n\nPerforming this action will delete your WHOLE STATE BUCKET (${bucket}) AND ALL ITS CONTENTS FOR ALL ENVIRONMENTS.\nAny state files you have created as part of this tfscaffold project will be IRRECOVERABLY DELETED! Forever!\n\nAcknowledge by typing out this exact sentence, removing all + characters: \"I+am+not+an+idiot,+I+know+what+I+am+doing!\": ";
read destroy_response;
if [ "${destroy_response}" != 'I am not an idiot, I know what I am doing!' ]; then
error_and_die "ABORT ABORT ABORT!! YOU ARE AN IDIOT!!";
fi;
fi;

# Bootstrap requires this parameter as explicit as it is constructed here
Expand Down Expand Up @@ -668,14 +672,18 @@ if [ "${bootstrapped}" == 'true' ]; then
# Configure remote state storage
echo "Setting up S3 remote state from s3://${bucket}/${backend_key}";
[ "${lock_table}" == 'true' ] && echo "Using DynamoDB Table for state locking: ${bucket}";
terraform init ${no_color} ${compact_warnings} ${lockfile_or_upgrade} \
terraform init ${no_color} ${lockfile_or_upgrade} \
|| error_and_die 'Terraform init failed';

if [ "${action}" == 'destroy' ] && [ "${destroy_response}" == 'I am not an idiot, I know what I am doing!' ]; then
echo -e "terraform {\n backend \"local\" {}\n}" > backend_tfscaffold.tf;
terraform init -migrate-state -force-copy;
fi;
else
# We are bootstrapping. Download the providers, skip the backend config.
terraform init \
-backend=false \
${no_color} \
${compact_warnings} \
${lockfile} \
|| error_and_die 'Terraform init failed';
fi;
Expand Down Expand Up @@ -740,7 +748,6 @@ case "${action}" in

# Support for terraform <0.10 is now deprecated
if [ "${action}" == 'apply' ]; then
echo 'Compatibility: Adding to terraform arguments: -auto-approve=true';
extra_args+=' -auto-approve=true';
else # action is `destroy`
# Check terraform version - if pre-0.15, need to add `-force`; 0.15 and above instead use `-auto-approve`
Expand Down Expand Up @@ -795,9 +802,7 @@ case "${action}" in
echo 'yes' | terraform init ${lockfile} || error_and_die 'Terraform init failed';

# Hard cleanup
rm -f backend_tfscaffold.tf;
rm -f terraform.tfstate # Prime not the backup
rm -rf .terraform;

# This doesn't mean anything here, we're just celebrating!
bootstrapped='true';
Expand All @@ -809,7 +814,7 @@ case "${action}" in
error_and_die "Terraform ${action} failed with exit code ${exit_code}";
fi;

if [ "${output_json}" == 'true' ]; then
if [ "${output_json}" == 'true' ] && [ "${action}" != 'destroy' ]; then
echo "Writing terraform output to $(pwd)/.terraform.output.json";
terraform output -json -no-color > .terraform.output.json;
fi;
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/.terraform-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.14.7
latest:^1\.[1-9][0-9]*\.[0-9]\+$
59 changes: 59 additions & 0 deletions bootstrap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.0.0 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | ~> 5.79.0 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | 5.79.0 |

## Modules

No modules.

## Resources

| Name | Type |
|------|------|
| [aws_dynamodb_table.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource |
| [aws_kms_alias.s3](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource |
| [aws_kms_key.s3](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource |
| [aws_s3_bucket.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
| [aws_s3_bucket_lifecycle_configuration.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration) | resource |
| [aws_s3_bucket_policy.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource |
| [aws_s3_bucket_public_access_block.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
| [aws_s3_bucket_server_side_encryption_configuration.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource |
| [aws_s3_bucket_versioning.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource |
| [aws_iam_policy_document.default_assumerole](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.kms_s3](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_iam_policy_document.s3_main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_aws_account_id"></a> [aws\_account\_id](#input\_aws\_account\_id) | The AWS Account ID into which we are bootstrapping tfscaffold | `string` | n/a | yes |
| <a name="input_bucket_name"></a> [bucket\_name](#input\_bucket\_name) | The name to use for the tfscaffold bucket. This should be provided from tfscaffold shell, not environment or group tfvars | `string` | n/a | yes |
| <a name="input_component"></a> [component](#input\_component) | The name of the component for the bootstrapping process; which is always bootstrap | `string` | `"bootstrap"` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | The name of the environment for the bootstrapping process; which is always bootstrap | `string` | `"bootstrap"` | no |
| <a name="input_project"></a> [project](#input\_project) | The name of the Project we are bootstrapping tfscaffold for | `string` | n/a | yes |
| <a name="input_region"></a> [region](#input\_region) | The AWS Region into which we are bootstrapping tfscaffold | `string` | n/a | yes |
| <a name="input_tfscaffold_ro_principals"></a> [tfscaffold\_ro\_principals](#input\_tfscaffold\_ro\_principals) | A list of Principals permitted to ListBucket and GetObject for Remote State purposes. Normally the root principal of the account | `list(string)` | `[]` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_s3_bucket_arn"></a> [s3\_bucket\_arn](#output\_s3\_bucket\_arn) | n/a |
| <a name="output_s3_bucket_id"></a> [s3\_bucket\_id](#output\_s3\_bucket\_id) | n/a |
| <a name="output_s3_bucket_name"></a> [s3\_bucket\_name](#output\_s3\_bucket\_name) | n/a |
| <a name="output_s3_bucket_policy"></a> [s3\_bucket\_policy](#output\_s3\_bucket\_policy) | n/a |
| <a name="output_s3_kms_key_arn"></a> [s3\_kms\_key\_arn](#output\_s3\_kms\_key\_arn) | n/a |
| <a name="output_s3_kms_key_id"></a> [s3\_kms\_key\_id](#output\_s3\_kms\_key\_id) | n/a |
| <a name="output_s3_kms_key_policy"></a> [s3\_kms\_key\_policy](#output\_s3\_kms\_key\_policy) | n/a |
<!-- END_TF_DOCS -->
26 changes: 26 additions & 0 deletions bootstrap/aws_dynamodb_table.main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
resource "aws_dynamodb_table" "main" {
name = var.bucket_name
hash_key = "LockID"
billing_mode = "PAY_PER_REQUEST"

attribute {
name = "LockID"
type = "S"
}

point_in_time_recovery {
enabled = true
}

server_side_encryption {
enabled = true
kms_key_arn = aws_kms_key.s3.arn
}

tags = merge(
local.default_tags,
{
Name = var.bucket_name
},
)
}
4 changes: 4 additions & 0 deletions bootstrap/aws_kms_alias.s3.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "aws_kms_alias" "s3" {
name = "alias/s3/${var.bucket_name}"
target_key_id = aws_kms_key.s3.key_id
}
15 changes: 7 additions & 8 deletions bootstrap/kms_key_s3.tf → bootstrap/aws_kms_key.s3.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ resource "aws_kms_key" "s3" {
deletion_window_in_days = 10
enable_key_rotation = true

policy = data.aws_iam_policy_document.kms_key_s3.json
policy = data.aws_iam_policy_document.kms_s3.json

# This does not use default tag map merging because bootstrapping is special
# You should use default tag map merging elsewhere
tags = {
Name = "tfscaffold Bootstrap S3 Bucket"
Environment = var.environment
Project = var.project
Component = var.component
Account = var.aws_account_id
}
tags = merge(
local.default_tags,
{
Name = "tfscaffold Bootstrap S3 Bucket"
}
)
}
14 changes: 14 additions & 0 deletions bootstrap/aws_s3_bucket.main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
resource "aws_s3_bucket" "main" {
bucket = var.bucket_name

force_destroy = true

# This does not use default tag map merging because bootstrapping is special
# You should use default tag map merging elsewhere
tags = merge(
local.default_tags,
{
Name = "Terraform Scaffold State File Bucket for account ${var.aws_account_id} in region ${var.region}"
}
)
}
16 changes: 16 additions & 0 deletions bootstrap/aws_s3_bucket_lifecycle_configuration.main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
resource "aws_s3_bucket_lifecycle_configuration" "main" {
bucket = aws_s3_bucket.main.id

rule {
id = "bootstrap"
status = "Enabled"

filter {
prefix = ""
}

noncurrent_version_expiration {
noncurrent_days = "90"
}
}
}
8 changes: 8 additions & 0 deletions bootstrap/aws_s3_bucket_policy.main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resource "aws_s3_bucket_policy" "main" {
bucket = aws_s3_bucket.main.id
policy = data.aws_iam_policy_document.s3_main.json

depends_on = [
aws_s3_bucket_public_access_block.main,
]
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
resource "aws_s3_bucket_public_access_block" "bucket" {
bucket = aws_s3_bucket.bucket.id
resource "aws_s3_bucket_public_access_block" "main" {
bucket = aws_s3_bucket.main.id

block_public_acls = true
block_public_policy = true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
bucket = aws_s3_bucket.main.id

rule {
apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_key.s3.arn
sse_algorithm = "aws:kms"
}

bucket_key_enabled = true
}
}
7 changes: 7 additions & 0 deletions bootstrap/aws_s3_bucket_versioning.main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id

versioning_configuration {
status = "Enabled"
}
}
18 changes: 18 additions & 0 deletions bootstrap/data.aws_iam_policy_document.default_assumerole.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
data "aws_iam_policy_document" "default_assumerole" {
statement {
sid = "DefaultAssumeRole"
effect = "Allow"

actions = [
"sts:AssumeRole",
]

principals {
type = "AWS"

identifiers = [
"arn:aws:iam::${var.aws_account_id}:root",
]
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
data "aws_iam_policy_document" "kms_key_s3" {
data "aws_iam_policy_document" "kms_s3" {
statement {
sid = "AllowLocalIAMAdministration"
effect = "Allow"
Expand Down
Loading

0 comments on commit 4d496e7

Please sign in to comment.