diff --git a/README.md b/README.md index d562225..069bb7c 100644 --- a/README.md +++ b/README.md @@ -111,13 +111,13 @@ Security scanning is graciously provided by Prowler. Proowler is the leading ful | [aws_kms_key.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | | [aws_lambda_permission.sns_lambda_slack_invoke](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | | [aws_secretsmanager_secret.secret_redis](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | +| [aws_secretsmanager_secret_version.redis_credentials](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | | [aws_security_group_rule.cidr_ingress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_security_group_rule.default_ingress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_sns_topic.slack_topic](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource | | [aws_sns_topic_subscription.slack-endpoint](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription) | resource | | [random_password.password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | | [archive_file.lambdazip](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | -| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | ## Inputs @@ -130,8 +130,11 @@ Security scanning is graciously provided by Prowler. Proowler is the leading ful | [allowed\_security\_groups](#input\_allowed\_security\_groups) | A list of Security Group ID's to allow access to | `list(any)` | `[]` | no | | [at\_rest\_encryption\_enabled](#input\_at\_rest\_encryption\_enabled) | (Optional) Whether to enable encryption at rest | `bool` | `true` | no | | [automatic\_failover\_enabled](#input\_automatic\_failover\_enabled) | Enable automatic failover | `bool` | `true` | no | -| [availability\_zones](#input\_availability\_zones) | The no. of AZs | `string` | `2` | no | +| [availability\_zones](#input\_availability\_zones) | The no. of AZs | `list(string)` | `[]` | no | | [cloudwatch\_metric\_alarms\_enabled](#input\_cloudwatch\_metric\_alarms\_enabled) | Boolean flag to enable/disable CloudWatch metrics alarms | `bool` | `false` | no | +| [cluster\_mode\_enabled](#input\_cluster\_mode\_enabled) | Whether to enable/disable creation of a native redis cluster. | `bool` | `false` | no | +| [cluster\_mode\_num\_node\_groups](#input\_cluster\_mode\_num\_node\_groups) | Number of node groups (shards) for this Redis replication group. | `number` | `0` | no | +| [cluster\_mode\_replicas\_per\_node\_group](#input\_cluster\_mode\_replicas\_per\_node\_group) | Number of replica nodes in each node group. Valid values are between 0 to 5. | `number` | `0` | no | | [cw\_sns\_topic\_arn](#input\_cw\_sns\_topic\_arn) | The username to use when sending notifications to Slack. | `string` | `""` | no | | [engine\_log\_destination](#input\_engine\_log\_destination) | The destination for engine logs(eg. Cloudwatch log-group name or kinesis firehose stream name) | `string` | `null` | no | | [engine\_log\_destination\_type](#input\_engine\_log\_destination\_type) | The type of destination for engine logs(eg . cloudwatch-logs or kinesis-firehose) | `string` | `""` | no | @@ -148,6 +151,7 @@ Security scanning is graciously provided by Prowler. Proowler is the leading ful | [notification\_topic\_arn](#input\_notification\_topic\_arn) | (Optional) ARN of an SNS topic to send ElastiCache notifications | `string` | `null` | no | | [num\_cache\_nodes](#input\_num\_cache\_nodes) | The number of cache nodes | `number` | `1` | no | | [ok\_actions](#input\_ok\_actions) | The list of actions to execute when this alarm transitions into an OK state from any other state. Each action is specified as an Amazon Resource Number (ARN) | `list(string)` | `[]` | no | +| [parameter](#input\_parameter) | A list of Redis parameters to apply. It can be different based on mode slection. |
list(object({
name = string
value = string
}))
| `[]` | no | | [parameter\_group\_description](#input\_parameter\_group\_description) | Parameter group | `string` | `null` | no | | [port](#input\_port) | The redis port | `number` | `6379` | no | | [recovery\_window\_aws\_secret](#input\_recovery\_window\_aws\_secret) | Number of days that AWS Secrets Manager waits before it can delete the secret. This value can be 0 to force deletion without recovery or range from 7 to 30 days. | `number` | `0` | no | @@ -170,11 +174,11 @@ Security scanning is graciously provided by Prowler. Proowler is the leading ful |------|-------------| | [auth\_token\_password](#output\_auth\_token\_password) | Elasticache-redis auth token password(this password may be old, because Terraform doesn't track it after initial creation) | | [elastic\_cache\_redis\_cluster\_id](#output\_elastic\_cache\_redis\_cluster\_id) | ID of the elasticache-redis cluster | -| [elastic\_cache\_redis\_endpoint](#output\_elastic\_cache\_redis\_endpoint) | Elasticache-redis cluster primary endpoint address | | [elastic\_cache\_redis\_port](#output\_elastic\_cache\_redis\_port) | Port number of Redis | | [elastic\_cache\_redis\_primary\_endpoint\_address](#output\_elastic\_cache\_redis\_primary\_endpoint\_address) | Primary endpoint address of redis | | [elastic\_cache\_redis\_security\_group](#output\_elastic\_cache\_redis\_security\_group) | The security group ID of the cluster | | [elastic\_cache\_redis\_subnet\_group\_name](#output\_elastic\_cache\_redis\_subnet\_group\_name) | Subnet group name of the elasticache\_redis cluster | +| [reader\_endpoint\_address](#output\_reader\_endpoint\_address) | The address of the endpoint for the reader node in the replication group, if the cluster mode is disabled. | ## Contribute & Issue Report diff --git a/examples/complete-cluster-mode/README.md b/examples/complete-cluster-mode/README.md new file mode 100644 index 0000000..31dd7f6 --- /dev/null +++ b/examples/complete-cluster-mode/README.md @@ -0,0 +1,47 @@ +## Redis Example With Cluster Mode Enable +![squareops_avatar] + +[squareops_avatar]: https://squareops.com/wp-content/uploads/2022/12/squareops-logo.png + +### [SquareOps Technologies](https://squareops.com/) Your DevOps Partner for Accelerating cloud journey. +
+ +This example will be very useful for users who are new to a module and want to quickly learn how to use it. By reviewing the examples, users can gain a better understanding of how the module works, what features it supports, and how to customize it to their specific needs. + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 0.13 | +| [aws](#requirement\_aws) | >= 3.63 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [redis](#module\_redis) | squareops/elasticache-redis/aws | n/a | + +## Resources + +No resources. + +## Inputs + +No inputs. + +## Outputs + +| Name | Description | +|------|-------------| +| [auth\_token\_password](#output\_auth\_token\_password) | Elasticache-redis auth token password(this password may be old, because Terraform doesn't track it after initial creation) | +| [elastic\_cache\_redis\_endpoint](#output\_elastic\_cache\_redis\_endpoint) | Elasticache-redis cluster primary endpoint address | +| [elastic\_cache\_redis\_security\_group](#output\_elastic\_cache\_redis\_security\_group) | The security group ID of the cluster | +| [id\_of\_redis\_cluster](#output\_id\_of\_redis\_cluster) | ID of the elasticache-redis cluster | +| [port\_no](#output\_port\_no) | Port number of Redis | +| [primary\_endpoint\_address](#output\_primary\_endpoint\_address) | Primary endpoint address of redis | +| [redis\_subnet\_group\_name](#output\_redis\_subnet\_group\_name) | Subnet group name of the elasticache-redis cluster | + diff --git a/examples/complete-cluster-mode/main.tf b/examples/complete-cluster-mode/main.tf new file mode 100644 index 0000000..27e41c1 --- /dev/null +++ b/examples/complete-cluster-mode/main.tf @@ -0,0 +1,123 @@ +locals { + name = "redis" + region = "us-east-2" + family = "redis6.x" + node_type = "cache.t3.small" + vpc_cidr = "10.0.0.0/16" + environment = "prod" + allowed_security_groups = ["sg-02c3f55874f6e0c64"] + redis_engine_version = "6.0" + additional_tags = { + Owner = "Organization_Name" + Expires = "Never" + Department = "Engineering" + } + current_identity = data.aws_caller_identity.current.arn + availability_zones = slice(data.aws_availability_zones.primary.names, 0, 3) + cluster_mode_enabled = true +} + +data "aws_availability_zones" "primary" {} +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +module "kms" { + source = "terraform-aws-modules/kms/aws" + + deletion_window_in_days = 7 + description = "Complete key example showing various configurations available" + enable_key_rotation = false + is_enabled = true + key_usage = "ENCRYPT_DECRYPT" + multi_region = false + + # Policy + enable_default_policy = true + key_owners = [local.current_identity] + key_administrators = [local.current_identity] + key_users = [local.current_identity] + key_service_users = [local.current_identity] + key_statements = [ + { + sid = "Allow use of the key" + actions = [ + "kms:Encrypt*", + "kms:Decrypt*", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Describe*" + ] + resources = ["*"] + + principals = [ + { + type = "Service" + identifiers = [ + "elasticache.amazonaws.com" + ] + } + ] + }, + { + sid = "Enable IAM User Permissions" + actions = ["kms:*"] + resources = ["*"] + + principals = [ + { + type = "AWS" + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + data.aws_caller_identity.current.arn, + ] + } + ] + } + ] + + # Aliases + aliases = ["${local.name}"] + + tags = local.additional_tags +} + +module "vpc" { + source = "squareops/vpc/aws" + version = "3.3.1" + name = local.name + vpc_cidr = local.vpc_cidr + environment = local.environment + availability_zones = local.availability_zones + public_subnet_enabled = true + auto_assign_public_ip = true + intra_subnet_enabled = false + private_subnet_enabled = true + one_nat_gateway_per_az = false + database_subnet_enabled = true +} + +module "redis" { + source = "squareops/elasticache-redis/aws" + name = local.name + family = local.family + node_type = local.node_type + environment = local.environment + engine_version = local.redis_engine_version + cluster_mode_enabled = local.cluster_mode_enabled + cluster_mode_num_node_groups = 1 + cluster_mode_replicas_per_node_group = 2 + vpc_id = module.vpc.vpc_id + subnets = module.vpc.database_subnets + kms_key_arn = module.kms.key_arn + multi_az_enabled = true + availability_zones = local.availability_zones + snapshot_window = "07:00-08:00" + maintenance_window = "sun:09:00-sun:10:00" + allowed_security_groups = local.allowed_security_groups + cloudwatch_metric_alarms_enabled = false + alarm_cpu_threshold_percent = 70 + alarm_memory_threshold_bytes = "10000000" # in bytes + slack_username = "" + slack_channel = "" + slack_webhook_url = "" +} diff --git a/examples/complete-cluster-mode/outputs.tf b/examples/complete-cluster-mode/outputs.tf new file mode 100644 index 0000000..5774b24 --- /dev/null +++ b/examples/complete-cluster-mode/outputs.tf @@ -0,0 +1,35 @@ +output "primary_endpoint_address" { + description = "Primary endpoint address of redis" + value = module.redis.elastic_cache_redis_primary_endpoint_address +} + +output "redis_subnet_group_name" { + description = "Subnet group name of the elasticache-redis cluster" + value = module.redis.elastic_cache_redis_subnet_group_name + +} + +output "id_of_redis_cluster" { + description = "ID of the elasticache-redis cluster" + value = module.redis.elastic_cache_redis_cluster_id +} + +output "port_no" { + description = "Port number of Redis" + value = module.redis.elastic_cache_redis_port +} + +output "elastic_cache_redis_reader_endpoint" { + description = "Elasticache-redis cluster primary endpoint address" + value = module.redis.reader_endpoint_address +} + +output "elastic_cache_redis_security_group" { + description = "The security group ID of the cluster" + value = module.redis.elastic_cache_redis_security_group +} + +output "auth_token_password" { + description = "Elasticache-redis auth token password(this password may be old, because Terraform doesn't track it after initial creation)" + value = module.redis.auth_token_password +} diff --git a/examples/complete-cluster-mode/provider.tf b/examples/complete-cluster-mode/provider.tf new file mode 100644 index 0000000..369af88 --- /dev/null +++ b/examples/complete-cluster-mode/provider.tf @@ -0,0 +1,6 @@ +provider "aws" { + region = local.region + default_tags { + tags = local.additional_tags + } +} diff --git a/examples/complete-cluster-mode/versions.tf b/examples/complete-cluster-mode/versions.tf new file mode 100644 index 0000000..765919c --- /dev/null +++ b/examples/complete-cluster-mode/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 0.13" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 3.63" + } + } +} diff --git a/examples/complete/README.md b/examples/complete/README.md index 5cca81a..89bdcd2 100644 --- a/examples/complete/README.md +++ b/examples/complete/README.md @@ -17,17 +17,25 @@ This example will be very useful for users who are new to a module and want to q ## Providers -No providers. +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 3.63 | ## Modules | Name | Source | Version | |------|--------|---------| +| [kms](#module\_kms) | terraform-aws-modules/kms/aws | n/a | | [redis](#module\_redis) | squareops/elasticache-redis/aws | n/a | +| [vpc](#module\_vpc) | squareops/vpc/aws | 3.3.1 | ## Resources -No resources. +| Name | Type | +|------|------| +| [aws_availability_zones.primary](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | ## Inputs @@ -38,7 +46,7 @@ No inputs. | Name | Description | |------|-------------| | [auth\_token\_password](#output\_auth\_token\_password) | Elasticache-redis auth token password(this password may be old, because Terraform doesn't track it after initial creation) | -| [elastic\_cache\_redis\_endpoint](#output\_elastic\_cache\_redis\_endpoint) | Elasticache-redis cluster primary endpoint address | +| [elastic\_cache\_redis\_reader\_endpoint](#output\_elastic\_cache\_redis\_reader\_endpoint) | Elasticache-redis cluster primary endpoint address | | [elastic\_cache\_redis\_security\_group](#output\_elastic\_cache\_redis\_security\_group) | The security group ID of the cluster | | [id\_of\_redis\_cluster](#output\_id\_of\_redis\_cluster) | ID of the elasticache-redis cluster | | [port\_no](#output\_port\_no) | Port number of Redis | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index 13bdab2..10d2a42 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -3,17 +3,96 @@ locals { region = "us-east-2" family = "redis6.x" node_type = "cache.t3.small" - vpc_id = "vpc-0220830b5260698db" - subnet_ids = ["subnet-0d4dee4a7ea31a96d", "subnet-07fdc14616382f833"] - kms_key_arn = "" + vpc_cidr = "10.0.0.0/16" + allowed_security_groups = ["sg-09b5da32f11bc36f"] environment = "prod" redis_engine_version = "6.0" - allowed_security_groups = ["sg-02c3f55874f6e0c64"] additional_tags = { Owner = "Organization_Name" Expires = "Never" Department = "Engineering" } + current_identity = data.aws_caller_identity.current.arn + availability_zones = slice(data.aws_availability_zones.primary.names, 0, 3) +} + +data "aws_availability_zones" "primary" {} +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +module "kms" { + source = "terraform-aws-modules/kms/aws" + + deletion_window_in_days = 7 + description = "Complete key example showing various configurations available" + enable_key_rotation = false + is_enabled = true + key_usage = "ENCRYPT_DECRYPT" + multi_region = false + + # Policy + enable_default_policy = true + key_owners = [local.current_identity] + key_administrators = [local.current_identity] + key_users = [local.current_identity] + key_service_users = [local.current_identity] + key_statements = [ + { + sid = "Allow use of the key" + actions = [ + "kms:Encrypt*", + "kms:Decrypt*", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:Describe*" + ] + resources = ["*"] + + principals = [ + { + type = "Service" + identifiers = [ + "elasticache.amazonaws.com" + ] + } + ] + }, + { + sid = "Enable IAM User Permissions" + actions = ["kms:*"] + resources = ["*"] + + principals = [ + { + type = "AWS" + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + data.aws_caller_identity.current.arn, + ] + } + ] + } + ] + + # Aliases + aliases = ["${local.name}"] + + tags = local.additional_tags +} + +module "vpc" { + source = "squareops/vpc/aws" + version = "3.3.1" + name = local.name + vpc_cidr = local.vpc_cidr + environment = local.environment + availability_zones = local.availability_zones + public_subnet_enabled = true + auto_assign_public_ip = true + intra_subnet_enabled = false + private_subnet_enabled = true + one_nat_gateway_per_az = false + database_subnet_enabled = true } module "redis" { @@ -24,15 +103,15 @@ module "redis" { environment = local.environment engine_version = local.redis_engine_version num_cache_nodes = 2 - vpc_id = local.vpc_id - subnets = local.subnet_ids - kms_key_arn = local.kms_key_arn - multi_az_enabled = false - availability_zones = 2 + vpc_id = module.vpc.vpc_id + subnets = module.vpc.database_subnets + kms_key_arn = module.kms.key_arn + multi_az_enabled = true + availability_zones = local.availability_zones snapshot_window = "07:00-08:00" maintenance_window = "sun:09:00-sun:10:00" allowed_security_groups = local.allowed_security_groups - cloudwatch_metric_alarms_enabled = true + cloudwatch_metric_alarms_enabled = false alarm_cpu_threshold_percent = 70 alarm_memory_threshold_bytes = "10000000" # in bytes slack_username = "" diff --git a/examples/complete/outputs.tf b/examples/complete/outputs.tf index c45b09c..5774b24 100644 --- a/examples/complete/outputs.tf +++ b/examples/complete/outputs.tf @@ -19,9 +19,9 @@ output "port_no" { value = module.redis.elastic_cache_redis_port } -output "elastic_cache_redis_endpoint" { +output "elastic_cache_redis_reader_endpoint" { description = "Elasticache-redis cluster primary endpoint address" - value = module.redis.elastic_cache_redis_endpoint + value = module.redis.reader_endpoint_address } output "elastic_cache_redis_security_group" { diff --git a/lambda/sns_slack.zip b/lambda/sns_slack.zip new file mode 100644 index 0000000..a4b3a7a Binary files /dev/null and b/lambda/sns_slack.zip differ diff --git a/main.tf b/main.tf index 547afa3..47437c8 100644 --- a/main.tf +++ b/main.tf @@ -6,9 +6,8 @@ locals { engine_log = var.engine_log_destination == null ? [] : [1] } -data "aws_availability_zones" "available" {} - resource "random_password" "password" { + count = var.transit_encryption_enabled ? 1 : 0 length = 16 special = false } @@ -27,6 +26,13 @@ resource "aws_elasticache_parameter_group" "default" { description, ] } + dynamic "parameter" { + for_each = var.cluster_mode_enabled ? concat([{ name = "cluster-enabled", value = "yes" }], var.parameter) : var.parameter + content { + name = parameter.value.name + value = tostring(parameter.value.value) + } + } } resource "aws_elasticache_replication_group" "redis" { @@ -36,22 +42,25 @@ resource "aws_elasticache_replication_group" "redis" { node_type = var.node_type description = "Redis cluster for ${var.environment}-${var.name}-redis" engine_version = var.engine_version - num_cache_clusters = var.num_cache_nodes + num_cache_clusters = var.cluster_mode_enabled ? null : var.num_cache_nodes parameter_group_name = join("", aws_elasticache_parameter_group.default.*.name) #var.parameter_group_name security_group_ids = [module.security_group_redis.security_group_id] subnet_group_name = aws_elasticache_subnet_group.elasticache.id - preferred_cache_cluster_azs = [for n in range(0, var.availability_zones) : data.aws_availability_zones.available.names[n]] + preferred_cache_cluster_azs = length(var.availability_zones) == 0 ? null : [for n in range(0, var.num_cache_nodes) : element(var.availability_zones, n)] snapshot_arns = var.snapshot_arns snapshot_window = var.snapshot_window snapshot_retention_limit = var.snapshot_retention_limit - automatic_failover_enabled = var.automatic_failover_enabled + automatic_failover_enabled = var.cluster_mode_enabled ? true : var.automatic_failover_enabled multi_az_enabled = var.multi_az_enabled - kms_key_id = var.kms_key_arn - auth_token = var.transit_encryption_enabled ? random_password.password.result : null - transit_encryption_enabled = var.transit_encryption_enabled + at_rest_encryption_enabled = var.at_rest_encryption_enabled + kms_key_id = var.at_rest_encryption_enabled ? var.kms_key_arn : null + auth_token = var.transit_encryption_enabled ? random_password.password[0].result : null + transit_encryption_enabled = var.transit_encryption_enabled ? var.transit_encryption_enabled || random_password.password[0].result != null : false notification_topic_arn = var.notification_topic_arn maintenance_window = var.maintenance_window final_snapshot_identifier = var.final_snapshot_identifier + num_node_groups = var.cluster_mode_enabled ? var.cluster_mode_num_node_groups : null + replicas_per_node_group = var.cluster_mode_enabled ? var.cluster_mode_replicas_per_node_group : null dynamic "log_delivery_configuration" { for_each = local.slow_log @@ -79,6 +88,7 @@ resource "aws_elasticache_replication_group" "redis" { } } + resource "aws_elasticache_subnet_group" "elasticache" { name = "${var.environment}-${var.name}-redis" subnet_ids = var.subnets @@ -146,6 +156,16 @@ resource "aws_secretsmanager_secret" "secret_redis" { recovery_window_in_days = var.recovery_window_aws_secret } +resource "aws_secretsmanager_secret_version" "redis_credentials" { + count = var.transit_encryption_enabled ? 1 : 0 + secret_id = aws_secretsmanager_secret.secret_redis[0].id + secret_string = <