Skip to content

Commit a711f5d

Browse files
Merge pull request #1120 from NHSDigital/feature/made14-NRL-1875-interweave-perftest-tweaks
NRL-1875 Add AWS bastion and tweak perfs tests for 15m pointer perftests
2 parents 6d08175 + 4a8e37f commit a711f5d

File tree

12 files changed

+561
-3
lines changed

12 files changed

+561
-3
lines changed

terraform/bastion/Makefile

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
ENV ?= dev
3+
TF_ARGS ?=
4+
ENV_ACCOUNT_NAME ?= $(shell ../../scripts/get-account-name-for-env.sh $(ENV))
5+
ENV_ACCOUNT_ID ?= $(shell aws secretsmanager get-secret-value --secret-id nhsd-nrlf--mgmt--$(ENV_ACCOUNT_NAME)-account-id --query SecretString --output text)
6+
BASTION_VPC_NAME ?= nhsd-nrlf--$(ENV_ACCOUNT_NAME)-vpc
7+
BASTION_SUBNET_NAME ?= nhsd-nrlf--$(ENV_ACCOUNT_NAME)-privsubnet
8+
POINTERS_TABLE_NAME ?= nhsd-nrlf--$(ENV)-pointers-table
9+
S3_METADATA_BUCKET_NAME ?= nhsd-nrlf--$(ENV)-metadata
10+
11+
export ENV ENV_ACCOUNT_NAME POINTERS_TABLE_NAME
12+
13+
help: ## Show this help message
14+
@echo "Usage: make [target]"
15+
@echo
16+
@echo "where [target] can be:"
17+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-30s\033[0m %s\n", $$1, $$2}'
18+
19+
plan-ro: ## Plan the Terraform changes for a bastion that can perform read-only operations
20+
terraform plan \
21+
-var assume_account=$(ENV_ACCOUNT_ID) \
22+
-var assume_role=terraform \
23+
-var vpc_name=$(BASTION_VPC_NAME) \
24+
-var subnet_name=$(BASTION_SUBNET_NAME) \
25+
-var dynamodb_table_name=$(POINTERS_TABLE_NAME) \
26+
-var s3_metadata_bucket_name=$(S3_METADATA_BUCKET_NAME) \
27+
$(TF_ARGS) \
28+
-out=./bastion.tfplan
29+
30+
plan-rw: ## Plan the Terraform changes for a bastion that can perform write operations
31+
terraform plan \
32+
-var assume_account=$(ENV_ACCOUNT_ID) \
33+
-var assume_role=terraform \
34+
-var vpc_name=$(BASTION_VPC_NAME) \
35+
-var subnet_name=$(BASTION_SUBNET_NAME) \
36+
-var dynamodb_table_name=$(POINTERS_TABLE_NAME) \
37+
-var allow_dynamodb_table_write=true \
38+
-var s3_metadata_bucket_name=$(S3_METADATA_BUCKET_NAME) \
39+
$(TF_ARGS) \
40+
-out=./bastion.tfplan
41+
42+
destroy: ## Destroy the bastion
43+
terraform destroy \
44+
-var assume_account=$(ENV_ACCOUNT_ID) \
45+
-var assume_role=terraform \
46+
-var vpc_name=$(BASTION_VPC_NAME) \
47+
-var subnet_name=$(BASTION_SUBNET_NAME) \
48+
-var dynamodb_table_name=$(POINTERS_TABLE_NAME) \
49+
-var s3_metadata_bucket_name=$(S3_METADATA_BUCKET_NAME) \
50+
$(TF_ARGS)
51+
52+
ssh-connection: ## Connect to the bastion via SSH
53+
@echo "Connecting to bastion via SSM connect...."
54+
AWS_ACCOUNT_ID=$(ENV_ACCOUNT_ID) \
55+
AWS_ROLE_NAME=terraform \
56+
./scripts/start-bastion-connection.sh $(shell terraform output -raw instance_id)

terraform/bastion/README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# NRLF AWS Bastion Infrastructure
2+
3+
This directory contains the infrastructure for the NRLF AWS Bastion. This is a host that is deployed into the NRLF AWS accounts and can be used to run operations tasks against the resources in that account. For example, to run a report that reads from the DynamoDB pointers table.
4+
5+
## Prerequisites
6+
7+
Before deploying a bastion, you will need:
8+
9+
- An AWS account that has already been bootstrapped, as described in [bootstrap/README.md](../bootstrap/README.md) and has the account-wide infrastructure deployed as described in [account-wide-infrastructure/README.md](../account-wide-infrastructure/README.md). This is a one-time account setup step.
10+
- Your CLI configured to allow authentication to your AWS account
11+
12+
## Deploying a bastion
13+
14+
The bastions are emphemeral resources that should be deploy when you need them.
15+
16+
To deploy a bastion, you will first need to login to the AWS mgmt account on the CLI.
17+
18+
Then, initialise the Terraform workspace with:
19+
20+
```
21+
terraform init
22+
```
23+
24+
If you want a read-only bastion (can only READ from the pointers table), plan the deployment like this:
25+
26+
```
27+
make plan-ro
28+
```
29+
30+
If you want a read-write bastion (can READ and WRITE from the pointers table), plan the deployment like this:
31+
32+
```
33+
make plan-rw
34+
```
35+
36+
Once you're happy with your planned changes, you can apply them with:
37+
38+
```
39+
terraform apply ./bastion.tfplan
40+
```
41+
42+
## Using the bastion
43+
44+
Once the bastion is deployed, you can connect to it via SSH with:
45+
46+
```
47+
make ssh-connection
48+
```
49+
50+
Once connected successfully, you will be at the SSM `$` prompt. To switch to the `nrlf_ops` user, run this command:
51+
52+
```
53+
sudo su - nrlf_ops
54+
```
55+
56+
Once switched to the `nrlf_ops` user, you can use the bastion and the installed tooling:
57+
58+
- `cd ./NRLF` - the NRLF repo (cloned from Github `develop`)
59+
- `aws`
60+
- `git`
61+
- `python` and `poetry`
62+
63+
see [user-data.sh](./scripts/user-data.sh) for exactly what's installed on there.
64+
65+
### Troubleshooting the bastion
66+
67+
#### I cannot connect to the bastion
68+
69+
If you're running the `make ssh-connection` and are seeing this error:
70+
71+
```
72+
$ make ssh-connection
73+
....
74+
An error occurred (TargetNotConnected) when calling the StartSession operation: i-06ff25164f004bee4 is not connected.
75+
make: *** [Makefile:54: ssh-connection] Error 254
76+
$
77+
```
78+
79+
If you've just created a new bastion, it may be that it hasn't started yet. Log in to the AWS console to see the state of the EC2 instance. Press the "Connect" button in the console and choose the SSM tab to see if things are working ok.
80+
81+
If the EC2 instance is running and the console looks ok, check you have defined the correct ENV param for the installed bastion.
82+
83+
#### The `nrlf_ops` using is missing
84+
85+
If you're getting this error:
86+
87+
```
88+
$ sudo su - nrlf_ops
89+
su: user some_other does not exist or the user entry does not contain all the required fields
90+
$
91+
```
92+
93+
If you've just created a new bastion, you may need to wait a little until the cloud-init script has finished. You can check the status of this process with:
94+
95+
```
96+
sudo tail -f /var/log/cloud-init-output.log
97+
```
98+
99+
#### The bastion cannot access an AWS resource that I want to use
100+
101+
If you're trying to access an AWS resource from the bastion and are getting an access denied error, it may be that the bastion's IAM role does not have the required permissions.
102+
103+
You can check the role in the AWS console to work out if things are missing and can edit it there too for immediate access to the resources you need.
104+
105+
If you want to permenantly grant new access to the bastion, you can add a policy and attach it to the EC2 instance in [iam.tf](iam.tf)
106+
107+
#### A tool I need is missing
108+
109+
If there's a tool missing from the instance, you can add it using `apt` or via `asdf` as/when you need it.
110+
111+
If you want a new tool to be deployed onto all bastions in the future too, add it to the [user-data.sh](./scripts/user-data.sh) script.

terraform/bastion/data.tf

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
data "aws_caller_identity" "current" {}
2+
3+
data "aws_vpc" "bastion_vpc" {
4+
filter {
5+
name = "tag:Name"
6+
values = [var.vpc_name]
7+
}
8+
}
9+
10+
data "aws_subnet" "bastion_subnet" {
11+
filter {
12+
name = "tag:Name"
13+
values = [var.subnet_name]
14+
}
15+
16+
filter {
17+
name = "vpc-id"
18+
values = [data.aws_vpc.bastion_vpc.id]
19+
}
20+
}
21+
22+
data "aws_dynamodb_table" "dynamodb-table" {
23+
count = var.dynamodb_table_name != null ? 1 : 0
24+
name = var.dynamodb_table_name
25+
}
26+
27+
data "aws_kms_key" "dynamodb-table-key" {
28+
count = var.dynamodb_table_name != null ? 1 : 0
29+
key_id = "alias/${data.aws_dynamodb_table.dynamodb-table[0].name}-key"
30+
}
31+
32+
data "aws_ami" "bastion_ubuntu_ami" {
33+
most_recent = true
34+
owners = ["amazon"]
35+
36+
filter {
37+
name = "name"
38+
values = [var.ami_name_match]
39+
}
40+
}

terraform/bastion/ec2.tf

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
resource "aws_instance" "node" {
2+
ami = data.aws_ami.bastion_ubuntu_ami.id
3+
instance_type = var.instance_type
4+
subnet_id = data.aws_subnet.bastion_subnet.id
5+
key_name = aws_key_pair.ec2_key_pair.key_name
6+
associate_public_ip_address = false
7+
iam_instance_profile = aws_iam_instance_profile.instance-profile.name
8+
9+
user_data = file("./scripts/user-data.sh")
10+
11+
tags = {
12+
Name = "${local.prefix}-node"
13+
}
14+
}
15+
16+
resource "aws_security_group" "node-sg" {
17+
name = "${local.prefix}-node-sg"
18+
description = "Security group for ${aws_instance.node.id} node host"
19+
vpc_id = data.aws_vpc.bastion_vpc.id
20+
21+
egress {
22+
from_port = 0
23+
to_port = 0
24+
protocol = "-1"
25+
cidr_blocks = ["0.0.0.0/0"]
26+
}
27+
}
28+
29+
resource "tls_private_key" "instance_key_pair" {
30+
algorithm = "RSA"
31+
}
32+
33+
resource "aws_key_pair" "ec2_key_pair" {
34+
key_name = "${local.prefix}-ec2-key-pair"
35+
public_key = tls_private_key.instance_key_pair.public_key_openssh
36+
}
37+
38+
resource "aws_secretsmanager_secret" "bastion_ssh_key_secret" {
39+
name_prefix = "${local.prefix}-ssh-key"
40+
description = "Private SSH key for accessing the bastion host"
41+
}
42+
43+
resource "aws_secretsmanager_secret_version" "bastion_ssh_key_secret_version" {
44+
secret_id = aws_secretsmanager_secret.bastion_ssh_key_secret.id
45+
secret_string = tls_private_key.instance_key_pair.private_key_pem
46+
}

terraform/bastion/iam.tf

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
resource "aws_iam_role" "instance-role" {
2+
name = "${local.prefix}-instance-role"
3+
assume_role_policy = jsonencode({
4+
Version = "2012-10-17"
5+
Statement = [
6+
{
7+
Action = "sts:AssumeRole"
8+
Effect = "Allow"
9+
Principal = {
10+
Service = "ec2.amazonaws.com"
11+
}
12+
}
13+
]
14+
})
15+
}
16+
17+
resource "aws_iam_instance_profile" "instance-profile" {
18+
name = "${local.prefix}-instance-profile"
19+
role = aws_iam_role.instance-role.name
20+
}
21+
22+
resource "aws_iam_policy" "dynamodb-table-read" {
23+
count = var.dynamodb_table_name != null ? 1 : 0
24+
name = "${local.prefix}-allow-table-read"
25+
description = "Read the ${var.dynamodb_table_name} table"
26+
policy = jsonencode({
27+
Version = "2012-10-17"
28+
Statement = [
29+
{
30+
Action = [
31+
"kms:Decrypt",
32+
"kms:DescribeKey"
33+
]
34+
Effect = "Allow"
35+
Resource = [
36+
data.aws_kms_key.dynamodb-table-key[0].arn
37+
]
38+
},
39+
{
40+
Effect = "Allow"
41+
Action = [
42+
"dynamodb:Query",
43+
"dynamodb:Scan",
44+
"dynamodb:GetItem",
45+
],
46+
Resource = [
47+
"${data.aws_dynamodb_table.dynamodb-table[0].arn}"
48+
]
49+
}
50+
]
51+
})
52+
}
53+
54+
resource "aws_iam_policy" "dynamodb-table-write" {
55+
count = var.allow_dynamodb_table_write && var.dynamodb_table_name != null ? 1 : 0
56+
name = "${local.prefix}-allow-table-write"
57+
description = "Write to the ${var.dynamodb_table_name} table"
58+
policy = jsonencode({
59+
Version = "2012-10-17"
60+
Statement = [
61+
{
62+
Action = [
63+
"kms:Encrypt",
64+
"kms:GenerateDataKey"
65+
]
66+
Effect = "Allow"
67+
Resource = [
68+
data.aws_kms_key.dynamodb-table-key[0].arn
69+
]
70+
},
71+
{
72+
Effect = "Allow"
73+
Action = [
74+
"dynamodb:PutItem",
75+
"dynamodb:UpdateItem",
76+
"dynamodb:DeleteItem",
77+
"dynamodb:BatchWriteItem"
78+
],
79+
Resource = [
80+
"${data.aws_dynamodb_table.dynamodb-table[0].arn}"
81+
]
82+
}
83+
]
84+
})
85+
}
86+
87+
resource "aws_iam_policy" "s3-metadata-bucket-readwrite" {
88+
name = "${local.prefix}-allow-s3-metadata-bucket-readwrite"
89+
description = "Read and write access to the S3 metadata bucket"
90+
policy = jsonencode({
91+
Version = "2012-10-17"
92+
Statement = [
93+
{
94+
Effect = "Allow"
95+
Action = [
96+
"s3:GetObject",
97+
"s3:ListBucket",
98+
"s3:HeadObject",
99+
"s3:PutObject",
100+
"s3:PutObjectAcl",
101+
"s3:DeleteObject"
102+
],
103+
Resource = [
104+
"arn:aws:s3:::${var.s3_metadata_bucket_name}",
105+
"arn:aws:s3:::${var.s3_metadata_bucket_name}/*"
106+
]
107+
}
108+
]
109+
})
110+
}
111+
112+
resource "aws_iam_role_policy_attachment" "ec2_role_policy_ssm" {
113+
role = aws_iam_role.instance-role.name
114+
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
115+
}
116+
117+
118+
resource "aws_iam_role_policy_attachment" "dynamodb-table-read-attachment" {
119+
count = var.dynamodb_table_name != null ? 1 : 0
120+
role = aws_iam_role.instance-role.name
121+
policy_arn = aws_iam_policy.dynamodb-table-read[0].arn
122+
}
123+
124+
resource "aws_iam_role_policy_attachment" "dynamodb-table-write-attachment" {
125+
count = var.allow_dynamodb_table_write && var.dynamodb_table_name != null ? 1 : 0
126+
role = aws_iam_role.instance-role.name
127+
policy_arn = aws_iam_policy.dynamodb-table-write[0].arn
128+
}
129+
130+
resource "aws_iam_role_policy_attachment" "s3-metadata-bucket-readwrite-attachment" {
131+
role = aws_iam_role.instance-role.name
132+
policy_arn = aws_iam_policy.s3-metadata-bucket-readwrite.arn
133+
}

terraform/bastion/locals.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
locals {
2+
region = "eu-west-2"
3+
project = "nhsd-nrlf--${var.bastion_name}-bastion"
4+
prefix = local.project
5+
}

0 commit comments

Comments
 (0)