Skip to content

Commit

Permalink
First pass at a full terraform example (#151)
Browse files Browse the repository at this point in the history
<!-- markdownlint-disable-next-line first-line-heading -->
## Description

As a general rule, the repository template should provide functionality
that works out of the box. The terraform example is problematic because
the options default to running the example, and the example doesn't give
any guidance as to how people should set up their own code.

This patch removes the example, and replaces it with a more fully
explained quick start example in `Scripting_Terraform.md`.

It also adds the `TF_ENV` environment variable, as a shorthand for
selecting the terraform environment directory under
`infrastructure/environments`.


## Type of changes

<!-- What types of changes does your code introduce? Put an `x` in all
the boxes that apply. -->

- [ ] Refactoring (non-breaking change)
- [x] New feature (non-breaking change which adds functionality)
- [x] Breaking change (fix or feature that would change existing
functionality)
- [ ] Bug fix (non-breaking change which fixes an issue)

## Checklist

<!-- Go over all the following points, and put an `x` in all the boxes
that apply. -->

- [ ] I am familiar with the [contributing
guidelines](../docs/CONTRIBUTING.md)
- [ ] I have followed the code style of the project
- [ ] I have added tests to cover my changes
- [ ] I have updated the documentation accordingly
- [ ] This PR is a result of pair or mob programming

---

## Sensitive Information Declaration

To ensure the utmost confidentiality and protect your and others
privacy, we kindly ask you to NOT including [PII (Personal Identifiable
Information) / PID (Personal Identifiable
Data)](https://digital.nhs.uk/data-and-information/keeping-data-safe-and-benefitting-the-public)
or any other sensitive data in this PR (Pull Request) and the codebase
changes. We will remove any PR that do contain any sensitive
information. We really appreciate your cooperation in this matter.

- [x] I confirm that neither PII/PID nor sensitive data are included in
this PR and the codebase changes.
  • Loading branch information
regularfry authored Apr 15, 2024
1 parent 5a9f3f2 commit 5be34c4
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 147 deletions.
203 changes: 185 additions & 18 deletions docs/developer-guides/Scripting_Terraform.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,35 +62,202 @@ Here are some key features built into this repository's Terraform module:

### Quick start

Run the example:
The Repository Template assumes that you will be constructing the bulk of your infrastructure in `infrastructure/modules` as generic deployment configuration, which you will then compose into environment-specific modules, each stored in their own directory under `infrastructure/environments`. Let's create a simple deployable thing, and configure an S3 bucket. We'll make the name of the bucket a variable, so that each environment can have its own.

Open the file `infrastructure/modules/private_s3_bucket/main.tf`, and put this in it:

```terraform
# Define the provider
provider "aws" {
region = "eu-west-2"
}
variable "bucket_name" {
description = "Name of the bucket, which can be different per environment"
}
resource "aws_s3_bucket" "my_bucket" {
bucket = var.bucket_name # Replace with your desired bucket name
acl = "private"
}
```

Note that the variable has been given no value. This is intentional, and allows us to pass the bucket name in as a parameter from the environment.

Now, we're going to define two deployment environments: `dev`, and `test`. Run this:

```bash
mkdir -p infrastructure/environments/{dev,test}
```

It is important that the directory names match your environment names.

Now, let's create the environment definition files. Open `infrastructure/environments/dev/main.tf` and copy in:

```terraform
module "dev_environment" {
source = "../../modules/private_s3_bucket"
bucket_name = "nhse-ee-my-fancy-bucket"
}
```

Some things to note:

- The `source` path is relative to the directory that the `main.tf` file is in. When `terraform` runs, it will `chdir` to that directory first, before doing anything else.
- The `module` name, `"dev_environment"` here, can be anything. Module names are only scoped to the file they're in, so you don't need to follow any particular convention here.
- The `bucket_name` is going to end up as the bucket name in AWS. It wants to be meaningful to you, and you need to pick your own. The framework doesn't constrain your choice, but remember that AWS needs them to be globally unique and if you steal `"nhse-ee-my-fancy-bucket"` then I can't test these docs and then I will be sad.

Let's create our `test` environment now. Open `infrastructure/environments/test/main.tf` and copy in:

```terraform
module "test_environment" {
source = "../../modules/private_s3_bucket"
bucket_name = "nhse-ee-my-fancy-test-bucket"
}
```

We have changed the bucket name here. In this example, I am making no assumptions as to how your AWS accounts are set up. If you intend for your development and test infrastructure to be in the same AWS account (perhaps by necessity, for organisational reasons) and you need to separate them by a naming convention, the framework can support that.

Now we have our modules and our environments configured, we need to initialise each of them. Run these two commands:

```bash
TF_ENV=dev make terraform-init
TF_ENV=test make terraform-init
```

Each invocation will download the `terraform` dependencies we need. The `TF_ENV` name we give to each invocation is the name of the environment, and must match the directory name we chose under `infrastructure/environments` so that `make` gives the right parameters to `terraform`.

We are now ready to try deploying to AWS, from our local environment.

I am going to assume that you have an `~/.aws/credentials` file set up with a separate profile for each environment that you want to use, called `my-test-environment` and `my-dev-environment`. They might have the same credential values in them, in which case `terraform` will create the resources in the same account; or you might have them set up to deploy to different accounts. Either would work.

Run the following:

```shell
TF_ENV=dev AWS_PROFILE=my-dev-environment make terraform-plan
```

If all is working correctly (and you may need to do a round of `aws sso login` first), you should see this output:

```text
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# module.dev_environment.aws_s3_bucket.my_bucket will be created
+ resource "aws_s3_bucket" "my_bucket" {
+ acceleration_status = (known after apply)
+ acl = "private"
+ arn = (known after apply)
+ bucket = "my-dev-bucket"
+ bucket_domain_name = (known after apply)
+ bucket_prefix = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ force_destroy = false
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ object_lock_enabled = (known after apply)
+ policy = (known after apply)
+ region = (known after apply)
+ request_payer = (known after apply)
+ tags_all = (known after apply)
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
```

No errors found, so we can now create the bucket:

```shell
# AWS console access setup
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
$ TF_ENV=dev AWS_PROFILE=my-dev-environment make terraform-apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create

Terraform will perform the following actions:

# module.dev_environment.aws_s3_bucket.my_bucket will be created
+ resource "aws_s3_bucket" "my_bucket" {
+ acceleration_status = (known after apply)
+ acl = (known after apply)
+ arn = (known after apply)
+ bucket = "nhse-ee-my-dev-bucket"
+ bucket_domain_name = (known after apply)
+ bucket_prefix = (known after apply)
+ bucket_regional_domain_name = (known after apply)
+ force_destroy = false
+ hosted_zone_id = (known after apply)
+ id = (known after apply)
+ object_lock_enabled = (known after apply)
+ policy = (known after apply)
+ region = (known after apply)
+ request_payer = (known after apply)
+ tags_all = (known after apply)
+ website_domain = (known after apply)
+ website_endpoint = (known after apply)
}

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.

Enter a value: yes

module.dev_environment.aws_s3_bucket.my_bucket: Creating...
module.dev_environment.aws_s3_bucket.my_bucket: Creation complete after 1s [id=nhse-ee-my-dev-bucket]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

```

You will notice here that I needed to confirm the action to `terraform` manually. If you don't want to do that, you can pass the `-auto-approve` option to `terraform` like this:

```shell
$ make terraform-example-provision-aws-infrastructure
TF_ENV=dev AWS_PROFILE=my-dev-environment make terraform-apply opts="-auto-approve"
```

Initializing the backend..
...
Plan: 5 to add, 0 to change, 0 to destroy.
Saved the plan to: terraform.tfplan
To perform exactly these actions, run the following command to apply:
terraform apply "terraform.tfplan"
If you check the contents of your AWS account, you should see your new bucket:

```shell
$ aws s3 ls --profile my-dev-environment
...
Apply complete! Resources: 5 added, 0 changed, 0 destroyed.
2024-03-01 16:33:55 nhse-ee-my-dev-bucket
```

$ make terraform-example-destroy-aws-infrastructure
Now I don't want to leave that there, so I will run the corresponding `destroy` command to get rid of it:

...
Plan: 0 to add, 0 to change, 5 to destroy.
...
Apply complete! Resources: 0 added, 0 changed, 5 destroyed.
```shell
$ TF_ENV=dev AWS_PROFILE=my-dev-environment make terraform-destroy opts="-auto-approve"
module.dev_environment.aws_s3_bucket.my_bucket: Refreshing state... [id=nhse-ee-my-dev-bucket]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy

Terraform will perform the following actions:

# module.dev_environment.aws_s3_bucket.my_bucket will be destroyed
...(more terraform output not shown because it's boring, but the end result is the bucket going away)
```
To create your `test` environment, you run the same commands with `test` where previously you had `dev`:
```shell
TF_ENV=test AWS_PROFILE=my-test-environment make terraform-apply opts="-auto-approve"
```
To use the same `terraform` files in a GitHub action, see the docs [here](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services).
### Your stack implementation
Always follow [best practices for using Terraform](https://cloud.google.com/docs/terraform/best-practices-for-terraform) while providing infrastructure as code (IaC) for your service.
Expand Down
File renamed without changes.
41 changes: 0 additions & 41 deletions scripts/terraform/examples/terraform-state-aws-s3/.gitignore

This file was deleted.

46 changes: 0 additions & 46 deletions scripts/terraform/examples/terraform-state-aws-s3/main.tf

This file was deleted.

3 changes: 0 additions & 3 deletions scripts/terraform/examples/terraform-state-aws-s3/provider.tf

This file was deleted.

This file was deleted.

8 changes: 0 additions & 8 deletions scripts/terraform/examples/terraform-state-aws-s3/versions.tf

This file was deleted.

27 changes: 5 additions & 22 deletions scripts/terraform/terraform.mk
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
# Custom implementation - implementation of a make target should not exceed 5 lines of effective code.
# In most cases there should be no need to modify the existing make targets.

TF_ENV ?= dev
STACK ?= ${stack}
TERRAFORM_STACK ?= $(or ${STACK}, infrastructure/environments/${TF_ENV})
dir ?= ${TERRAFORM_STACK}

terraform-init: # Initialise Terraform - optional: terraform_dir|dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], terraform_opts|opts=[options to pass to the Terraform init command, default is none/empty] @Development
make _terraform cmd="init" \
dir=$(or ${terraform_dir}, ${dir}) \
Expand Down Expand Up @@ -41,8 +46,6 @@ clean:: # Remove Terraform files (terraform) - optional: terraform_dir|dir=[path
opts=$(or ${terraform_opts}, ${opts})

_terraform: # Terraform command wrapper - mandatory: cmd=[command to execute]; optional: dir=[path to a directory where the command will be executed, relative to the project's top-level directory, default is one of the module variables or the example directory, if not set], opts=[options to pass to the Terraform command, default is none/empty]
# 'TERRAFORM_STACK' is passed to the functions as environment variable
TERRAFORM_STACK=$(or ${TERRAFORM_STACK}, $(or ${terraform_stack}, $(or ${STACK}, $(or ${stack}, scripts/terraform/examples/terraform-state-aws-s3))))
dir=$(or ${dir}, ${TERRAFORM_STACK})
source scripts/terraform/terraform.lib.sh
terraform-${cmd} # 'dir' and 'opts' are accessible by the function as environment variables, if set
Expand All @@ -55,23 +58,6 @@ terraform-shellscript-lint: # Lint all Terraform module shell scripts @Quality
file=$${file} scripts/shellscript-linter.sh
done

# ==============================================================================
# Module tests and examples - please DO NOT edit this section!

terraform-example-provision-aws-infrastructure: # Provision example of AWS infrastructure @ExamplesAndTests
make terraform-init
make terraform-plan opts="-out=terraform.tfplan"
make terraform-apply opts="-auto-approve terraform.tfplan"

terraform-example-destroy-aws-infrastructure: # Destroy example of AWS infrastructure @ExamplesAndTests
make terraform-destroy opts="-auto-approve"

terraform-example-clean: # Remove Terraform example files @ExamplesAndTests
dir=$(or ${dir}, ${TERRAFORM_STACK})
source scripts/terraform/terraform.lib.sh
terraform-clean
rm -f ${TERRAFORM_STACK}/.terraform.lock.hcl

# ==============================================================================
# Configuration - please DO NOT edit this section!

Expand All @@ -85,9 +71,6 @@ ${VERBOSE}.SILENT: \
clean \
terraform-apply \
terraform-destroy \
terraform-example-clean \
terraform-example-destroy-aws-infrastructure \
terraform-example-provision-aws-infrastructure \
terraform-fmt \
terraform-init \
terraform-install \
Expand Down

0 comments on commit 5be34c4

Please sign in to comment.