Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions bin/data-replication
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash

# Data replication script - creates read-only database role and keeps container running

set -e

echo "Starting data replication setup..."

if [ -z "$READ_ONLY_DB_PASSWORD" ]; then
echo "Error: READ_ONLY_DB_PASSWORD environment variable is not set"
exit 1
fi

echo "Creating read-only database role..."

bundle exec rails runner "
begin
ActiveRecord::Base.connection.execute(\"
DO \$\$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'grafana_read_only') THEN
CREATE ROLE grafana_read_only WITH LOGIN PASSWORD '#{ENV['READ_ONLY_DB_PASSWORD']}';
ELSE
ALTER ROLE grafana_read_only WITH PASSWORD '#{ENV['READ_ONLY_DB_PASSWORD']}';
END IF;
END
\$\$;
\")

ActiveRecord::Base.connection.execute(\"
GRANT CONNECT ON DATABASE #{ActiveRecord::Base.connection.current_database} TO grafana_read_only;
GRANT USAGE ON SCHEMA public TO grafana_read_only;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana_read_only;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO grafana_read_only;
\")

puts 'Read-only role created/updated successfully'
rescue => e
puts \"Error creating read-only role: \#{e.message}\"
exit 1
end
"

echo "Data replication setup completed. Keeping container running..."

exec tail -f /dev/null
3 changes: 3 additions & 0 deletions bin/docker-start
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ if [ "$SERVER_TYPE" == "web" ]; then
elif [ "$SERVER_TYPE" == "sidekiq" ]; then
echo "Starting sidekiq server..."
exec "$BIN_DIR"/sidekiq
elif [ "$SERVER_TYPE" == "data-replication" ]; then
echo "Starting data replication server..."
exec "$BIN_DIR"/data-replication
elif [ "$SERVER_TYPE" == "none" ]; then
echo "No server started"
exec tail -f /dev/null # Keep container running
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
"secretsmanager:PutSecretValue",
"secretsmanager:UpdateSecret",
"secretsmanager:GetSecretValue",
"secretsmanager:GetRandomPassword",
"secretsmanager:RotateSecret",
"secretsmanager:DeleteSecret",
"secretsmanager:CancelRotateSecret",
Expand Down
5 changes: 3 additions & 2 deletions terraform/app/modules/ecs_service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ resource "aws_security_group" "this" {
}
}

