diff --git a/README.md b/README.md index b251eb8..b01b416 100644 --- a/README.md +++ b/README.md @@ -84,4 +84,4 @@ module "aws_vault" { [Apache 2.0](LICENSE) -Copyright (c) 2018 [Flaconi GmbH](https://github.com/Flaconi) +Copyright (c) 2018-2021 [Flaconi GmbH](https://github.com/Flaconi) diff --git a/data.tf b/data.tf new file mode 100644 index 0000000..81941c7 --- /dev/null +++ b/data.tf @@ -0,0 +1,46 @@ +data "aws_region" "current" {} + +data "aws_ami" "vault_consul" { + most_recent = true + + owners = [var.ami_owner] + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "name" + values = var.ami_name_filter + } +} + +data "aws_elb" "vault_elb" { + name = module.vault_elb.name +} + +data "template_file" "user_data_vault_cluster" { + template = file("${path.module}/user-data-vault.sh") + + vars = { + enable_s3_backend = var.enable_s3_backend ? 1 : 0 + s3_bucket_region = data.aws_region.current.name + s3_bucket_name = var.s3_bucket_name + consul_cluster_tag_key = local.consul_cluster_tag_key + consul_cluster_tag_value = local.consul_cluster_tag_val + ssh_keys = join("\n", var.ssh_keys) + ssh_user = "ubuntu" + } +} + +data "template_file" "user_data_consul" { + template = file("${path.module}/user-data-consul.sh") + + vars = { + consul_cluster_tag_key = local.consul_cluster_tag_key + consul_cluster_tag_value = local.consul_cluster_tag_val + ssh_keys = join("\n", var.ssh_keys) + ssh_user = "ubuntu" + } +} diff --git a/locals.tf b/locals.tf index a556f33..fd68d61 100644 --- a/locals.tf +++ b/locals.tf @@ -1,9 +1,4 @@ -# ------------------------------------------------------------------------------------------------- -# Locals -# ------------------------------------------------------------------------------------------------- - locals { consul_cluster_tag_key = "consul-servers" consul_cluster_tag_val = var.consul_cluster_name } - diff --git a/main.tf b/main.tf index 33f03a1..8624ef5 100644 --- a/main.tf +++ b/main.tf @@ -1,51 +1,3 @@ -# This module has been copy/pasted from the following repository: -# https://github.com/hashicorp/terraform-aws-vault -# -# After having copy/pasted it, I have done some heavy rewriting. -# Customization was necessary as the default provided module is not production ready: -# https://github.com/hashicorp/terraform-aws-vault/issues/103 -# -# Additionally the following pitfalls were discovered: -# * AMI needs to be built by ourselves in order to provide valid SSL certificates -# * Security groups are to open and cannot be easily closed without rewriting the submodules -# (https://github.com/hashicorp/terraform-aws-vault/issues/107) -# * Security groups are written in a way that Terraform will not detect any manual changes -# -# For the above reasons, also some submodules had to be rewritten (see modules/) -# - -# ------------------------------------------------------------------------------------------------- -# Terraform Settings -# ------------------------------------------------------------------------------------------------- -# Terraform 0.9.5 suffered from https://github.com/hashicorp/terraform/issues/14399, which causes -# this template the conditionals in this template to fail. -terraform { - required_version = ">= 0.9.3, != 0.9.5" -} - -# ------------------------------------------------------------------------------------------------- -# TODO: Use custom build AMI. -# ------------------------------------------------------------------------------------------------- -# TODO: Create custom AMI baked with our own SSL certificates for HTTPS access. -data "aws_ami" "vault_consul" { - most_recent = true - - owners = [var.ami_owner] - - filter { - name = "virtualization-type" - values = ["hvm"] - } - - filter { - name = "name" - values = var.ami_name_filter - } -} - -# ------------------------------------------------------------------------------------------------- -# DEPLOY THE VAULT SERVER CLUSTER -# ------------------------------------------------------------------------------------------------- module "vault_cluster" { source = "./modules/vault-cluster" @@ -59,62 +11,27 @@ module "vault_cluster" { vpc_id = var.vpc_id subnet_ids = var.private_subnet_ids - # Use S3 Storage Backend? enable_s3_backend = var.enable_s3_backend s3_bucket_name = var.s3_bucket_name - # Encrypt S3 Storage Backend? enable_s3_backend_encryption = var.enable_s3_backend_encryption kms_alias_name = var.kms_alias_name - # Do NOT use the ELB for the ASG health check, or the ASG will assume all sealed instances are - # unhealthy and repeatedly try to redeploy them. - # The ELB health check does not work on unsealed Vault instances. health_check_type = "EC2" - # Security groups elb_security_group_id = module.vault_elb.security_group_ids[0] consul_security_group_id = module.consul_cluster.security_group_id - ssh_security_group_ids = var.ssh_security_group_ids + ssh_security_group_id = var.ssh_security_group_id tags = var.tags } -# ------------------------------------------------------------------------------------------------- -# ATTACH IAM POLICIES FOR CONSUL -# To allow our Vault servers to automatically discover the Consul servers, we need to give them the -# IAM permissions from the Consul AWS Module's consul-iam-policies module. -# ------------------------------------------------------------------------------------------------- module "consul_iam_policies_servers" { source = "github.com/hashicorp/terraform-aws-consul//modules/consul-iam-policies?ref=v0.7.0" iam_role_id = module.vault_cluster.iam_role_id } -# ------------------------------------------------------------------------------------------------- -# THE USER DATA SCRIPT THAT WILL RUN ON EACH VAULT SERVER WHEN IT'S BOOTING -# This script will configure and start Vault -# ------------------------------------------------------------------------------------------------- -data "template_file" "user_data_vault_cluster" { - template = file("${path.module}/user-data-vault.sh") - - vars = { - enable_s3_backend = var.enable_s3_backend ? 1 : 0 - s3_bucket_region = data.aws_region.current.name - s3_bucket_name = var.s3_bucket_name - consul_cluster_tag_key = local.consul_cluster_tag_key - consul_cluster_tag_value = local.consul_cluster_tag_val - ssh_keys = join("\n", var.ssh_keys) - ssh_user = "ubuntu" - } -} - -data "aws_region" "current" { -} - -# ------------------------------------------------------------------------------------------------- -# Vault ELB -# ------------------------------------------------------------------------------------------------- module "vault_elb" { source = "github.com/Flaconi/terraform-aws-elb?ref=v1.0.0" @@ -122,55 +39,36 @@ module "vault_elb" { vpc_id = var.vpc_id subnet_ids = var.public_subnet_ids - # Listener lb_port = "443" lb_protocol = "HTTPS" instance_port = "8200" instance_protocol = "HTTPS" ssl_certificate_id = var.ssl_certificate_id - # Health Checks target = "HTTPS:8200/v1/sys/health?standbyok=true" timeout = "5" interval = "15" healthy_threshold = "2" unhealthy_threshold = "2" - # Security inbound_cidr_blocks = var.vault_ingress_cidr_https security_group_names = var.security_group_names - # DNS route53_public_dns_name = var.vault_route53_public_dns_name - # https://github.com/hashicorp/terraform-aws-vault/blob/master/modules/vault-elb/main.tf#L104 - # When set to true, if either none of the ELB's EC2 instances are healthy or the ELB itself is - # unhealthy, Route 53 routes queries to "other resources." But since we haven't defined any other - # resources, we'd rather avoid any latency due to switchovers and just wait for the ELB and Vault - # instances to come back online. For more info, see - # http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-values-alias.html#rrsets-values-alias-evaluate-target-health public_dns_evaluate_target_health = false tags = var.tags } -# Attach Vault ASG to Vault ELB resource "aws_autoscaling_attachment" "vault" { autoscaling_group_name = module.vault_cluster.asg_name elb = data.aws_elb.vault_elb.id } -data "aws_elb" "vault_elb" { - name = module.vault_elb.name -} - -# ------------------------------------------------------------------------------------------------- -# DEPLOY THE CONSUL SERVER CLUSTER -# ------------------------------------------------------------------------------------------------- module "consul_cluster" { source = "./modules/consul-cluster" - # Naming/Tagging cluster_name = var.consul_cluster_name cluster_size = var.consul_cluster_size instance_type = var.consul_instance_type @@ -181,29 +79,11 @@ module "consul_cluster" { vpc_id = var.vpc_id subnet_ids = var.private_subnet_ids - # Security groups vault_security_group_id = module.vault_cluster.security_group_id - ssh_security_group_ids = var.ssh_security_group_ids + ssh_security_group_id = var.ssh_security_group_id - # The EC2 Instances will use these tags to automatically discover each other and form a cluster cluster_tag_key = local.consul_cluster_tag_key cluster_tag_value = local.consul_cluster_tag_val tags = var.tags } - -# ------------------------------------------------------------------------------------------------- -# THE USER DATA SCRIPT THAT WILL RUN ON EACH CONSUL SERVER WHEN IT'S BOOTING -# This script will configure and start Consul -# ------------------------------------------------------------------------------------------------- -data "template_file" "user_data_consul" { - template = file("${path.module}/user-data-consul.sh") - - vars = { - consul_cluster_tag_key = local.consul_cluster_tag_key - consul_cluster_tag_value = local.consul_cluster_tag_val - ssh_keys = join("\n", var.ssh_keys) - ssh_user = "ubuntu" - } -} - diff --git a/modules/consul-cluster/README.md b/modules/consul-cluster/README.md index d3d3ca4..3efd75d 100644 --- a/modules/consul-cluster/README.md +++ b/modules/consul-cluster/README.md @@ -1,17 +1,20 @@ # Consul Cluster -This module has been copy/pasted from the following repository: -https://github.com/hashicorp/terraform-aws-consul/tree/master/modules/consul-cluster +This module was inspired by the following repository: [terraform-aws-vault][1]. -Security groups have been re-written in order to make sure they are exclusively managed -by Terraform and any other rules that have been added by hand (or other means) will be -removed, whenever this module is called. +## Caveats -This is achieved by moving all separately defined rules from 'aws_security_group_rule' -into a single 'aws_security_group' block. +### Security Groups + +See this [GitHub issue][2], for clarifying the purpose of the SGs and their +rules. + +[Here][3] are the ports in use and their purpose. ## Inputs +See all required rules [here][2]. + | Name | Description | Type | Default | Required | |------|-------------|:----:|:-----:|:-----:| | vpc_id | The ID of the VPC in which to deploy the Consul cluster | string | - | yes | @@ -48,3 +51,7 @@ into a single 'aws_security_group' block. | iam_role_id | ID of the IAM role attached to the Consul instance. | | iam_role_name | Name of the IAM role attached to the Consul instance. | | security_group_id | Security group ID to attach to other security group rules as destination. | + +[1]: https://github.com/hashicorp/terraform-aws-consul/tree/master/modules/consul-cluster +[2]: https://github.com/hashicorp/terraform-aws-vault/issues/107 +[3]: https://www.consul.io/docs/install/ports#ports-table diff --git a/modules/consul-cluster/iam.tf b/modules/consul-cluster/iam.tf new file mode 100644 index 0000000..2297a7f --- /dev/null +++ b/modules/consul-cluster/iam.tf @@ -0,0 +1,36 @@ +data "aws_iam_policy_document" "instance_role" { + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} + +module "iam_policies" { + source = "github.com/hashicorp/terraform-aws-consul//modules/consul-iam-policies?ref=v0.7.0" + + iam_role_id = aws_iam_role.instance_role.id +} + +resource "aws_iam_role" "instance_role" { + name_prefix = var.cluster_name + assume_role_policy = data.aws_iam_policy_document.instance_role.json + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_iam_instance_profile" "instance_profile" { + name_prefix = var.cluster_name + path = var.instance_profile_path + role = aws_iam_role.instance_role.name + + lifecycle { + create_before_destroy = true + } +} diff --git a/modules/consul-cluster/locals.tf b/modules/consul-cluster/locals.tf index 02d877e..b896795 100644 --- a/modules/consul-cluster/locals.tf +++ b/modules/consul-cluster/locals.tf @@ -1,9 +1,3 @@ -# ------------------------------------------------------------------------------------------------- -# Locals -# ------------------------------------------------------------------------------------------------- - -# The following example converts key/val maps into AWS ASG 3er tuple maps. -# Credits: https://github.com/terraform-aws-modules/terraform-aws-autoscaling/blob/master/locals.tf locals { tags_asg_format = null_resource.tags_as_list_of_maps.*.triggers } @@ -17,4 +11,3 @@ resource "null_resource" "tags_as_list_of_maps" { "propagate_at_launch" = "true" } } - diff --git a/modules/consul-cluster/main.tf b/modules/consul-cluster/main.tf index c730ea9..f25b800 100644 --- a/modules/consul-cluster/main.tf +++ b/modules/consul-cluster/main.tf @@ -1,24 +1,3 @@ -# This module has been copy/pasted from the following repository: -# https://github.com/hashicorp/terraform-aws-consul/tree/master/modules/consul-cluster -# -# Security groups have been re-written in order to make sure they are exclusively managed -# by Terraform and any other rules that have been added by hand (or other means) will be -# removed, whenever this module is called. -# -# This is achieved by moving all separately defined rules from 'aws_security_group_rule' -# into a single 'aws_security_group' block. - -# ------------------------------------------------------------------------------------------------- -# THESE TEMPLATES REQUIRE TERRAFORM VERSION 0.8 AND ABOVE -# ------------------------------------------------------------------------------------------------- -terraform { - required_version = ">= 0.9.3" -} - -# ------------------------------------------------------------------------------------------------- -# CREATE AN AUTO SCALING GROUP (ASG) TO RUN CONSUL -# ------------------------------------------------------------------------------------------------- -# NOTE: This block has been kept unchanged. resource "aws_autoscaling_group" "autoscaling_group" { name_prefix = var.cluster_name @@ -26,7 +5,6 @@ resource "aws_autoscaling_group" "autoscaling_group" { vpc_zone_identifier = flatten(var.subnet_ids) - # Run a fixed number of instances in the ASG min_size = var.cluster_size max_size = var.cluster_size desired_capacity = var.cluster_size @@ -53,35 +31,22 @@ resource "aws_autoscaling_group" "autoscaling_group" { ) } -# ------------------------------------------------------------------------------------------------- -# CREATE LAUNCH CONFIGURATION TO DEFINE WHAT RUNS ON EACH INSTANCE IN THE ASG -# ------------------------------------------------------------------------------------------------- -# NOTE: This block has been altered resource "aws_launch_configuration" "launch_configuration" { name_prefix = "${var.cluster_name}-" image_id = var.ami_id instance_type = var.instance_type user_data = var.user_data - # Edit: no need for this - #spot_price = "${var.spot_price}" - iam_instance_profile = aws_iam_instance_profile.instance_profile.name placement_tenancy = var.tenancy - # Edit: key has been removed, as we will add our own SSH keys to the launch configuratoin - #key_name = "${var.ssh_key_name}" - - # Edit: only allow the consul required consul rules and an external group for ssh access - security_groups = [aws_security_group.lc_security_group.id, aws_security_group.attach_security_group.id] - - #security_groups = ["${concat(list(aws_security_group.lc_security_group.id), var.additional_security_group_ids)}"] + security_groups = [ + module.lc_security_group.security_group_id, + module.attach_security_group.security_group_id, + ] - # Edit: removed dynamic configuration option, we want Consul to be private by default associate_public_ip_address = false - #associate_public_ip_address = "${var.associate_public_ip_address}" - ebs_optimized = var.root_volume_ebs_optimized root_block_device { volume_type = var.root_volume_type @@ -89,320 +54,7 @@ resource "aws_launch_configuration" "launch_configuration" { delete_on_termination = var.root_volume_delete_on_termination } - # Important note: whenever using a launch configuration with an auto scaling group, you must set - # create_before_destroy = true. However, as soon as you set create_before_destroy = true in one - # resource, you must also set it in every resource that it depends on, or you'll get an error - # about cyclic dependencies (especially when removing resources). For more info, see: - # - # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html - # https://terraform.io/docs/configuration/resources.html lifecycle { create_before_destroy = true } } - -# ------------------------------------------------------------------------------------------------- -# CREATE A SECURITY GROUP TO CONTROL WHAT REQUESTS CAN GO IN AND OUT OF EACH EC2 INSTANCE -# ------------------------------------------------------------------------------------------------- -# NOTE: This section has been rewritten to use 'aws_security_group' resource. -# CLARIFICATION OF SECURITY GROUPS: https://github.com/hashicorp/terraform-aws-vault/issues/107 - -# 1. Vault needs to allow inbound Consul connections. This is done by using the Consuls security -# group as destination in the vault "lc_security_group" rules. -# 2. Consul needs to allow inbound Vault connections. This is done by using the Vaults security -# group as destination in the consul "lc_security_group" rules. -# -# This however creates a circular dependency in Terraform, as both rules need to be created -# and linked to each other. -# In order to overcome this problem, each of the launch configurations attaches to an (almost) -# empty NULL security group that can be used by the other in their "lc_security_group" to act -# as destination. -# Once this behaviour is fixed in Terraform, each second security group will be removed. -# The "attach_security_group" represents the NULL security group that is also exported by this -# module in order to be used by security groups of other machines. -resource "aws_security_group" "attach_security_group" { - name_prefix = "${var.cluster_name}-att" - description = "Null Placeholder security group for other instances to use as destination to access ${var.cluster_name}" - vpc_id = var.vpc_id - - # This is the least possible access I came up with. - # Note, if no rule is defined, Terraform is not going to see any manually made changes. - # This is why we need at least one ingress and one egress rule here. - ingress { - from_port = "8" - to_port = "0" - protocol = "icmp" - cidr_blocks = ["255.255.255.255/32"] - description = "(NULL) Terraform requires at least one rule in order to fully manage this security rule" - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - description = "Default AWS egress rule." - } - - revoke_rules_on_delete = true - - # aws_launch_configuration.launch_configuration in this module sets create_before_destroy to - # true, which means everything it depends on, including this resource, must set it as well, or - # you'll get cyclic dependency errors when you try to do a terraform destroy. - lifecycle { - create_before_destroy = true - } - - tags = merge( - { - "Name" = "${var.cluster_name}-null" - }, - var.tags, - ) -} - -# 8300/tcp - server_rpc_port: The port used by servers to handle incoming requests from other agents -# 8301/tcp - sef_lan_port: The port used to handle gossip in the LAN. Required by all agents -# 8301/udp - sef_lan_port: The port used to handle gossip in the LAN. Required by all agents -# 8302/tcp - serf_wan_port: The port used by servers to gossip over the WAN to other servers -# 8302/udp - serf_wan_port: The port used by servers to gossip over the WAN to other servers -# (OFF) 8400/tcp - cli_rpc_port: The port used by all agents to handle RPC from the CLI -# 8500/tcp - http_api_port: The port used by clients to talk to the HTTP API -# 8600/tcp - dns_port: The port used to resolve DNS queries -# 8600/udp - dns_port: The port used to resolve DNS queries -# 22/tcp - ssh: Allow ssh access to consul instances -# -# See all required rules here: https://github.com/hashicorp/terraform-aws-vault/issues/107 -resource "aws_security_group" "lc_security_group" { - name_prefix = var.cluster_name - description = "Security group for the ${var.cluster_name} launch configuration" - vpc_id = var.vpc_id - - # Consul Access to itself - ingress { - from_port = "8300" - to_port = "8300" - protocol = "tcp" - self = true - description = "TODO" - } - - ingress { - from_port = "8301" - to_port = "8301" - protocol = "tcp" - self = true - description = "TODO" - } - - ingress { - from_port = "8301" - to_port = "8301" - protocol = "udp" - self = true - description = "TODO" - } - - ingress { - from_port = "8302" - to_port = "8302" - protocol = "tcp" - self = true - description = "TODO" - } - - ingress { - from_port = "8302" - to_port = "8302" - protocol = "udp" - self = true - description = "TODO" - } - - ingress { - from_port = "8500" - to_port = "8500" - protocol = "tcp" - self = true - description = "TODO" - } - - ingress { - from_port = "8600" - to_port = "8600" - protocol = "tcp" - self = true - description = "Self DNS. Allow consul instances to query themselves for DNS." - } - - ingress { - from_port = "8600" - to_port = "8600" - protocol = "udp" - self = true - description = "Self DNS. Allow consul instances to query themselves for DNS." - } - - # Access from Vault - # 8300/tcp - # 8301/tcp - # 8302/tcp - # 8302/udp - # 8400/tpc - # 8500/tcp - # 8600/tcp - # 8600/udp - ingress { - from_port = "8300" - to_port = "8300" - protocol = "tcp" - security_groups = [var.vault_security_group_id] - description = "TODO" - } - - ingress { - from_port = "8301" - to_port = "8301" - protocol = "tcp" - security_groups = [var.vault_security_group_id] - description = "TODO" - } - - ingress { - from_port = "8302" - to_port = "8302" - protocol = "tcp" - security_groups = [var.vault_security_group_id] - description = "TODO" - } - - ingress { - from_port = "8302" - to_port = "8302" - protocol = "udp" - security_groups = [var.vault_security_group_id] - description = "TODO" - } - - ingress { - from_port = "8400" - to_port = "8400" - protocol = "tcp" - security_groups = [var.vault_security_group_id] - description = "TODO" - } - - ingress { - from_port = "8500" - to_port = "8500" - protocol = "tcp" - security_groups = [var.vault_security_group_id] - description = "TODO" - } - - ingress { - from_port = "8600" - to_port = "8600" - protocol = "tcp" - security_groups = [var.vault_security_group_id] - description = "TODO" - } - - ingress { - from_port = "8600" - to_port = "8600" - protocol = "udp" - security_groups = [var.vault_security_group_id] - description = "TODO" - } - - # SSH access from bastion host - ingress { - from_port = "22" - to_port = "22" - protocol = "tcp" - security_groups = var.ssh_security_group_ids - description = "External SSH. Allow SSH access to Consul instances from this security group (from ELB or instance)." - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - description = "Default AWS egress rule." - } - - revoke_rules_on_delete = true - - # aws_launch_configuration.launch_configuration in this module sets create_before_destroy to - # true, which means everything it depends on, including this resource, must set it as well, or - # you'll get cyclic dependency errors when you try to do a terraform destroy. - lifecycle { - create_before_destroy = true - } - - tags = merge( - { - "Name" = var.cluster_name - }, - var.tags, - ) -} - -# ------------------------------------------------------------------------------------------------- -# ATTACH AN IAM ROLE TO EACH EC2 INSTANCE -# We can use the IAM role to grant the instance IAM permissions so we can use the AWS CLI without -# having to figure out how to get our secret AWS access keys onto the box. -# ------------------------------------------------------------------------------------------------- -# NOTE: This block has been kept unchanged. -resource "aws_iam_instance_profile" "instance_profile" { - name_prefix = var.cluster_name - path = var.instance_profile_path - role = aws_iam_role.instance_role.name - - # aws_launch_configuration.launch_configuration in this module sets create_before_destroy to true, which means - # everything it depends on, including this resource, must set it as well, or you'll get cyclic dependency errors - # when you try to do a terraform destroy. - lifecycle { - create_before_destroy = true - } -} - -resource "aws_iam_role" "instance_role" { - name_prefix = var.cluster_name - assume_role_policy = data.aws_iam_policy_document.instance_role.json - - # aws_iam_instance_profile.instance_profile in this module sets create_before_destroy to true, which means - # everything it depends on, including this resource, must set it as well, or you'll get cyclic dependency errors - # when you try to do a terraform destroy. - lifecycle { - create_before_destroy = true - } -} - -data "aws_iam_policy_document" "instance_role" { - statement { - effect = "Allow" - actions = ["sts:AssumeRole"] - - principals { - type = "Service" - identifiers = ["ec2.amazonaws.com"] - } - } -} - -# ------------------------------------------------------------------------------------------------- -# THE IAM POLICIES COME FROM THE CONSUL-IAM-POLICIES MODULE -# ------------------------------------------------------------------------------------------------- -# NOTE: This block has been altered -module "iam_policies" { - # Edit: Use remote source - source = "github.com/hashicorp/terraform-aws-consul//modules/consul-iam-policies?ref=v0.7.0" - - #source = "../modules/consul-iam-policies" - - iam_role_id = aws_iam_role.instance_role.id -} - diff --git a/modules/consul-cluster/outputs.tf b/modules/consul-cluster/outputs.tf index f45d730..a94e876 100644 --- a/modules/consul-cluster/outputs.tf +++ b/modules/consul-cluster/outputs.tf @@ -29,7 +29,7 @@ output "iam_role_name" { } output "security_group_id" { - value = aws_security_group.attach_security_group.id + value = module.attach_security_group.security_group_id description = "Security group ID to attach to other security group rules as destination." } diff --git a/modules/consul-cluster/security-groups.tf b/modules/consul-cluster/security-groups.tf new file mode 100644 index 0000000..f483eff --- /dev/null +++ b/modules/consul-cluster/security-groups.tf @@ -0,0 +1,176 @@ +module "attach_security_group" { + source = "terraform-aws-modules/security-group/aws" + version = "4.3.0" + + name = "${var.cluster_name}-att" + description = "Null Placeholder security group for other instances to use as destination to access ${var.cluster_name}" + vpc_id = var.vpc_id + + revoke_rules_on_delete = true + + tags = merge( + { + "Name" = "${var.cluster_name}-null" + }, + var.tags, + ) + + ingress_with_cidr_blocks = [ + { + from_port = 0 + to_port = 0 + protocol = "icmp" + cidr_blocks = "255.255.255.255/32" + description = "(NULL) Terraform requires at least one rule in order to fully manage this security rule" + }, + ] + + egress_with_cidr_blocks = [ + { + from_port = 0 + to_port = 0 + protocol = -1 + cidr_blocks = "0.0.0.0/0" + description = "Default AWS egress rule." + }, + ] +} + +module "lc_security_group" { + source = "terraform-aws-modules/security-group/aws" + version = "4.3.0" + + name = var.cluster_name + description = "Security group for the ${var.cluster_name} launch configuration" + vpc_id = var.vpc_id + + revoke_rules_on_delete = true + + tags = merge( + { + "Name" = var.cluster_name + }, + var.tags, + ) + + ingress_with_source_security_group_id = [ + { + from_port = "8300" + to_port = "8300" + protocol = "tcp" + source_security_group_id = var.vault_security_group_id + }, + { + from_port = "8301" + to_port = "8301" + protocol = "tcp" + source_security_group_id = var.vault_security_group_id + }, + { + from_port = "8302" + to_port = "8302" + protocol = "tcp" + source_security_group_id = var.vault_security_group_id + }, + { + from_port = "8302" + to_port = "8302" + protocol = "udp" + source_security_group_id = var.vault_security_group_id + }, + { + from_port = "8400" + to_port = "8400" + protocol = "tcp" + source_security_group_id = var.vault_security_group_id + }, + { + from_port = "8500" + to_port = "8500" + protocol = "tcp" + source_security_group_id = var.vault_security_group_id + }, + { + from_port = "8600" + to_port = "8600" + protocol = "tcp" + source_security_group_id = var.vault_security_group_id + }, + { + from_port = "8600" + to_port = "8600" + protocol = "udp" + source_security_group_id = var.vault_security_group_id + }, + { + from_port = "22" + to_port = "22" + protocol = "tcp" + source_security_group_id = var.ssh_security_group_id + description = "External SSH. Allow SSH access to Consul instances from this security group (from ELB or instance)." + } + ] + + ingress_with_self = [ + { + from_port = "8300" + to_port = "8300" + protocol = "tcp" + self = true + }, + { + from_port = "8301" + to_port = "8301" + protocol = "tcp" + self = true + }, + { + from_port = "8301" + to_port = "8301" + protocol = "udp" + self = true + }, + { + from_port = "8302" + to_port = "8302" + protocol = "tcp" + self = true + }, + { + from_port = "8302" + to_port = "8302" + protocol = "udp" + self = true + }, + { + from_port = "8500" + to_port = "8500" + protocol = "tcp" + self = true + }, + { + from_port = "8600" + to_port = "8600" + protocol = "tcp" + self = true + description = "Self DNS. Allow consul instances to query themselves for DNS." + }, + { + from_port = "8600" + to_port = "8600" + protocol = "udp" + self = true + description = "Self DNS. Allow consul instances to query themselves for DNS." + }, + ] + + egress_with_cidr_blocks = [ + { + from_port = 0 + to_port = 0 + protocol = -1 + cidr_blocks = "0.0.0.0/0" + description = "Default AWS egress rule." + } + ] +} diff --git a/modules/consul-cluster/variables.tf b/modules/consul-cluster/variables.tf index 19ad859..498e31c 100644 --- a/modules/consul-cluster/variables.tf +++ b/modules/consul-cluster/variables.tf @@ -1,112 +1,110 @@ -# ------------------------------------------------------------------------------------------------- -# Placement (required) -# ------------------------------------------------------------------------------------------------- variable "vpc_id" { description = "The ID of the VPC in which to deploy the Consul cluster" + type = string } variable "subnet_ids" { description = "The subnet IDs into which the EC2 Instances should be deployed. We recommend one subnet ID per node in the cluster_size variable. At least one of var.subnet_ids or var.availability_zones must be non-empty." + type = list(string) } -# ------------------------------------------------------------------------------------------------- -# Operating System (required) -# ------------------------------------------------------------------------------------------------- variable "ami_id" { description = "The ID of the AMI to run in this cluster. Should be an AMI that had Consul installed and configured by the install-consul module." + type = string } variable "user_data" { description = "A User Data script to execute while the server is booting. We remmend passing in a bash script that executes the run-consul script, which should have been installed in the Consul AMI by the install-consul module." + type = string } -# ------------------------------------------------------------------------------------------------- -# Cluster Nodes (optional) -# ------------------------------------------------------------------------------------------------- variable "instance_type" { - description = "The type of EC2 Instances to run for each node in the cluster (e.g. t2.micro)." + description = "The type of EC2 Instances to run for each node in the cluster (e.g. t3.micro)." + default = "t3.micro" + type = string } variable "cluster_size" { description = "The number of nodes to have in the Consul cluster. We strongly recommended that you use either 3 or 5." default = 3 + type = number } variable "tenancy" { description = "The tenancy of the instance. Must be one of: empty string, default or dedicated. For EC2 Spot Instances only empty string or dedicated can be used." default = "" + type = string } variable "root_volume_ebs_optimized" { description = "If true, the launched EC2 instance will be EBS-optimized." default = false + type = bool } variable "root_volume_type" { description = "The type of volume. Must be one of: standard, gp2, or io1." default = "standard" + type = string } variable "root_volume_size" { description = "The size, in GB, of the root EBS volume." default = 50 + type = number } variable "root_volume_delete_on_termination" { description = "Whether the volume should be destroyed on instance termination." default = true + type = bool } -# ------------------------------------------------------------------------------------------------- -# Autoscaling (optional) -# ------------------------------------------------------------------------------------------------- variable "termination_policies" { description = "A list of policies to decide how the instances in the auto scale group should be terminated. The allowed values are OldestInstance, NewestInstance, OldestLaunchConfiguration, ClosestToNextInstanceHour, Default." default = "Default" + type = string } variable "wait_for_capacity_timeout" { description = "A maximum duration that Terraform should wait for ASG instances to be healthy before timing out. Setting this to '0' causes Terraform to skip all Capacity Waiting behavior." default = "10m" + type = string } variable "health_check_type" { description = "Controls how health checking is done. Must be one of EC2 or ELB." default = "EC2" + type = string } variable "health_check_grace_period" { description = "Time, in seconds, after instance comes into service before checking health." default = 60 + type = number } variable "instance_profile_path" { description = "Path in which to create the IAM instance profile." default = "/" + type = string } -# ------------------------------------------------------------------------------------------------- -# Security groups (required) -# ------------------------------------------------------------------------------------------------- -variable "ssh_security_group_ids" { - description = "IDs of the security groups of a bastion ssh instance from where you can ssh into the Consul instances." - type = list(string) +variable "ssh_security_group_id" { + description = "ID of the security group of a bastion ssh instance from where you can ssh into the Consul instances." + type = string } variable "vault_security_group_id" { description = "ID of the security group of the Vault instances to allow traffic from Vault into Consul." + type = string } -# ------------------------------------------------------------------------------------------------- -# Tagging/Naming (required) -# ------------------------------------------------------------------------------------------------- variable "cluster_name" { description = "The name of the Consul cluster (e.g. consul-stage). This variable is used to namespace all resources created by this module." + type = string } -# ------------------------------------------------------------------------------------------------- -# Tagging/Naming (optional) -# ------------------------------------------------------------------------------------------------- variable "tags" { description = "Tags to attach to all AWS resources" type = map(string) @@ -116,10 +114,11 @@ variable "tags" { variable "cluster_tag_key" { description = "Add a tag with this key and the value var.cluster_tag_value to each Instance in the ASG. This can be used to automatically find other Consul nodes and form a cluster." default = "consul-servers" + type = string } variable "cluster_tag_value" { description = "Add a tag with key var.clsuter_tag_key and this value to each Instance in the ASG. This can be used to automatically find other Consul nodes and form a cluster." default = "auto-join" + type = string } - diff --git a/modules/vault-cluster/README.md b/modules/vault-cluster/README.md index 2da19ea..6238504 100644 --- a/modules/vault-cluster/README.md +++ b/modules/vault-cluster/README.md @@ -1,14 +1,35 @@ # Vault Cluster -This module has been copy/pasted from the following repository: -https://github.com/hashicorp/terraform-aws-vault/tree/master/modules/vault-cluster +This module was inspired by the following repository: [terraform-aws-vault][1]. -Security groups have been re-written in order to make sure they are exclusively managed -by Terraform and any other rules that have been added by hand (or other means) will be -removed, whenever this module is called. +## Caveats -This is achieved by moving all separately defined rules from 'aws_security_group_rule' -into a single 'aws_security_group' block. +### Security Groups + +See this [GitHub issue][2], for clarifying the purpose of the SGs and their +rules. + +__IMPORTANT:__ + +1. Vault needs to allow inbound Consul connections. This is done by using + Consul's security group as destination in the vault `lc_security_group` + rules. +1. Consul needs to allow inbound Vault connections. This is done by using + Vault's security group as destination in the consul `lc_security_group` + rules. + +This however creates a circular dependency in Terraform, as both rules need to +be created and linked to each other. +In order to overcome this problem, each of the launch configurations attaches +an (almost) empty NULL security group that can be used by the other in their +`lc_security_group` to act as destination. +Once this behaviour is fixed in Terraform, each second security group will be +removed. +The `attach_security_group` represents the NULL security group that is also +exported by this module in order to be used by security groups of other +machines. + +[Here][3] are the ports in use and their purpose. ## Inputs @@ -51,3 +72,7 @@ into a single 'aws_security_group' block. | iam_role_name | Name of the IAM role attached to the Vault instance. | | security_group_id | Security group ID to attach to other security group rules as destination. | | s3_bucket_arn | ARN of the S3 bucket if used as storage backend | + +[1]: https://github.com/hashicorp/terraform-aws-vault/tree/master/modules/vault-cluster +[2]: https://github.com/hashicorp/terraform-aws-vault/issues/107 +[3]: https://www.consul.io/docs/install/ports#ports-table diff --git a/modules/vault-cluster/data.tf b/modules/vault-cluster/data.tf new file mode 100644 index 0000000..9d0d675 --- /dev/null +++ b/modules/vault-cluster/data.tf @@ -0,0 +1,53 @@ +data "aws_iam_policy_document" "vault_s3" { + count = var.enable_s3_backend ? 1 : 0 + + statement { + effect = "Allow" + actions = ["s3:*"] + + resources = [ + data.aws_s3_bucket.vault_storage[0].arn, + "${data.aws_s3_bucket.vault_storage[0].arn}/*", + ] + } +} + +data "aws_s3_bucket" "vault_storage" { + count = var.enable_s3_backend ? 1 : 0 + bucket = var.s3_bucket_name +} + +data "aws_iam_policy_document" "vault_s3_kms" { + count = var.enable_s3_backend && var.enable_s3_backend_encryption ? 1 : 0 + + statement { + effect = "Allow" + + actions = [ + "kms:Encrypt", + "kms:Decrypt", + "kms:GenerateDataKey", + ] + + resources = [ + data.aws_kms_key.vault_encryption[0].arn, + ] + } +} + +data "aws_kms_key" "vault_encryption" { + count = var.enable_s3_backend && var.enable_s3_backend_encryption ? 1 : 0 + key_id = var.kms_alias_name +} + +data "aws_iam_policy_document" "instance_role" { + statement { + effect = "Allow" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ec2.amazonaws.com"] + } + } +} diff --git a/modules/vault-cluster/iam.tf b/modules/vault-cluster/iam.tf new file mode 100644 index 0000000..3239bec --- /dev/null +++ b/modules/vault-cluster/iam.tf @@ -0,0 +1,38 @@ +resource "aws_iam_role_policy" "vault_s3_kms" { + count = var.enable_s3_backend && var.enable_s3_backend_encryption ? 1 : 0 + name = "vault_s3_kms" + role = aws_iam_role.instance_role.id + policy = element( + concat(data.aws_iam_policy_document.vault_s3_kms.*.json, [""]), + 0, + ) +} + +resource "aws_iam_role_policy" "vault_s3" { + count = var.enable_s3_backend ? 1 : 0 + name = "vault_s3" + role = aws_iam_role.instance_role.id + policy = element( + concat(data.aws_iam_policy_document.vault_s3.*.json, [""]), + 0, + ) +} + +resource "aws_iam_role" "instance_role" { + name_prefix = var.cluster_name + assume_role_policy = data.aws_iam_policy_document.instance_role.json + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_iam_instance_profile" "instance_profile" { + name_prefix = var.cluster_name + path = "/" + role = aws_iam_role.instance_role.name + + lifecycle { + create_before_destroy = true + } +} diff --git a/modules/vault-cluster/locals.tf b/modules/vault-cluster/locals.tf index 02d877e..b896795 100644 --- a/modules/vault-cluster/locals.tf +++ b/modules/vault-cluster/locals.tf @@ -1,9 +1,3 @@ -# ------------------------------------------------------------------------------------------------- -# Locals -# ------------------------------------------------------------------------------------------------- - -# The following example converts key/val maps into AWS ASG 3er tuple maps. -# Credits: https://github.com/terraform-aws-modules/terraform-aws-autoscaling/blob/master/locals.tf locals { tags_asg_format = null_resource.tags_as_list_of_maps.*.triggers } @@ -17,4 +11,3 @@ resource "null_resource" "tags_as_list_of_maps" { "propagate_at_launch" = "true" } } - diff --git a/modules/vault-cluster/main.tf b/modules/vault-cluster/main.tf index 99e7496..fdd37ec 100644 --- a/modules/vault-cluster/main.tf +++ b/modules/vault-cluster/main.tf @@ -1,24 +1,3 @@ -# This module has been copy/pasted from the following repository: -# https://github.com/hashicorp/terraform-aws-vault/tree/master/modules/vault-cluster -# -# Security groups have been re-written in order to make sure they are exclusively managed -# by Terraform and any other rules that have been added by hand (or other means) will be -# removed, whenever this module is called. -# -# This is achieved by moving all separately defined rules from 'aws_security_group_rule' -# into a single 'aws_security_group' block. - -# ------------------------------------------------------------------------------------------------- -# THESE TEMPLATES REQUIRE TERRAFORM VERSION 0.8 AND ABOVE -# ------------------------------------------------------------------------------------------------- -terraform { - required_version = ">= 0.9.3" -} - -# ------------------------------------------------------------------------------------------------- -# CREATE AN AUTO SCALING GROUP (ASG) TO RUN VAULT -# ------------------------------------------------------------------------------------------------- -# NOTE: This block has been kept unchanged. resource "aws_autoscaling_group" "autoscaling_group" { name_prefix = var.cluster_name @@ -26,7 +5,6 @@ resource "aws_autoscaling_group" "autoscaling_group" { vpc_zone_identifier = flatten(var.subnet_ids) - # Use a fixed-size cluster min_size = var.cluster_size max_size = var.cluster_size desired_capacity = var.cluster_size @@ -54,10 +32,6 @@ resource "aws_autoscaling_group" "autoscaling_group" { } } -# ------------------------------------------------------------------------------------------------- -# CREATE LAUNCH CONFIGURATION TO DEFINE WHAT RUNS ON EACH INSTANCE IN THE ASG -# ------------------------------------------------------------------------------------------------- -# NOTE: This block has been altered resource "aws_launch_configuration" "launch_configuration" { name_prefix = "${var.cluster_name}-" image_id = var.ami_id @@ -67,19 +41,6 @@ resource "aws_launch_configuration" "launch_configuration" { iam_instance_profile = aws_iam_instance_profile.instance_profile.name placement_tenancy = var.tenancy - # Edit: key has been removed, as we will add our own SSH keys to the launch configuratoin - #key_name = "${var.ssh_key_name}" - - # Edit: only allow the vault required vault rules and an external group for ssh access - security_groups = [aws_security_group.lc_security_group.id, aws_security_group.attach_security_group.id] - - #security_groups = ["${concat(list(aws_security_group.lc_security_group.id), var.additional_security_group_ids)}"] - - # Edit: removed dynamic configuration option, we want Vault to be served by an ELB - associate_public_ip_address = false - - #associate_public_ip_address = "${var.associate_public_ip_address}" - ebs_optimized = var.root_volume_ebs_optimized root_block_device { volume_type = var.root_volume_type @@ -87,298 +48,7 @@ resource "aws_launch_configuration" "launch_configuration" { delete_on_termination = var.root_volume_delete_on_termination } - # Important note: whenever using a launch configuration with an auto scaling group, you must set - # create_before_destroy = true. However, as soon as you set create_before_destroy = true in one - # resource, you must also set it in every resource that it depends on, or you'll get an error - # about cyclic dependencies (especially when removing resources). For more info, see: - # - # https://www.terraform.io/docs/providers/aws/r/launch_configuration.html - # https://terraform.io/docs/configuration/resources.html - lifecycle { - create_before_destroy = true - } -} - -# ------------------------------------------------------------------------------------------------- -# CREATE A SECURITY GROUP TO CONTROL WHAT REQUESTS CAN GO IN AND OUT OF EACH EC2 INSTANCE -# ------------------------------------------------------------------------------------------------- -# NOTE: This section has been rewritten to use 'aws_security_group' resource. -# CLARIFICATION OF SECURITY GROUPS: https://github.com/hashicorp/terraform-aws-vault/issues/107 - -# IMPORTANT: -# 1. Vault needs to allow inbound Consul connections. This is done by using the Consuls security -# group as destination in the vault "lc_security_group" rules. -# 2. Consul needs to allow inbound Vault connections. This is done by using the Vaults security -# group as destination in the consul "lc_security_group" rules. -# -# This however creates a circular dependency in Terraform, as both rules need to be created -# and linked to each other. -# In order to overcome this problem, each of the launch configurations attaches to an (almost) -# empty NULL security group that can be used by the other in their "lc_security_group" to act -# as destination. -# Once this behaviour is fixed in Terraform, each second security group will be removed. -# The "attach_security_group" represents the NULL security group that is also exported by this -# module in order to be used by security groups of other machines. -resource "aws_security_group" "attach_security_group" { - name_prefix = "${var.cluster_name}-att" - description = "Null Placeholder security group for other instances to use as destination to access ${var.cluster_name}" - vpc_id = var.vpc_id - - # This is the least possible access I came up with. - # Note, if no rule is defined, Terraform is not going to see any manually made changes. - # This is why we need at least one ingress and one egress rule here. - ingress { - from_port = "8" - to_port = "0" - protocol = "icmp" - cidr_blocks = ["255.255.255.255/32"] - description = "(NULL) Terraform requires at least one rule in order to fully manage this security rule" - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - description = "Default AWS egress rule." - } - - revoke_rules_on_delete = true - - # aws_launch_configuration.launch_configuration in this module sets create_before_destroy to - # true, which means everything it depends on, including this resource, must set it as well, or - # you'll get cyclic dependency errors when you try to do a terraform destroy. - lifecycle { - create_before_destroy = true - } - - tags = merge( - { - "Name" = "${var.cluster_name}-null" - }, - var.tags, - ) -} - -# 8200/tcp - api_port: The port to use for Vault API calls -# 8201/tcp - cluster_port: The port to use for Vault server-to-server communication -# 8301/tcp - consul agent: The port used to handle Consul gossip in the LAN. Required by all Consul agents -# 8301/udp - consul agent: The port used to handle Consul gossip in the LAN. Required by all Consul agents -# 22/tcp - ssh: Allow ssh access to vault instances -# -# See all required rules here: https://github.com/hashicorp/terraform-aws-vault/issues/107 -resource "aws_security_group" "lc_security_group" { - name_prefix = var.cluster_name - description = "Security group for the ${var.cluster_name} launch configuration" - vpc_id = var.vpc_id - - # Vault HA connections (ensure vault instances find themselves and form a cluster) - ingress { - from_port = "8201" - to_port = "8201" - protocol = "tcp" - self = true - description = "Self HA Cluster. Allow Vault instances to communicate with each other via their HA cluster port." - } - - # Vault API access (via browser or cli to query the vault) - ingress { - from_port = "8200" - to_port = "8200" - protocol = "tcp" - self = true - description = "Self API. Allow vault instances to access their own API." - } - - ingress { - from_port = "8200" - to_port = "8200" - protocol = "tcp" - security_groups = [var.elb_security_group_id] - description = "External API. Allow API access to Vault instances from this security group (from ELB or instances)." - } - - # Consul Agents for push/pull memberlist (from self) - # If not set for itself throwing this error in /opt/consul/log/consul-stdout.log: - # [ERR] memberlist: Push/Pull with i-00276fbd4e248abc5 failed: dial tcp [vault-server-ip]:8301: i/o timeout - ingress { - from_port = "8301" - to_port = "8301" - protocol = "tcp" - self = true - description = "Consul Agent (TCP). Allow the Vault servers to access the Vault Consul agent from this security group (from ELB or instance)." - } - - ingress { - from_port = "8301" - to_port = "8301" - protocol = "udp" - self = true - description = "Consul Agent (UDP). Allow the Vault servers to access the Vault Consul agent from this security group (from ELB or instance)." - } - - # Consul Agents for push/pull memberlist (from Consul) - # If not set for itself throwing this error in /opt/consul/log/consul-stdout.log: - # [ERR] memberlist: Push/Pull with i-00276fbd4e248abc5 failed: dial tcp [consul-server-ip]:8301: i/o timeout - ingress { - from_port = "8301" - to_port = "8301" - protocol = "tcp" - security_groups = [var.consul_security_group_id] - description = "Consul Agent (TCP). Allow the Consul servers to access the Vault Consul agent from this security group (from ELB or instance)." - } - - ingress { - from_port = "8301" - to_port = "8301" - protocol = "udp" - security_groups = [var.consul_security_group_id] - description = "Consul Agent (UDP). Allow the Consul servers to access the Vault Consul agent from this security group (from ELB or instance)." - } - - # SSH access from bastion host - ingress { - from_port = "22" - to_port = "22" - protocol = "tcp" - security_groups = var.ssh_security_group_ids - description = "External SSH. Allow SSH access to Vault instances from this security group (from ELB or instance)." - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - description = "Default AWS egress rule." - } - - revoke_rules_on_delete = true - - # aws_launch_configuration.launch_configuration in this module sets create_before_destroy to - # true, which means everything it depends on, including this resource, must set it as well, or - # you'll get cyclic dependency errors when you try to do a terraform destroy. - lifecycle { - create_before_destroy = true - } - - tags = merge( - { - "Name" = var.cluster_name - }, - var.tags, - ) -} - -# ------------------------------------------------------------------------------------------------- -# ATTACH AN IAM ROLE TO EACH EC2 INSTANCE -# We can use the IAM role to grant the instance IAM permissions so we can use the AWS APIs without -# having to figure out how to get our secret AWS access keys onto the box. -# ------------------------------------------------------------------------------------------------- -# NOTE: This block has been kept unchanged. -resource "aws_iam_instance_profile" "instance_profile" { - name_prefix = var.cluster_name - path = "/" - role = aws_iam_role.instance_role.name - - # aws_launch_configuration.launch_configuration in this module sets create_before_destroy to - # true, which means everything it depends on, including this resource, must set it as well, or - # you'll get cyclic dependency errors when you try to do a terraform destroy. lifecycle { create_before_destroy = true } } - -resource "aws_iam_role" "instance_role" { - name_prefix = var.cluster_name - assume_role_policy = data.aws_iam_policy_document.instance_role.json - - # aws_launch_configuration.launch_configuration in this module sets create_before_destroy to - # true, which means everything it depends on, including this resource, must set it as well, or - # you'll get cyclic dependency errors when you try to do a terraform destroy. - lifecycle { - create_before_destroy = true - } -} - -data "aws_iam_policy_document" "instance_role" { - statement { - effect = "Allow" - actions = ["sts:AssumeRole"] - - principals { - type = "Service" - identifiers = ["ec2.amazonaws.com"] - } - } -} - -# ------------------------------------------------------------------------------------------------- -# Policy to allow access to S3 -# ------------------------------------------------------------------------------------------------- -resource "aws_iam_role_policy" "vault_s3" { - count = var.enable_s3_backend ? 1 : 0 - name = "vault_s3" - role = aws_iam_role.instance_role.id - policy = element( - concat(data.aws_iam_policy_document.vault_s3.*.json, [""]), - 0, - ) -} - -data "aws_iam_policy_document" "vault_s3" { - count = var.enable_s3_backend ? 1 : 0 - - statement { - effect = "Allow" - actions = ["s3:*"] - - resources = [ - data.aws_s3_bucket.vault_storage[0].arn, - "${data.aws_s3_bucket.vault_storage[0].arn}/*", - ] - } -} - -data "aws_s3_bucket" "vault_storage" { - count = var.enable_s3_backend ? 1 : 0 - bucket = var.s3_bucket_name -} - -# ------------------------------------------------------------------------------------------------- -# Policy to allow access to KMS S3 encryption key -# ------------------------------------------------------------------------------------------------- -# https://keita.blog/2017/02/21/iam-policy-for-kms-encrypted-remote-terraform-state-in-s3/ -resource "aws_iam_role_policy" "vault_s3_kms" { - count = var.enable_s3_backend && var.enable_s3_backend_encryption ? 1 : 0 - name = "vault_s3_kms" - role = aws_iam_role.instance_role.id - policy = element( - concat(data.aws_iam_policy_document.vault_s3_kms.*.json, [""]), - 0, - ) -} - -data "aws_iam_policy_document" "vault_s3_kms" { - count = var.enable_s3_backend && var.enable_s3_backend_encryption ? 1 : 0 - - statement { - effect = "Allow" - - actions = [ - "kms:Encrypt", - "kms:Decrypt", - "kms:GenerateDataKey", - ] - - resources = [ - data.aws_kms_key.vault_encryption[0].arn, - ] - } -} - -data "aws_kms_key" "vault_encryption" { - count = var.enable_s3_backend && var.enable_s3_backend_encryption ? 1 : 0 - key_id = var.kms_alias_name -} - diff --git a/modules/vault-cluster/outputs.tf b/modules/vault-cluster/outputs.tf index 777069f..53aedc9 100644 --- a/modules/vault-cluster/outputs.tf +++ b/modules/vault-cluster/outputs.tf @@ -29,7 +29,7 @@ output "iam_role_name" { } output "security_group_id" { - value = aws_security_group.attach_security_group.id + value = module.attach_security_group.security_group_id description = "Security group ID to attach to other security group rules as destination." } diff --git a/modules/vault-cluster/security-groups.tf b/modules/vault-cluster/security-groups.tf new file mode 100644 index 0000000..1748f3f --- /dev/null +++ b/modules/vault-cluster/security-groups.tf @@ -0,0 +1,128 @@ +module "attach_security_group" { + source = "terraform-aws-modules/security-group/aws" + version = "4.3.0" + + name = "${var.cluster_name}-att" + description = "Null Placeholder security group for other instances to use as destination to access ${var.cluster_name}" + vpc_id = var.vpc_id + + revoke_rules_on_delete = true + + tags = merge( + { + "Name" = "${var.cluster_name}-null" + }, + var.tags, + ) + + ingress_with_cidr_blocks = [ + { + from_port = 0 + to_port = 0 + protocol = "icmp" + cidr_blocks = "255.255.255.255/32" + description = "(NULL) Terraform requires at least one rule in order to fully manage this security rule" + }, + ] + + egress_with_cidr_blocks = [ + { + from_port = 0 + to_port = 0 + protocol = -1 + cidr_blocks = "0.0.0.0/0" + description = "Default AWS egress rule." + }, + ] +} + +module "lc_security_group" { + source = "terraform-aws-modules/security-group/aws" + version = "4.3.0" + + name = var.cluster_name + description = "Security group for the ${var.cluster_name} launch configuration" + vpc_id = var.vpc_id + + revoke_rules_on_delete = true + + tags = merge( + { + "Name" = var.cluster_name + }, + var.tags, + ) + + ingress_with_source_security_group_id = [ + { + from_port = "22" + to_port = "22" + protocol = "tcp" + source_security_group_id = var.ssh_security_group_id + description = "External SSH. Allow SSH access to Vault instances from this security group (from ELB or instance)." + }, + { + from_port = "8301" + to_port = "8301" + protocol = "udp" + source_security_group_id = var.consul_security_group_id + description = "Consul Agent (UDP). Allow the Consul servers to access the Vault Consul agent from this security group (from ELB or instance)." + }, + { + from_port = "8301" + to_port = "8301" + protocol = "tcp" + source_security_group_id = var.consul_security_group_id + description = "Consul Agent (TCP). Allow the Consul servers to access the Vault Consul agent from this security group (from ELB or instance)." + }, + { + from_port = "8200" + to_port = "8200" + protocol = "tcp" + source_security_group_id = var.elb_security_group_id + description = "External API. Allow API access to Vault instances from this security group (from ELB or instances)." + }, + ] + + ingress_with_self = [ + { + from_port = "8301" + to_port = "8301" + protocol = "udp" + self = true + description = "Consul Agent (UDP). Allow the Vault servers to access the Vault Consul agent from this security group (from ELB or instance)." + }, + { + from_port = "8301" + to_port = "8301" + protocol = "tcp" + self = true + description = "Consul Agent (TCP). Allow the Vault servers to access the Vault Consul agent from this security group (from ELB or instance)." + }, + { + from_port = "8200" + to_port = "8200" + protocol = "tcp" + self = true + description = "Self API. Allow vault instances to access their own API." + }, + { + from_port = "8201" + to_port = "8201" + protocol = "tcp" + self = true + description = "Self HA Cluster. Allow Vault instances to communicate with each other via their HA cluster port." + }, + ] + + egress_with_cidr_blocks = [ + { + from_port = 0 + to_port = 0 + protocol = -1 + cidr_blocks = "0.0.0.0/0" + description = "Default AWS egress rule." + }, + ] + +} diff --git a/modules/vault-cluster/variables.tf b/modules/vault-cluster/variables.tf index 92c514b..7c4053c 100644 --- a/modules/vault-cluster/variables.tf +++ b/modules/vault-cluster/variables.tf @@ -1,8 +1,6 @@ -# ------------------------------------------------------------------------------------------------- -# Placement (required) -# ------------------------------------------------------------------------------------------------- variable "vpc_id" { description = "The ID of the VPC in which to deploy the cluster" + type = string } variable "subnet_ids" { @@ -10,130 +8,128 @@ variable "subnet_ids" { type = list(string) } -# ------------------------------------------------------------------------------------------------- -# Operating System (required) -# ------------------------------------------------------------------------------------------------- variable "ami_id" { description = "The ID of the AMI to run in this cluster. Should be an AMI that had Vault installed and configured by the install-vault module." + type = string } variable "user_data" { description = "A User Data script to execute while the server is booting. We recommend passing in a bash script that executes the run-vault script, which should have been installed in the AMI by the install-vault module." + type = string } -# ------------------------------------------------------------------------------------------------- -# Cluster Nodes (optional) -# ------------------------------------------------------------------------------------------------- variable "instance_type" { description = "The type of EC2 Instances to run for each node in the cluster (e.g. t2.micro)." - default = "t2.micro" + default = "t3.micro" + type = string } variable "cluster_size" { description = "The number of nodes to have in the cluster. We strongly recommend setting this to 3 or 5." default = 3 + type = number } variable "tenancy" { description = "The tenancy of the instance. Must be one of: default or dedicated." default = "default" + type = string } variable "root_volume_ebs_optimized" { description = "If true, the launched EC2 instance will be EBS-optimized." default = false + type = bool } variable "root_volume_type" { description = "The type of volume. Must be one of: standard, gp2, or io1." default = "standard" + type = string } variable "root_volume_size" { description = "The size, in GB, of the root EBS volume." default = 50 + type = number } variable "root_volume_delete_on_termination" { description = "Whether the volume should be destroyed on instance termination." default = true + type = bool } -# ------------------------------------------------------------------------------------------------- -# Autoscaling (optional) -# ------------------------------------------------------------------------------------------------- variable "termination_policies" { description = "A list of policies to decide how the instances in the auto scale group should be terminated. The allowed values are OldestInstance, NewestInstance, OldestLaunchConfiguration, ClosestToNextInstanceHour, Default." default = "Default" + type = string } variable "wait_for_capacity_timeout" { description = "A maximum duration that Terraform should wait for ASG instances to be healthy before timing out. Setting this to '0' causes Terraform to skip all Capacity Waiting behavior." default = "10m" + type = string } variable "health_check_type" { description = "Controls how health checking is done. Must be one of EC2 or ELB." default = "EC2" + type = string } variable "health_check_grace_period" { description = "Time, in seconds, after instance comes into service before checking health." default = 60 + type = number } -# ------------------------------------------------------------------------------------------------- -# Security groups (required) -# ------------------------------------------------------------------------------------------------- variable "elb_security_group_id" { description = "ID of the security group of a public ELB from which you can API access the Vault instances." + type = string } -variable "ssh_security_group_ids" { - description = "IDs of the security groups of a bastion ssh instance from where you can ssh into the Vault instances." - type = list(string) +variable "ssh_security_group_id" { + description = "ID of the security group of a bastion ssh instance from where you can ssh into the Vault instances." + type = string } variable "consul_security_group_id" { description = "ID of the security group of the Consul instances to allow traffic from Consul into Vault." + type = string } -# ------------------------------------------------------------------------------------------------- -# Tagging/Naming (required) -# ------------------------------------------------------------------------------------------------- variable "cluster_name" { description = "The name of the Vault cluster (e.g. vault-stage). This variable is used to namespace all resources created by this module." + type = string } -# ------------------------------------------------------------------------------------------------- -# Tagging/Naming (optional) -# ------------------------------------------------------------------------------------------------- variable "tags" { description = "Tags to attach to all AWS resources" type = map(string) default = {} } -# ------------------------------------------------------------------------------------------------- -# S3 backend (optional) -# ------------------------------------------------------------------------------------------------- variable "enable_s3_backend" { description = "Whether to configure an S3 storage backend in addition to Consul." default = false + type = bool } variable "s3_bucket_name" { description = "The name of the S3 bucket in the same region to use as a storage backend. Only used if 'enable_s3_backend' is set to true." default = "" + type = string } variable "enable_s3_backend_encryption" { description = "Whether to configure the S3 storage backend to be encrypted with a KMS key." default = false + type = bool } variable "kms_alias_name" { description = "The name of the KMS key that is used for S3 storage backend encryption." default = "" + type = string } - diff --git a/outputs.tf b/outputs.tf index a3c27e3..34ff54a 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,6 +1,3 @@ -# ------------------------------------------------------------------------------------------------- -# ELB -# ------------------------------------------------------------------------------------------------- output "elb_fqdn_vault" { value = module.vault_elb.fqdn description = "The AWS provided CNAME of the Vault ELB." @@ -11,9 +8,6 @@ output "elb_route53_public_dns_name_vault" { description = "The Route53 name attached to the Vault ELB, if spcified in variables." } -# ------------------------------------------------------------------------------------------------- -# Autoscaling Groups -# ------------------------------------------------------------------------------------------------- output "asg_name_consul_cluster" { value = module.consul_cluster.asg_name description = "Autoscaling group name of the Consul cluster." @@ -24,9 +18,6 @@ output "asg_name_vault_cluster" { description = "Autoscaling group name of the Vault cluster." } -# ------------------------------------------------------------------------------------------------- -# Launch Configuration -# ------------------------------------------------------------------------------------------------- output "launch_config_name_consul_cluster" { value = module.consul_cluster.launch_config_name description = "Launch configuration name of the Consul cluster." @@ -37,9 +28,6 @@ output "launch_config_name_vault_cluster" { description = "Launch configuration name of the Vault cluster." } -# ------------------------------------------------------------------------------------------------- -# IAM -# ------------------------------------------------------------------------------------------------- output "iam_role_arn_consul_cluster" { value = module.consul_cluster.iam_role_arn description = "IAM role ARN attached to the Consul cluster." @@ -60,9 +48,6 @@ output "iam_role_id_vault_cluster" { description = "IAM role ID attached to the Vault cluster." } -# ------------------------------------------------------------------------------------------------- -# Security Groups -# ------------------------------------------------------------------------------------------------- output "security_group_id_consul_cluster" { value = module.consul_cluster.security_group_id description = "Security group ID of the Consul cluster to attach to other security group rules." @@ -73,11 +58,7 @@ output "security_group_id_vault_cluster" { description = "Security group ID of the Vault cluster to attach to other security group rules." } -# ------------------------------------------------------------------------------------------------- -# AWS -# ------------------------------------------------------------------------------------------------- output "aws_region" { value = data.aws_region.current.name description = "Used AWS region." } - diff --git a/variables.tf b/variables.tf index 073c133..c7e8544 100644 --- a/variables.tf +++ b/variables.tf @@ -1,8 +1,6 @@ -# ------------------------------------------------------------------------------------------------- -# VPC (required) -# ------------------------------------------------------------------------------------------------- variable "vpc_id" { description = "The VPC ID into which you want to provision Vault." + type = string } variable "public_subnet_ids" { @@ -15,12 +13,10 @@ variable "private_subnet_ids" { type = list(string) } -# ------------------------------------------------------------------------------------------------- -# Resource Naming/Tagging (optional) -# ------------------------------------------------------------------------------------------------- variable "name" { description = "The name(-prefix) tag to apply to all AWS resources" default = "vault" + type = string } variable "tags" { @@ -32,58 +28,53 @@ variable "tags" { variable "consul_cluster_name" { description = "What to name the Consul server cluster and all of its associated resources" default = "vault-consul" + type = string } variable "vault_cluster_name" { description = "What to name the Vault server cluster and all of its associated resources" default = "vault-vault" + type = string } -# ------------------------------------------------------------------------------------------------- -# DNS (optional) -# ------------------------------------------------------------------------------------------------- variable "vault_route53_public_dns_name" { description = "The Route53 public DNS name for the vault ELB. If not set, no Route53 record will be created." default = "" + type = string } -# ------------------------------------------------------------------------------------------------- -# Instances (required) -# ------------------------------------------------------------------------------------------------- variable "ssh_keys" { description = "A list of public ssh keys to add to authorized_keys files." type = list(string) } -# ------------------------------------------------------------------------------------------------- -# Instances (optional) -# ------------------------------------------------------------------------------------------------- variable "consul_instance_type" { description = "The type of EC2 Instance to run in the Consul ASG" - default = "t2.micro" + default = "t3.micro" + type = string } variable "vault_instance_type" { description = "The type of EC2 Instance to run in the Vault ASG" - default = "t2.micro" + default = "t3.micro" + type = string } variable "consul_cluster_size" { description = "The number of Consul server nodes to deploy. We strongly recommend using 3 or 5." default = 3 + type = number } variable "vault_cluster_size" { description = "The number of Vault server nodes to deploy. We strongly recommend using 3 or 5." default = 3 + type = number } -# ------------------------------------------------------------------------------------------------- -# Security -# ------------------------------------------------------------------------------------------------- -variable "ssh_security_group_ids" { - description = "Security group IDs of a bastion (or other EC2 instance) from which you will be allowed to ssh into Vault and Consul." - type = list(string) +variable "ssh_security_group_id" { + description = "Security group ID of a bastion (or other EC2 instance) from which you will be allowed to ssh into Vault and Consul." + type = string } variable "vault_ingress_cidr_https" { @@ -103,32 +94,34 @@ variable "ssl_certificate_id" { type = string } -# ------------------------------------------------------------------------------------------------- -# S3 backend (optional) -# ------------------------------------------------------------------------------------------------- variable "enable_s3_backend" { description = "Whether to configure an S3 storage backend in the same region in addition to Consul." default = false + type = bool } variable "s3_bucket_name" { description = "The name of the S3 bucket in the same region to use as a storage backend. Only used if 'enable_s3_backend' is set to true." default = "" + type = string } variable "enable_s3_backend_encryption" { description = "Whether to configure the S3 storage backend to be encrypted with a KMS key." default = false + type = bool } variable "kms_alias_name" { description = "The name of the KMS key that is used for S3 storage backend encryption." default = "" + type = string } variable "ami_name_filter" { description = "Name filter to help pick the AMI." default = ["vault-consul-ubuntu-*"] + type = list(string) } variable "ami_id" { @@ -140,4 +133,5 @@ variable "ami_id" { variable "ami_owner" { description = "AWS account ID of the AMI owner. Defaults to HashiCorp." default = "562637147889" + type = string }