Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove database subnet and add architecture diagram #63

Merged
merged 10 commits into from
Aug 10, 2023
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ Deploy XNAT on AWS using Terraform and Ansible.

Terraform is used to create the infrastructure on AWS and Ansible is then used to configure the instances for XNAT deployment.

Below is an overview of the infrastructure that will be created. See the notes on [provisioning the infrastructure](provision/README.md) for a more detailed description.

<p align="center" width="100%">
<img src="assets/xnat-aws-architecture.png" alt="XNAT-AWS architecture" width="70%" >
</p>

## Requirements

- An [AWS account](https://portal.aws.amazon.com/billing/signup?refid=em_127222&redirect_url=https%3A%2F%2Faws.amazon.com%2Fregistration-confirmation#/start/email)
Expand Down Expand Up @@ -74,7 +80,7 @@ To destroy the infrastructure, go to the `xnat-aws/provision` directory and type
terraform destroy
```

# AWS cost estimate
## AWS cost estimate

[It is estimated](provision/aws-cost-estimate.pdf) the AWS resources will cost approximately $90 USD per month.

Expand Down
Binary file added assets/xnat-aws-architecture.png
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't ask how long this took 😂

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/xnat-aws-architecture.pptx
Binary file not shown.
1 change: 1 addition & 0 deletions assets/xnat-aws-architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions credentials/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ The generated private and public SSH keys will be saved in `xnat-aws/ssh/aws-rsa
The generated passwords are used to create and encrypt an Ansible vault. The vault is saved in `xnat-aws/configure/group_vars/all/vault` and the password to decrypt it is in `xnat-aws/configure/.vault_password`.

Once you have generated these credentials, you can [create the infrastructure on AWS](../provision/README.md).

Note, you only need to generate these credentials once - you can then create and destroy infrastructure on AWS re-using the same set of credentials.
91 changes: 80 additions & 11 deletions provision/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,90 @@
# Create infrastructure on AWS using Terraform

The Terraform scripts will create the following:
The Terraform scripts will create the following infrastructure on AWS:

- a Virtual Private Cloud (VPC) with one public subnet, 2 private subnets and required security groups
<p align="center" width="100%">
<img src="../assets/xnat-aws-architecture.png" alt="XNAT-AWS architecture" width="70%" >
</p>

- a Virtual Private Cloud (VPC), a public subnet, and two private subnets
- two EC2 instances - `xnat_web` and `xnat_cserv` for the web server and [XNAT Container Service](https://wiki.xnat.org/container-service/), respectively
- an RDS instance - `xnat_db` - for the PostgreSQL database
- an EFS instance used to store data uploaded to xnat, this volume is shared beetween the web server
and container service
- an RDS instance - `xnat_db` - for managing the PostgreSQL database
- an EFS instance used to store data uploaded to xnat; this volume is mounted on both the web server and Container Service server
- security groups to manage access to the servers

<details><summary>Notes on the infrastructure that is created</summary>

### Instance types

The smallest instance types (`t2.nano`) do not provide enough RAM for running XNAT. We have found that we need to use `t3.medium` instances for the Container Service and database, and `t3.large` instances for the web server, to prevent the site from crashing when uploading data or running containers.

You can change the instance type used by setting `ec2_instance_type` in your `xnat-aws/provision/terraform.tfvars` file, e.g.:

```terraform
ec2_instance_types = {
"xnat_web" = "t3.large"
"xnat_db" = "db.t3.large"
"xnat_cserv" = "t3.large"
}
```

You can also increase the amount of RAM reserved for Java (and thus XNAT) in the Ansible configuration. In the file `xnat-aws/configure/group_vars/web/vars/tomcat.yml` you would need to modify the `java.mem` variable, e.g.:

```yaml
java_mem:
Xms: "512M"
Xmx: "6G"
MetaspaceSize: "300M"
```

### Subnets and availability zones

We create a public and private subnet in a single availability zone, and this is where all resources are deployed. However, we also create a second private subnet in a second availability zone, but nothing is deployed here.

This is due a [requirement of RDS to have subnets defined in at least two availability zones, even if you're deploying in a single availability zone](https://repost.aws/questions/QUf7DbNMKFQmWiRg8oMB0obA/why-must-an-rds-always-have-two-subnets#ANurWZpEHBRPa1SwrtRh9Q9w). but deploy instance in only two subnets in a single availability zone.

### Security groups and access

We create a security group for each instance - the web server, database, and container service.

#### Web server security group

## Warning
The web server security group allows SSH, HTTP, and HTTPS access from the IP address from which Terraform was run (i.e. your own IP address). Access is restricted for security reasons.

SSH access is required to configure the server using Ansible.

#### Database security group

The database security group only allows access to port 5432 (for connecting to the database). Access is limited to the web server only - all other connections will be refused.

#### Container Service security group

The Container Service security group allows SSH access from the IP address from which Terraform was ran. It also allows access to port 2376 (for the Container Service) from the web server only.

SSH access is required to configure the server using Ansible.

#### Extending access to other IP addresses

HTTP access to the web server can be extended to other IP addresses through the `extend_http_cidr`
variable. For example, to allow access from all IP addresses, in the file `xnat-aws/provision/terraform.tfvar`:

```
extend_http_cidr = [
"0.0.0.0/0",
]
```

Similarly, SSH access to the web server and Container Service server can be extended to other IP addresses through the `extend_SSH` variable:

```
extend_ssh_cidr = [
"0.0.0.0/0",
]
```

A single, **public** subnet is created in the VPC. This is to make it straightforward to configure
the instances with Ansible. However, this means that the instances will have public IP addresses and
may be vulnerable to attack. To protect against this:
However, extending access to all IP addresses is not recommended.

- only the web server has HTTP and HTTPS ports open to the internet
- SSH access is only allowed from the IP address of the user that creates the infrastructure
</details>

## Usage

Expand Down
37 changes: 18 additions & 19 deletions provision/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ module "setup_vpc" {

# NOTE: database subnets only without private subnets doesn't work
# cfr. https://github.com/terraform-aws-modules/terraform-aws-vpc/issues/944
p-j-smith marked this conversation as resolved.
Show resolved Hide resolved
azs = var.availability_zones
public_subnets = var.subnet_cidr_blocks["public"]
private_subnets = var.subnet_cidr_blocks["private"]
database_subnets = var.subnet_cidr_blocks["database"]
azs = var.availability_zones
public_subnets = var.subnet_cidr_blocks["public"]
private_subnets = var.subnet_cidr_blocks["private"]

# Assign public IP address to subnet
map_public_ip_on_launch = true
Expand All @@ -39,17 +38,17 @@ module "setup_vpc" {
manage_default_security_group = false
manage_default_route_table = false

public_dedicated_network_acl = true
private_dedicated_network_acl = true
create_database_subnet_group = true
enable_nat_gateway = false
enable_vpn_gateway = false
public_dedicated_network_acl = true
private_dedicated_network_acl = true
create_database_subnet_group = false
create_database_subnet_route_table = false
create_database_internet_gateway_route = false
enable_nat_gateway = false
enable_vpn_gateway = false

# override default names of the resources
public_subnet_names = ["xnat-public"]
private_subnet_names = ["xnat-private-1", "xnat-private-2"]
database_subnet_names = ["xnat-db-1", "xnat-db-2"]
database_subnet_group_name = "xnat-db"
public_subnet_names = ["xnat-public"]
private_subnet_names = ["xnat-private-1", "xnat-private-2"]

default_security_group_name = "default-xnat-sg"
default_vpc_name = "default-xnat-vpc"
Expand Down Expand Up @@ -105,12 +104,12 @@ module "efs" {
module "database" {
source = "./modules/database"

name = "xnat_db"
vpc_id = module.setup_vpc.vpc_id
instance_type = var.ec2_instance_types["xnat_db"]
availability_zone = var.availability_zones[0]
db_subnet_group_name = module.setup_vpc.database_subnet_group_name
webserver_sg_id = module.web_server.webserver_sg_id
name = "xnat_db"
vpc_id = module.setup_vpc.vpc_id
instance_type = var.ec2_instance_types["xnat_db"]
availability_zone = var.availability_zones[0]
subnet_ids = module.setup_vpc.private_subnets
webserver_sg_id = module.web_server.webserver_sg_id
}

# Copy public key to AWS
Expand Down
12 changes: 11 additions & 1 deletion provision/modules/database/database.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
resource "aws_db_subnet_group" "xnat-db" {
name = "xnat-db"
subnet_ids = var.subnet_ids

tags = {
Name = var.name
}
}

resource "aws_db_instance" "db" {
identifier_prefix = "${local.identifier_prefix}-"
db_name = local.db_name
Expand All @@ -14,7 +23,8 @@ resource "aws_db_instance" "db" {
# https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance#managed-master-passwords-via-secrets-manager-default-kms-key
password = random_password.db_credentials.result

db_subnet_group_name = var.db_subnet_group_name
availability_zone = var.availability_zone
db_subnet_group_name = aws_db_subnet_group.xnat-db.name
vpc_security_group_ids = [aws_security_group.db.id]

skip_final_snapshot = true
Expand Down
6 changes: 3 additions & 3 deletions provision/modules/database/vars.tf
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ variable "availability_zone" {
default = "eu-west-2a"
}

variable "db_subnet_group_name" {
type = string
description = "The subnet ID to use for the RDS instance"
variable "subnet_ids" {
type = list(string)
description = "List of subnet ids to deploy RDS instances in"
}

variable "webserver_sg_id" {
Expand Down
2 changes: 0 additions & 2 deletions provision/terraform.tfvars_sample
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ vpc_cidr_block = "192.168.0.0/16" # 192.168.0.0 to 192.168.255.255
subnet_cidr_blocks = {
"public" = ["192.168.56.0/24"] # 192.168.56.0 to 192.168.56.255
"private" = ["192.168.100.0/24", "192.168.101.0/24"]
"database" = ["192.168.110.0/24", "192.168.120.0/24"]
}

# EC2 private IPs
instance_private_ips = {
"xnat_web" = "192.168.56.10"
"xnat_db" = "192.168.56.11"
"xnat_cserv" = "192.168.56.14"
}
smtp_private_ip = "192.168.56.101"
Expand Down
6 changes: 2 additions & 4 deletions provision/vars.tf
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,8 @@ variable "subnet_cidr_blocks" {
description = "CIDR block for the VPC and subnets"
type = map(any)
default = {
"public" = ["192.168.56.0/24"] # 192.168.56.0 to 192.168.56.255
"private" = ["192.168.100.0/24", "192.168.101.0/24"]
"database" = ["192.168.110.0/24", "192.168.120.0/24"]
"public" = ["192.168.56.0/24"] # 192.168.56.0 to 192.168.56.255
"private" = ["192.168.100.0/24", "192.168.101.0/24"]
}
}

Expand All @@ -34,7 +33,6 @@ variable "instance_private_ips" {
description = "Private IP addresses for each instance"
default = {
"xnat_web" = "192.168.56.10"
"xnat_db" = "192.168.100.11"
"xnat_cserv" = "192.168.56.14"
}
}
Expand Down