resource "aws_security_group_rule" "egress_all" {
resource "aws_security_group_rule" "egress" {
count = min(length(var.default_egress_cidr_blocks), 1)
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
cidr_blocks = var.default_egress_cidr_blocks
security_group_id = aws_security_group.this.id
}

Expand Down
7 changes: 7 additions & 0 deletions terraform/app/modules/ecs_service/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ variable "server_type_name" {
nullable = true
}

variable "default_egress_cidr_blocks" {
type = list(string)
description = "The default CIDR blocks for egress rules from the service. Defaults to allow all outbound traffic."
default = ["0.0.0.0/0"]
nullable = false
}

variable "minimum_replica_count" {
type = number
description = "Minimum amount of allowed replicas for the service. Also the replica count when creating th service."
Expand Down
14 changes: 7 additions & 7 deletions terraform/app/modules/vpc_endpoint/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ resource "aws_security_group" "this" {
}

resource "aws_security_group_rule" "ingress" {
count = length(var.ingress_ports)
type = "ingress"
from_port = var.ingress_ports[count.index]
to_port = var.ingress_ports[count.index]
protocol = "tcp"
security_group_id = aws_security_group.this.id
source_security_group_id = var.source_security_group
count = length(var.ingress_ports)
type = "ingress"
from_port = var.ingress_ports[count.index]
to_port = var.ingress_ports[count.index]
protocol = "tcp"
security_group_id = aws_security_group.this.id
cidr_blocks = ["0.0.0.0/0"]
lifecycle {
create_before_destroy = true
}
Expand Down
6 changes: 3 additions & 3 deletions terraform/data_replication/ecs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ module "db_access_service" {
subnets = local.subnet_list
vpc_id = aws_vpc.vpc.id
}
server_type = "none"
server_type_name = "data-replication"
server_type = "data-replication"
task_config = {
environment = local.task_envs
secrets = local.task_secrets
Expand All @@ -40,5 +39,6 @@ module "db_access_service" {
region = var.region
health_check_command = ["CMD-SHELL", "echo 'alive' || exit 1"]
}
depends_on = [aws_rds_cluster_instance.instance]
default_egress_cidr_blocks = var.allowed_egress_cidr_blocks
depends_on = [aws_rds_cluster_instance.instance]
}
3 changes: 2 additions & 1 deletion terraform/data_replication/iam.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ data "aws_iam_policy_document" "ecs_permissions" {
sid = "dbSecretSid"
actions = ["secretsmanager:GetSecretValue"]
resources = [
var.db_secret_arn
var.db_secret_arn,
aws_secretsmanager_secret.read_only_db_password.arn
]
effect = "Allow"
}
Expand Down
53 changes: 30 additions & 23 deletions terraform/data_replication/network.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ resource "aws_subnet" "subnet_a" {
vpc_id = aws_vpc.vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.region}a"
tags = {
Private = true
}
}

resource "aws_subnet" "subnet_b" {
vpc_id = aws_vpc.vpc.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.region}b"
tags = {
Private = true
}
}

resource "aws_route_table" "private" {
Expand All @@ -36,45 +42,43 @@ resource "aws_subnet" "public_subnet" {
vpc_id = aws_vpc.vpc.id
cidr_block = "10.0.3.0/24"
availability_zone = "${var.region}a"
tags = {
Private = false
}
}

resource "aws_internet_gateway" "internet_gateway" {
count = local.shared_egress_infrastructure_count
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "data-replication-igw-${var.environment}"
}
}

resource "aws_eip" "nat_ip" {
count = local.shared_egress_infrastructure_count
resource "aws_eip" "this" {
domain = "vpc"
depends_on = [aws_internet_gateway.internet_gateway]
depends_on = [aws_internet_gateway.this]
}

resource "aws_nat_gateway" "nat_gateway" {
count = local.shared_egress_infrastructure_count
resource "aws_nat_gateway" "this" {
subnet_id = aws_subnet.public_subnet.id
allocation_id = aws_eip.nat_ip[0].id
allocation_id = aws_eip.this.id
connectivity_type = "public"
depends_on = [aws_internet_gateway.internet_gateway]
depends_on = [aws_internet_gateway.this]
tags = {
Name = "data-replication-nat-gateway-${var.environment}"
}
}

resource "aws_route" "private_to_public" {
count = length(var.allowed_egress_cidr_blocks)
route_table_id = aws_route_table.private.id
destination_cidr_block = var.allowed_egress_cidr_blocks[count.index]
nat_gateway_id = aws_nat_gateway.nat_gateway[0].id
nat_gateway_id = aws_nat_gateway.this.id
destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route" "public_to_igw" {
count = length(var.allowed_egress_cidr_blocks)
route_table_id = aws_route_table.public.id
destination_cidr_block = var.allowed_egress_cidr_blocks[count.index]
gateway_id = aws_internet_gateway.internet_gateway[0].id
gateway_id = aws_internet_gateway.this.id
destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route_table" "public" {
Expand Down Expand Up @@ -117,12 +121,15 @@ module "vpc_endpoints" {
}
}

resource "aws_vpc_endpoint" "s3_gateway" {
vpc_id = aws_vpc.vpc.id
service_name = "com.amazonaws.${var.region}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [aws_route_table.private.id]
tags = {
Name = "${local.name_prefix}-s3-gw-endpoint"
}
data "aws_prefix_list" "s3" {
name = "com.amazonaws.${var.region}.s3"
}

resource "aws_security_group_rule" "egress_to_s3" {
type = "egress"
from_port = 443
to_port = 443
protocol = "tcp"
prefix_list_ids = [data.aws_prefix_list.s3.id]
security_group_id = module.db_access_service.security_group_id
}
24 changes: 23 additions & 1 deletion terraform/data_replication/rds.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ resource "aws_db_subnet_group" "dbsg" {

resource "aws_security_group" "rds" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${local.name_prefix}-rds-sg"
}
}

resource "aws_security_group_rule" "ecs_to_grafana" {
type = "egress"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_group_id = module.db_access_service.security_group_id
source_security_group_id = aws_security_group.rds.id
}

resource "aws_security_group_rule" "rds_inbound" {
Expand All @@ -16,6 +28,16 @@ resource "aws_security_group_rule" "rds_inbound" {
source_security_group_id = module.db_access_service.security_group_id
}

resource "aws_security_group_rule" "rds_inbound_grafana" {
type = "ingress"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_group_id = aws_security_group.rds.id
cidr_blocks = [aws_vpc.vpc.cidr_block]
description = "Allow Grafana workspace access to PostgreSQL"
}

resource "aws_rds_cluster" "cluster" {
cluster_identifier = "${local.name_prefix}-rds-${formatdate("hh-mm-ss", timestamp())}"
engine = "aurora-postgresql"
Expand All @@ -36,7 +58,7 @@ resource "aws_rds_cluster" "cluster" {
}

lifecycle {
ignore_changes = [cluster_identifier]
ignore_changes = [cluster_identifier, snapshot_identifier]
}
}

Expand Down
21 changes: 21 additions & 0 deletions terraform/data_replication/ssm_parameters.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
ephemeral "aws_secretsmanager_random_password" "read_only_db_password" {
}

resource "aws_secretsmanager_secret" "read_only_db_password" {
name = "${local.name_prefix}-grafana-read-only-db-password-${substr(uuid(), 0, 4)}"
description = "Read-only database user password for data replication"
recovery_window_in_days = 7

tags = {
Name = "${local.name_prefix}-read-only-db-password"
}
lifecycle {
ignore_changes = [name]
}
}

resource "aws_secretsmanager_secret_version" "read_only_db_password" {
secret_id = aws_secretsmanager_secret.read_only_db_password.id
secret_string_wo = ephemeral.aws_secretsmanager_random_password.read_only_db_password.random_password
secret_string_wo_version = 1
}
9 changes: 6 additions & 3 deletions terraform/data_replication/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,8 @@ variable "rails_master_key_path" {
}

locals {
name_prefix = "mavis-${var.environment}-data-replication"
subnet_list = [aws_subnet.subnet_a.id, aws_subnet.subnet_b.id]
shared_egress_infrastructure_count = min(length(var.allowed_egress_cidr_blocks), 1)
name_prefix = "mavis-${var.environment}-data-replication"
subnet_list = [aws_subnet.subnet_a.id, aws_subnet.subnet_b.id]

task_envs = [
{
Expand Down Expand Up @@ -124,6 +123,10 @@ locals {
{
name = "RAILS_MASTER_KEY"
valueFrom = var.rails_master_key_path
},
{
name = "READ_ONLY_DB_PASSWORD"
valueFrom = aws_secretsmanager_secret.read_only_db_password.arn
}
]
}
Expand Down
4 changes: 3 additions & 1 deletion terraform/monitoring/aws/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,7 @@ locals {
AWS-Mavis-ReadOnly = "16b29214-60a1-7008-ff52-0ccd29b7e2d4"
}
}
bucket_name = "nhse-mavis-grafana-${var.environment}"
bucket_name = "nhse-mavis-grafana-${var.environment}"
prefix_environment = var.environment == "development" ? "qa" : var.environment
data_replication_prefix = "mavis-${local.prefix_environment}-data-replication"
}
Loading