diff --git a/locals.tf b/locals.tf index 54e31b4d..5d4f48d6 100644 --- a/locals.tf +++ b/locals.tf @@ -48,7 +48,16 @@ locals { } ) - redis = try( + redis = var.enable_redis_sentinel ? try( + module.redis_sentinel[0], + { + hostname = null + password = null + redis_port = null + use_password_auth = null + use_tls = null + } + ) : try( module.redis[0], { hostname = null diff --git a/main.tf b/main.tf index e0484860..a48ed147 100644 --- a/main.tf +++ b/main.tf @@ -71,7 +71,7 @@ module "networking" { # ----------------------------------------------------------------------------- module "redis" { source = "./modules/redis" - count = local.enable_redis_module ? 1 : 0 + count = local.enable_redis_module && var.enable_redis_sentinel == false ? 1 : 0 active_active = var.operational_mode == "active-active" friendly_name_prefix = var.friendly_name_prefix @@ -91,6 +91,29 @@ module "redis" { redis_port = var.redis_encryption_in_transit ? "6380" : "6379" } +# ----------------------------------------------------------------------------- +# Redis Sentinel +# ----------------------------------------------------------------------------- + +module "redis_sentinel" { + count = var.enable_redis_sentinel ? 1 : 0 + source = "./modules/redis-sentinel" + + domain_name = var.domain_name + + aws_iam_instance_profile = module.service_accounts.iam_instance_profile.name + asg_tags = var.asg_tags + ec2_launch_template_tag_specifications = var.ec2_launch_template_tag_specifications + friendly_name_prefix = var.friendly_name_prefix + health_check_grace_period = var.health_check_grace_period + health_check_type = var.health_check_type + instance_type = var.instance_type + key_name = var.key_name + network_id = local.network_id + network_subnets_private = local.network_private_subnets + network_private_subnet_cidrs = local.network_private_subnet_cidrs +} + # ----------------------------------------------------------------------------- # AWS PostreSQL Database # ----------------------------------------------------------------------------- diff --git a/modules/redis-sentinel/compose.yaml b/modules/redis-sentinel/compose.yaml new file mode 100644 index 00000000..fe3bf2a1 --- /dev/null +++ b/modules/redis-sentinel/compose.yaml @@ -0,0 +1,79 @@ +# Description: This file contains the docker-compose configuration for the redis-sentinel module. +services: + redis-leader: + container_name: redis-leader + command: "redis-server --appendonly yes --requirepass ${redis_sentinel_password}" + image: redis:7 + ports : + - ${redis_port}:${redis_port} + redis-follower-1: + container_name: redis-follower-1 + depends_on: + - redis-leader + command: "redis-server --replicaof redis-leader ${redis_port} --requirepass ${redis_password} --masterauth ${redis_sentinel_password}" + image: redis:7 + ports : + - 6380:${redis_port} + redis-follower-2: + container_name: redis-follower-2 + depends_on: + - redis-leader + command: "redis-server --replicaof redis-leader ${redis_port} --requirepass ${redis_password} --masterauth ${redis_sentinel_password}" + image: redis:7 + ports : + - 6381:${redis_port} + redis-sentinel-1: + container_name: redis-sentinel-1 + depends_on: + - redis-leader + - redis-follower-1 + - redis-follower-2 + command: > + sh -c 'echo "sentinel resolve-hostnames yes" > /etc/sentinel.conf && + echo "sentinel monitor ${redis_sentinel_leader_name} redis-leader 6379 2" >> /etc/sentinel.conf && + echo "sentinel auth-pass ${redis_sentinel_leader_name} ${redis_sentinel_password}" >> /etc/sentinel.conf && + echo "sentinel down-after-milliseconds ${redis_sentinel_leader_name} 5000" >> /etc/sentinel.conf && + echo "sentinel failover-timeout ${redis_sentinel_leader_name} 10000" >> /etc/sentinel.conf && + echo "loglevel notice" >> /etc/sentinel.conf && + echo "sentinel deny-scripts-reconfig yes" >> /etc/sentinel.conf && + redis-sentinel /etc/sentinel.conf' + image: redis:7 + ports: + - ${redis_sentinel_port}:${redis_sentinel_port} + redis-sentinel-2: + container_name: redis-sentinel-2 + depends_on: + - redis-leader + - redis-follower-1 + - redis-follower-2 + command: > + sh -c 'echo "sentinel resolve-hostnames yes" > /etc/sentinel.conf && + echo "sentinel monitor ${redis_sentinel_leader_name} redis-leader 6379 2" >> /etc/sentinel.conf && + echo "sentinel auth-pass ${redis_sentinel_leader_name} ${redis_sentinel_password}" >> /etc/sentinel.conf && + echo "sentinel down-after-milliseconds ${redis_sentinel_leader_name} 5000" >> /etc/sentinel.conf && + echo "sentinel failover-timeout ${redis_sentinel_leader_name} 10000" >> /etc/sentinel.conf && + echo "loglevel notice" >> /etc/sentinel.conf && + echo "sentinel deny-scripts-reconfig yes" >> /etc/sentinel.conf && + redis-sentinel /etc/sentinel.conf' + image: redis:7 + ports: + - 26380:${redis_sentinel_port} + redis-sentinel-3: + container_name: redis-sentinel-3 + depends_on: + - redis-leader + - redis-follower-1 + - redis-follower-2 + command: > + sh -c 'echo "sentinel resolve-hostnames yes" > /etc/sentinel.conf && + echo "sentinel monitor ${redis_sentinel_leader_name} redis-leader 6379 2" >> /etc/sentinel.conf && + echo "sentinel auth-pass ${redis_sentinel_leader_name} ${redis_sentinel_password}" >> /etc/sentinel.conf && + echo "sentinel down-after-milliseconds ${redis_sentinel_leader_name} 5000" >> /etc/sentinel.conf && + echo "sentinel failover-timeout ${redis_sentinel_leader_name} 10000" >> /etc/sentinel.conf && + echo "loglevel notice" >> /etc/sentinel.conf && + echo "sentinel deny-scripts-reconfig yes" >> /etc/sentinel.conf && + redis-sentinel /etc/sentinel.conf' + image: redis:7 + ports: + - 26381:${redis_sentinel_port} + diff --git a/modules/redis-sentinel/locals.tf b/modules/redis-sentinel/locals.tf new file mode 100644 index 00000000..43a234a5 --- /dev/null +++ b/modules/redis-sentinel/locals.tf @@ -0,0 +1,50 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +locals { + redis_user_data_template = "${path.module}/script.sh" + redis_leader_user_data = templatefile(local.redis_user_data_template, { + + compose = base64encode(templatefile(local.compose_path, { + redis_sentinel_password = var.redis_sentinel_password + redis_sentinel_leader_name = var.redis_sentinel_leader_name + redis_sentinel_port = var.redis_sentinel_port + redis_port = var.redis_port + redis_password = var.redis_password + })) }) + compose_path = "${path.module}/compose.yaml" + tags = concat( + [ + { + key = "Name" + value = "${var.friendly_name_prefix}-tfe" + propagate_at_launch = true + }, + ], + [ + for k, v in var.asg_tags : { + key = k + value = v + propagate_at_launch = true + } + ] + ) + default_health_check_grace_period = 1500 + health_check_grace_period = var.health_check_grace_period != null ? var.health_check_grace_period : local.default_health_check_grace_period +} + +data "aws_ami" "ubuntu" { + most_recent = true + + filter { + name = "name" + values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + owners = ["099720109477"] # Canonical +} \ No newline at end of file diff --git a/modules/redis-sentinel/main.tf b/modules/redis-sentinel/main.tf new file mode 100644 index 00000000..5ef52333 --- /dev/null +++ b/modules/redis-sentinel/main.tf @@ -0,0 +1,86 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + + +# Launch Template for Redis Sentinel +# ---------------------------------- + +resource "aws_launch_template" "redis_sentinel_leader" { + name_prefix = "${var.friendly_name_prefix}-redis-sentinel-leader" + image_id = data.aws_ami.ubuntu.id + instance_type = var.instance_type + user_data = base64encode(local.redis_leader_user_data) + key_name = var.key_name + vpc_security_group_ids = [aws_security_group.redis_sentinel_inbound_allow.id, aws_security_group.redis_sentinel_outbound_allow.id] + + dynamic "tag_specifications" { + for_each = var.ec2_launch_template_tag_specifications + + content { + resource_type = tag_specifications.value["resource_type"] + tags = tag_specifications.value["tags"] + } + } + + iam_instance_profile { + name = var.aws_iam_instance_profile + } + + metadata_options { + http_endpoint = "enabled" + http_put_response_hop_limit = 2 + http_tokens = "optional" + } + + block_device_mappings { + device_name = "/dev/sda1" + ebs { + encrypted = true + volume_type = "gp2" + volume_size = 50 + delete_on_termination = true + } + } + + lifecycle { + create_before_destroy = true + } +} + +# Autoscaling Group for Redis Sentinel +# ------------------------------------ + +resource "aws_autoscaling_group" "redis_sentinel" { + name = "${var.friendly_name_prefix}-redis-sentinel-leader-asg" + min_size = 1 + max_size = 1 + desired_capacity = 1 + vpc_zone_identifier = var.network_subnets_private + target_group_arns = [aws_lb_target_group.redis_sentinel_tg_6379.arn, + aws_lb_target_group.redis_sentinel_tg_26379.arn + ] + + # Increases grace period for any AMI that is not the default Ubuntu + # since RHEL has longer startup time + health_check_grace_period = local.health_check_grace_period + health_check_type = var.health_check_type + + launch_template { + id = aws_launch_template.redis_sentinel_leader.id + version = "$Latest" + } + + dynamic "tag" { + for_each = local.tags + + content { + key = tag.value["key"] + value = tag.value["value"] + propagate_at_launch = tag.value["propagate_at_launch"] + } + } + + lifecycle { + create_before_destroy = true + } +} diff --git a/modules/redis-sentinel/networking.tf b/modules/redis-sentinel/networking.tf new file mode 100644 index 00000000..9657c6b4 --- /dev/null +++ b/modules/redis-sentinel/networking.tf @@ -0,0 +1,75 @@ +## DNS Record for Redis Sentinel cluster Load Balancer +# ---------------------------------------------------- +data "aws_route53_zone" "tfe" { + name = var.domain_name + private_zone = false +} + +resource "aws_route53_record" "sentinel" { + zone_id = data.aws_route53_zone.tfe.zone_id + name = "${var.friendly_name_prefix}-redis-sentinel" + type = "A" + + alias { + name = aws_lb.redis_sentinel_lb.dns_name + zone_id = aws_lb.redis_sentinel_lb.zone_id + evaluate_target_health = true + } +} + +# Network Load Balancer for Redis Sentinel cluster +# ------------------------------------------------ + +resource "aws_lb" "redis_sentinel_lb" { + name = "${var.friendly_name_prefix}-redis-sentinel-nlb" + internal = true + load_balancer_type = "network" + subnets = var.network_subnets_private +} + +# Network Load Balancer Listener and Target Group for Redis and Sentinel +# ---------------------------------------------------------------------- + +resource "aws_lb_listener" "redis_sentinel_listener_6379" { + load_balancer_arn = aws_lb.redis_sentinel_lb.arn + port = 6379 + protocol = "TCP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.redis_sentinel_tg_6379.arn + } +} + +resource "aws_lb_target_group" "redis_sentinel_tg_6379" { + name = "${var.friendly_name_prefix}-redis-sentinel-tg-6379" + port = 6379 + protocol = "TCP" + vpc_id = var.network_id + + health_check { + protocol = "TCP" + } +} + +resource "aws_lb_listener" "redis_sentinel_listener_26379" { + load_balancer_arn = aws_lb.redis_sentinel_lb.arn + port = 26379 + protocol = "TCP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.redis_sentinel_tg_26379.arn + } +} + +resource "aws_lb_target_group" "redis_sentinel_tg_26379" { + name = "${var.friendly_name_prefix}-redis-sentinel-tg-26379" + port = 26379 + protocol = "TCP" + vpc_id = var.network_id + + health_check { + protocol = "TCP" + } +} diff --git a/modules/redis-sentinel/outputs.tf b/modules/redis-sentinel/outputs.tf new file mode 100644 index 00000000..3e85bcf0 --- /dev/null +++ b/modules/redis-sentinel/outputs.tf @@ -0,0 +1,39 @@ +output "hostname" { + value = aws_route53_record.sentinel.fqdn + description = "The hostname of the Redis Sentinel" +} + +output "redis_port" { + value = var.redis_port + description = "The port of the Redis" +} + +output "redis_sentinel_port" { + value = var.redis_sentinel_port + description = "The port of the Redis Sentinel" +} + +output "redis_sentinel_leader_name" { + value = var.redis_sentinel_leader_name + description = "The name of the Redis Sentinel leader" +} + +output "redis_sentinel_password" { + value = var.redis_sentinel_password + description = "value of the Redis Sentinel password" +} + +output "password" { + value = var.redis_password + description = "value of the Redis password" +} + +output "use_password_auth" { + value = var.redis_use_password_auth ? true : false + description = "A boolean which indicates if password authentication is required by the Redis" +} + +output "use_tls" { + value = var.use_tls ? true : false + description = "A boolean which indicates if TLS is required by the Redis" +} \ No newline at end of file diff --git a/modules/redis-sentinel/script.sh b/modules/redis-sentinel/script.sh new file mode 100644 index 00000000..9fa649b6 --- /dev/null +++ b/modules/redis-sentinel/script.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -eu pipefail + +curl --noproxy '*' --fail --silent --show-error --location https://download.docker.com/linux/ubuntu/gpg \ + | gpg --dearmor --output /usr/share/keyrings/docker-archive-keyring.gpg +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] \ + https://download.docker.com/linux/ubuntu $(lsb_release --codename --short) stable" \ + | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null +apt-get --assume-yes update +apt-get --assume-yes install docker-ce docker-ce-cli containerd.io +apt-get --assume-yes autoremove + +tfe_dir="/etc/redis" +mkdir -p $tfe_dir + +echo ${compose} | base64 -d > $tfe_dir/compose.yaml +docker compose -f $tfe_dir/compose.yaml up -d \ No newline at end of file diff --git a/modules/redis-sentinel/security-group.tf b/modules/redis-sentinel/security-group.tf new file mode 100644 index 00000000..05d734d3 --- /dev/null +++ b/modules/redis-sentinel/security-group.tf @@ -0,0 +1,61 @@ +# Allow inbound from Redis Sentinel instances to TFE VPC + +resource "aws_security_group" "redis_sentinel_inbound_allow" { + name = "${var.friendly_name_prefix}-redis-sentinel-inbound-allow" + vpc_id = var.network_id +} + +resource "aws_security_group_rule" "redis_sentinel_leader" { + security_group_id = aws_security_group.redis_sentinel_inbound_allow.id + type = "ingress" + from_port = 6379 + to_port = 6379 + protocol = "tcp" + cidr_blocks = var.network_private_subnet_cidrs +} + +resource "aws_security_group_rule" "redis_sentinel" { + security_group_id = aws_security_group.redis_sentinel_inbound_allow.id + type = "ingress" + from_port = 26379 + to_port = 26379 + protocol = "tcp" + cidr_blocks = var.network_private_subnet_cidrs +} + +resource "aws_security_group_rule" "ssh_inbound" { + + security_group_id = aws_security_group.redis_sentinel_inbound_allow.id + type = "ingress" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = var.network_private_subnet_cidrs +} + +resource "aws_security_group_rule" "redis_sentinel_inbound" { + security_group_id = aws_security_group.redis_sentinel_inbound_allow.id + type = "ingress" + from_port = 0 + to_port = 0 + protocol = "-1" + self = true +} + +# Allow all traffic outbound from Redis Sentinel instances to www + +resource "aws_security_group" "redis_sentinel_outbound_allow" { + name = "${var.friendly_name_prefix}-redis-sentinel-outbound-allow" + vpc_id = var.network_id +} + +resource "aws_security_group_rule" "redis_sentinel_outbound" { + type = "egress" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + description = "Allow all traffic outbound from Redis Sentinel instances to TFE" + + security_group_id = aws_security_group.redis_sentinel_outbound_allow.id +} \ No newline at end of file diff --git a/modules/redis-sentinel/variables.tf b/modules/redis-sentinel/variables.tf new file mode 100644 index 00000000..17adefd2 --- /dev/null +++ b/modules/redis-sentinel/variables.tf @@ -0,0 +1,129 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +# VM Configuration +# ---------------- + +variable "aws_iam_instance_profile" { + description = "The name of the IAM instance profile to be associated with the TFE EC2 instance(s)." + type = string +} + +variable "friendly_name_prefix" { + type = string + description = "(Required) Friendly name prefix used for tagging and naming AWS resources." +} + +variable "health_check_grace_period" { + default = null + description = "The health grace period aws provides to allow for an instance to pass it's health check." + type = number +} + +variable "health_check_type" { + description = "Type of health check to perform on the instance." + type = string + default = "ELB" + + validation { + condition = contains(["ELB", "EC2"], var.health_check_type) + error_message = "Must be one of [ELB, EC2]." + } +} + +variable "instance_type" { + default = "m5.xlarge" + description = "The instance type of EC2 instance(s) to create." + type = string +} + +variable "network_id" { + description = "The identity of the VPC in which the security group attached to the TFE EC2 instance will be delpoyed." + type = string +} + +variable "network_subnets_private" { + description = "A list of the identities of the private subnetworks in which the EC2 autoscaling group will be deployed." + type = list(string) +} + +variable "asg_tags" { + type = map(string) + description = "(Optional) Map of tags only used for the autoscaling group. If you are using the AWS provider's default_tags, please note that it tags every taggable resource except for the autoscaling group, therefore this variable may be used to duplicate the key/value pairs in the default_tags if you wish." + default = {} +} + +variable "network_private_subnet_cidrs" { + type = list(string) + description = "(Optional) List of private subnet CIDR ranges to create in VPC." + default = ["10.0.32.0/20", "10.0.48.0/20"] +} + +variable "key_name" { + default = null + description = "The name of the key pair to be used for SSH access to the EC2 instance(s)." + type = string +} + +variable "ec2_launch_template_tag_specifications" { + description = "(Optional) List of tag specifications to apply to the launch template." + type = list(object({ + resource_type = string + tags = map(string) + })) + default = [] +} + + +# Domain Installation +# ------------------- + +variable "domain_name" { + description = "The name of the Route 53 Hosted Zone in which a record will be created." + type = string +} + +# Redis Sentinel Variables +# ------------------------ + +variable "redis_sentinel_port" { + description = "The redis sentinel follower port" + type = number + default = 26379 +} + +variable "redis_port" { + description = "The redis sentinel follower port" + type = number + default = 6379 +} + +variable "redis_sentinel_password" { + description = "The password for the redis sentinel" + type = string + default = "hashicorp" +} + +variable "redis_sentinel_leader_name" { + description = "The redis sentinel leader hostname" + type = string + default = "main" +} + +variable "redis_password" { + description = "The password for the redis sentinel" + type = string + default = "hashicorp" +} + +variable "redis_use_password_auth" { + description = "A boolean which indicates if password authentication is required by the Redis" + type = bool + default = true +} + +variable "use_tls" { + description = "A boolean which indicates if TLS is required by the Redis" + type = bool + default = false +} \ No newline at end of file diff --git a/modules/redis-sentinel/versions.tf b/modules/redis-sentinel/versions.tf new file mode 100644 index 00000000..17ed3f55 --- /dev/null +++ b/modules/redis-sentinel/versions.tf @@ -0,0 +1,12 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +terraform { + required_version = ">= 0.14" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} diff --git a/variables.tf b/variables.tf index 17c76e21..c4b1dec6 100644 --- a/variables.tf +++ b/variables.tf @@ -109,6 +109,13 @@ variable "vm_key_secret_id" { # Redis # ----- + +variable "enable_redis_sentinel" { + type = bool + description = "Enable Redis Sentinel." + default = false +} + variable "redis_cache_size" { type = string default = "cache.m4.large"