diff --git a/modules/opensearch/main.tf b/modules/opensearch/main.tf new file mode 100644 index 0000000..a5e9609 --- /dev/null +++ b/modules/opensearch/main.tf @@ -0,0 +1,447 @@ +data "aws_region" "current" { + count = var.create ? 1 : 0 +} +data "aws_partition" "current" { + count = var.create ? 1 : 0 +} +data "aws_caller_identity" "current" { + count = var.create ? 1 : 0 +} +data "aws_iam_session_context" "current" { + count = var.create ? 1 : 0 + arn = data.aws_caller_identity.current[0].arn +} + +locals { + account_id = try(data.aws_caller_identity.current[0].account_id, "") + partition = try(data.aws_partition.current[0].partition, "") + region = try(data.aws_region.current[0].name, "") + + static_domain_arn = "arn:${local.partition}:es:${local.region}:${local.account_id}:domain/${var.domain_name}" + + tags = merge(var.tags, { terraform-aws-modules = "opensearch" }) +} + +################################################################################ +# Domain +################################################################################ + +resource "aws_opensearch_domain" "this" { + count = var.create ? 1 : 0 + + # Controlled via aws_opensearch_domain_policy below + # access_policies = var.access_policies + advanced_options = var.advanced_options + + dynamic "advanced_security_options" { + for_each = length(var.advanced_security_options) > 0 ? [var.advanced_security_options] : [] + + content { + anonymous_auth_enabled = try(advanced_security_options.value.anonymous_auth_enabled, false) + enabled = try(advanced_security_options.value.enabled, true) + internal_user_database_enabled = try(advanced_security_options.value.internal_user_database_enabled, null) + + dynamic "master_user_options" { + for_each = try([advanced_security_options.value.master_user_options], [{}]) + + content { + master_user_arn = try(master_user_options.value.master_user_arn, null) != null ? try(master_user_options.value.master_user_arn, data.aws_iam_session_context.current[0].issuer_arn) : null + master_user_name = try(master_user_options.value.master_user_arn, null) == null ? try(master_user_options.value.master_user_name, null) : null + master_user_password = try(master_user_options.value.master_user_arn, null) == null ? try(master_user_options.value.master_user_password, null) : null + } + } + } + } + + dynamic "auto_tune_options" { + for_each = length(var.auto_tune_options) > 0 ? [var.auto_tune_options] : [] + + content { + desired_state = try(auto_tune_options.value.desired_state, "ENABLED") + + dynamic "maintenance_schedule" { + for_each = try(auto_tune_options.value.maintenance_schedule, []) + + content { + cron_expression_for_recurrence = maintenance_schedule.value.cron_expression_for_recurrence + + dynamic "duration" { + for_each = [maintenance_schedule.value.duration] + + content { + unit = duration.value.unit + value = duration.value.value + } + } + + start_at = maintenance_schedule.value.start_at + } + } + + rollback_on_disable = try(auto_tune_options.value.rollback_on_disable, null) + } + } + + dynamic "cluster_config" { + for_each = length(var.cluster_config) > 0 ? [var.cluster_config] : [] + + content { + dynamic "cold_storage_options" { + for_each = try([cluster_config.value.cold_storage_options], []) + + content { + enabled = try(cold_storage_options.value.enabled, null) + } + } + + dedicated_master_count = try(cluster_config.value.dedicated_master_count, 3) + dedicated_master_enabled = try(cluster_config.value.dedicated_master_enabled, true) + dedicated_master_type = try(cluster_config.value.dedicated_master_type, "c6g.large.search") + instance_count = try(cluster_config.value.instance_count, 3) + instance_type = try(cluster_config.value.instance_type, "r6g.large.search") + warm_count = try(cluster_config.value.warm_count, null) + warm_enabled = try(cluster_config.value.warm_enabled, null) + warm_type = try(cluster_config.value.warm_type, null) + + dynamic "zone_awareness_config" { + for_each = try([cluster_config.value.zone_awareness_config], []) + + content { + availability_zone_count = try(zone_awareness_config.value.availability_zone_count, null) + } + } + + zone_awareness_enabled = try(cluster_config.value.zone_awareness_enabled, true) + } + } + + dynamic "cognito_options" { + for_each = length(var.cognito_options) > 0 ? [var.cognito_options] : [] + + content { + enabled = try(cognito_options.value.enabled, null) + identity_pool_id = cognito_options.value.identity_pool_id + role_arn = cognito_options.value.role_arn + user_pool_id = cognito_options.value.user_pool_id + } + } + + dynamic "domain_endpoint_options" { + for_each = length(var.domain_endpoint_options) > 0 ? [var.domain_endpoint_options] : [] + + content { + custom_endpoint = try(domain_endpoint_options.value.custom_endpoint, null) + custom_endpoint_certificate_arn = try(domain_endpoint_options.value.custom_endpoint_certificate_arn, null) + custom_endpoint_enabled = try(domain_endpoint_options.value.custom_endpoint_enabled, null) + enforce_https = try(domain_endpoint_options.value.enforce_https, true) + tls_security_policy = try(domain_endpoint_options.value.tls_security_policy, "Policy-Min-TLS-1-2-2019-07") + } + } + + domain_name = var.domain_name + + dynamic "ebs_options" { + for_each = length(var.ebs_options) > 0 ? [var.ebs_options] : [] + + content { + ebs_enabled = try(ebs_options.value.ebs_enabled, true) + iops = try(ebs_options.value.iops, null) + throughput = try(ebs_options.value.throughput, null) + volume_size = try(ebs_options.value.volume_size, null) + volume_type = try(ebs_options.value.volume_type, "gp3") + } + } + + dynamic "encrypt_at_rest" { + for_each = length(var.encrypt_at_rest) > 0 ? [var.encrypt_at_rest] : [] + + content { + enabled = try(encrypt_at_rest.value.enabled, true) + kms_key_id = try(encrypt_at_rest.value.kms_key_id, null) + } + } + + engine_version = var.engine_version + + dynamic "log_publishing_options" { + for_each = { for opt in var.log_publishing_options : opt.log_type => opt } + + content { + cloudwatch_log_group_arn = try(log_publishing_options.value.cloudwatch_log_group_arn, aws_cloudwatch_log_group.this[log_publishing_options.key].arn) + enabled = try(log_publishing_options.value.enabled, true) + log_type = log_publishing_options.value.log_type + } + } + + dynamic "node_to_node_encryption" { + for_each = length(var.node_to_node_encryption) > 0 ? [var.node_to_node_encryption] : [] + + content { + enabled = try(node_to_node_encryption.value.enabled, true) + } + } + + dynamic "vpc_options" { + for_each = length(var.vpc_options) > 0 ? [var.vpc_options] : [] + + content { + security_group_ids = concat(try(vpc_options.value.security_group_ids, []), aws_security_group.this[*].id) + subnet_ids = try(vpc_options.value.subnet_ids, null) + } + } + + timeouts { + create = try(var.timeouts.create, null) + delete = try(var.timeouts.delete, null) + } + + tags = local.tags +} + +################################################################################ +# Access Policy +################################################################################ + +locals { + create_access_policy = var.create && var.create_access_policy && (length(var.access_policy_statements) > 0 || length(var.access_policy_source_policy_documents) > 0 || length(var.access_policy_override_policy_documents) > 0) +} + +resource "aws_opensearch_domain_policy" "this" { + count = var.create && var.enable_access_policy && (local.create_access_policy || var.access_policies != null) ? 1 : 0 + + domain_name = aws_opensearch_domain.this[0].domain_name + access_policies = local.create_access_policy ? data.aws_iam_policy_document.this[0].json : var.access_policies +} + +data "aws_iam_policy_document" "this" { + count = local.create_access_policy ? 1 : 0 + + source_policy_documents = var.access_policy_source_policy_documents + override_policy_documents = var.access_policy_override_policy_documents + + dynamic "statement" { + for_each = var.access_policy_statements + + content { + sid = try(statement.value.sid, null) + actions = try(statement.value.actions, null) + not_actions = try(statement.value.not_actions, null) + effect = try(statement.value.effect, null) + resources = try(statement.value.resources, + [for path in try(statement.value.resource_paths, ["*"]) : "${aws_opensearch_domain.this[0].arn}/${path}"] + ) + not_resources = try(statement.value.not_resources, null) + + dynamic "principals" { + for_each = try(statement.value.principals, []) + + content { + type = principals.value.type + identifiers = principals.value.identifiers + } + } + + dynamic "not_principals" { + for_each = try(statement.value.not_principals, []) + + content { + type = not_principals.value.type + identifiers = not_principals.value.identifiers + } + } + + dynamic "condition" { + for_each = try(statement.value.conditions, []) + + content { + test = condition.value.test + values = condition.value.values + variable = condition.value.variable + } + } + } + } +} + +################################################################################ +# SAML Options +################################################################################ + +resource "aws_opensearch_domain_saml_options" "this" { + count = var.create && var.create_saml_options ? 1 : 0 + + domain_name = aws_opensearch_domain.this[0].domain_name + + dynamic "saml_options" { + for_each = length(var.saml_options) > 0 ? [var.saml_options] : [] + + content { + enabled = try(saml_options.value.enabled, null) + + dynamic "idp" { + for_each = try([saml_options.value.idp], []) + + content { + entity_id = idp.value.entity_id + metadata_content = idp.value.metadata_content + } + } + + master_backend_role = try(saml_options.value.master_backend_role, null) + master_user_name = try(saml_options.value.master_user_name, null) + roles_key = try(saml_options.value.roles_key, null) + session_timeout_minutes = try(saml_options.value.session_timeout_minutes, null) + subject_key = try(saml_options.value.subject_key, null) + } + } +} + +################################################################################ +# Outbound Connections +################################################################################ + +resource "aws_opensearch_outbound_connection" "this" { + for_each = { for k, v in var.outbound_connections : k => v if var.create } + + connection_alias = try(each.value.connection_alias, each.key) + + local_domain_info { + owner_id = try(each.value.local_domain_info.owner_id, local.account_id) + region = try(each.value.local_domain_info.region, local.region) + domain_name = try(each.value.local_domain_info.domain_name, aws_opensearch_domain.this[0].domain_name) + } + + remote_domain_info { + owner_id = each.value.remote_domain_info.owner_id + region = each.value.remote_domain_info.region + domain_name = each.value.remote_domain_info.domain_name + } +} + +################################################################################ +# Cloudwatch Log Group +################################################################################ + +locals { + create_cloudwatch_log_groups = var.create && var.create_cloudwatch_log_groups +} + +resource "aws_cloudwatch_log_group" "this" { + for_each = { for opt in var.log_publishing_options : opt.log_type => opt if try(opt.enabled, true) && local.create_cloudwatch_log_groups } + + name = try(each.value.log_group_name, "/aws/opensearch/${var.domain_name}/${each.key}") + retention_in_days = try(each.value.log_group_retention_in_days, var.cloudwatch_log_group_retention_in_days) + kms_key_id = try(each.value.log_group_kms_key_id, var.cloudwatch_log_group_kms_key_id) + skip_destroy = try(each.value.log_group_skip_destroy, var.cloudwatch_log_group_skip_destroy) + + tags = merge(local.tags, try(each.value.log_group_tags, {})) +} + +data "aws_iam_policy_document" "cloudwatch" { + count = local.create_cloudwatch_log_groups && var.create_cloudwatch_log_resource_policy ? 1 : 0 + + statement { + actions = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:PutLogEventsBatch", + ] + + # https://github.com/hashicorp/terraform-provider-aws/issues/14497 + # resources = coalescelist([for log in aws_cloudwatch_log_group.this : "${log.arn}:*"], ["arn:${local.partition}:logs:*"]) + resources = ["arn:${local.partition}:logs:*"] + + principals { + identifiers = ["es.amazonaws.com"] + type = "Service" + } + + condition { + test = "StringEquals" + variable = "aws:SourceAccount" + values = [local.account_id] + } + + condition { + test = "ArnLike" + variable = "aws:SourceArn" + values = [local.static_domain_arn] + } + } +} + +resource "aws_cloudwatch_log_resource_policy" "this" { + count = local.create_cloudwatch_log_groups && var.create_cloudwatch_log_resource_policy ? 1 : 0 + + policy_document = data.aws_iam_policy_document.cloudwatch[0].json + policy_name = coalesce(var.cloudwatch_log_resource_policy_name, "opensearch-${var.domain_name}") +} + +################################################################################ +# Security Group +################################################################################ + +locals { + create_security_group = var.create && var.create_security_group && length(var.vpc_options) > 0 + security_group_name = try(coalesce(var.security_group_name, var.domain_name), "") +} + +data "aws_subnet" "this" { + count = local.create_security_group ? 1 : 0 + + id = element(var.vpc_options.subnet_ids, 0) +} + +resource "aws_security_group" "this" { + count = local.create_security_group ? 1 : 0 + + name = var.security_group_use_name_prefix ? null : local.security_group_name + name_prefix = var.security_group_use_name_prefix ? "${local.security_group_name}-" : null + description = var.security_group_description + vpc_id = data.aws_subnet.this[0].vpc_id + revoke_rules_on_delete = true + + tags = merge(local.tags, var.security_group_tags) + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_vpc_security_group_ingress_rule" "this" { + for_each = { for k, v in var.security_group_rules : k => v if local.create_security_group && try(v.type, "ingress") == "ingress" } + + # Required + security_group_id = aws_security_group.this[0].id + ip_protocol = try(each.value.ip_protocol, "tcp") + + # Optional + cidr_ipv4 = lookup(each.value, "cidr_ipv4", null) + cidr_ipv6 = lookup(each.value, "cidr_ipv6", null) + description = try(each.value.description, null) + from_port = try(each.value.from_port, 443) + prefix_list_id = lookup(each.value, "prefix_list_id", null) + referenced_security_group_id = lookup(each.value, "referenced_security_group_id", null) + to_port = try(each.value.to_port, 443) + + tags = merge(local.tags, var.security_group_tags, try(each.value.tags, {})) +} + +resource "aws_vpc_security_group_egress_rule" "this" { + for_each = { for k, v in var.security_group_rules : k => v if local.create_security_group && try(v.type, "ingress") == "egress" } + + # Required + security_group_id = aws_security_group.this[0].id + ip_protocol = try(each.value.ip_protocol, "tcp") + + # Optional + cidr_ipv4 = lookup(each.value, "cidr_ipv4", null) + cidr_ipv6 = lookup(each.value, "cidr_ipv6", null) + description = try(each.value.description, null) + from_port = try(each.value.from_port, null) + prefix_list_id = lookup(each.value, "prefix_list_id", null) + referenced_security_group_id = lookup(each.value, "referenced_security_group_id", null) + to_port = try(each.value.to_port, null) + + tags = merge(local.tags, var.security_group_tags, try(each.value.tags, {})) +} diff --git a/modules/opensearch/outputs.tf b/modules/opensearch/outputs.tf new file mode 100644 index 0000000..61c35c5 --- /dev/null +++ b/modules/opensearch/outputs.tf @@ -0,0 +1,55 @@ +################################################################################ +# Domain +################################################################################ + +output "domain_arn" { + description = "The Amazon Resource Name (ARN) of the domain" + value = try(aws_opensearch_domain.this[0].arn, null) +} + +output "domain_id" { + description = "The unique identifier for the domain" + value = try(aws_opensearch_domain.this[0].domain_id, null) +} + +output "domain_endpoint" { + description = "Domain-specific endpoint used to submit index, search, and data upload requests" + value = try(aws_opensearch_domain.this[0].endpoint, null) +} + +output "domain_dashboard_endpoint" { + description = "Domain-specific endpoint for Dashboard without https scheme" + value = try(aws_opensearch_domain.this[0].dashboard_endpoint, null) +} + +################################################################################ +# Outbound Connections +################################################################################ + +output "outbound_connections" { + description = "Map of outbound connections created and their attributes" + value = aws_opensearch_outbound_connection.this +} + +################################################################################ +# CloudWatch Log Groups +################################################################################ + +output "cloudwatch_logs" { + description = "Map of CloudWatch log groups created and their attributes" + value = aws_cloudwatch_log_group.this +} + +################################################################################ +# Security Group +################################################################################ + +output "security_group_arn" { + description = "Amazon Resource Name (ARN) of the security group" + value = try(aws_security_group.this[0].arn, null) +} + +output "security_group_id" { + description = "ID of the security group" + value = try(aws_security_group.this[0].id, null) +} \ No newline at end of file diff --git a/modules/opensearch/variables.tf b/modules/opensearch/variables.tf new file mode 100644 index 0000000..96c3eeb --- /dev/null +++ b/modules/opensearch/variables.tf @@ -0,0 +1,273 @@ +variable "create" { + description = "Determines whether resources will be created (affects all resources)" + type = bool + default = true +} + +variable "tags" { + description = "A map of tags to add to all resources" + type = map(string) + default = {} +} + +################################################################################ +# Domain +################################################################################ + +variable "advanced_options" { + description = "Key-value string pairs to specify advanced configuration options. Note that the values for these configuration options must be strings (wrapped in quotes) or they may be wrong and cause a perpetual diff, causing Terraform to want to recreate your Elasticsearch domain on every apply" + type = map(string) + default = {} +} + +variable "advanced_security_options" { + description = "Configuration block for [fine-grained access control](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/fgac.html)" + type = any + default = { + enabled = true + anonymous_auth_enabled = false + } +} + +variable "auto_tune_options" { + description = "Configuration block for the Auto-Tune options of the domain" + type = any + default = { + desired_state = "ENABLED" + rollback_on_disable = "NO_ROLLBACK" + } +} + +variable "cluster_config" { + description = "Configuration block for the cluster of the domain" + type = any + default = { + dedicated_master_enabled = true + } +} + +variable "cognito_options" { + description = "Configuration block for authenticating Kibana with Cognito" + type = any + default = {} +} + +variable "domain_endpoint_options" { + description = "Configuration block for domain endpoint HTTP(S) related options" + type = any + default = { + enforce_https = true + tls_security_policy = "Policy-Min-TLS-1-2-2019-07" + } +} + +variable "domain_name" { + description = "Name of the domain" + type = string + default = "" +} + +variable "ebs_options" { + description = "Configuration block for EBS related options, may be required based on chosen [instance size](https://aws.amazon.com/elasticsearch-service/pricing/)" + type = any + default = { + ebs_enabled = true + volume_size = 64 + volume_type = "gp3" + } +} + +variable "encrypt_at_rest" { + description = "Configuration block for encrypting at rest" + type = any + default = { + enabled = true + } +} + +variable "engine_version" { + description = "Version of the OpenSearch engine to use" + type = string + default = null +} + +variable "log_publishing_options" { + description = "Configuration block for publishing slow and application logs to CloudWatch Logs. This block can be declared multiple times, for each log_type, within the same resource" + type = any + default = [ + { log_type = "INDEX_SLOW_LOGS" }, + { log_type = "SEARCH_SLOW_LOGS" }, + ] +} + +variable "node_to_node_encryption" { + description = "Configuration block for node-to-node encryption options" + type = any + default = { + enabled = true + } +} + +variable "vpc_options" { + description = "Configuration block for VPC related options. Adding or removing this configuration forces a new resource ([documentation](https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-vpc.html#es-vpc-limitations))" + type = any + default = {} +} + +variable "timeouts" { + description = "Create and delete timeout configurations for the domain" + type = map(string) + default = {} +} + +################################################################################ +# Access Policy +################################################################################ + +variable "enable_access_policy" { + description = "Determines whether an access policy will be applied to the domain" + type = bool + default = true +} + +variable "create_access_policy" { + description = "Determines whether an access policy will be created" + type = bool + default = true +} + +variable "access_policies" { + description = "IAM policy document specifying the access policies for the domain. Required if `create_access_policy` is `false`" + type = string + default = null +} + +variable "access_policy_statements" { + description = "A map of IAM policy [statements](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document#statement) for custom permission usage" + type = any + default = {} +} + +variable "access_policy_source_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. Statements must have unique `sid`s" + type = list(string) + default = [] +} + +variable "access_policy_override_policy_documents" { + description = "List of IAM policy documents that are merged together into the exported document. In merging, statements with non-blank `sid`s will override statements with the same `sid`" + type = list(string) + default = [] +} + +################################################################################ +# SAML Options +################################################################################ + +variable "create_saml_options" { + description = "Determines whether SAML options will be created" + type = bool + default = false +} + +variable "saml_options" { + description = "SAML authentication options for an AWS OpenSearch Domain" + type = any + default = {} +} + +################################################################################ +# Outbound Connections +################################################################################ + +variable "outbound_connections" { + description = "Map of AWS OpenSearch outbound connections to create" + type = any + default = {} +} + +################################################################################ +# CloudWatch Log Group +################################################################################ + +variable "create_cloudwatch_log_groups" { + description = "Determines whether log groups are created" + type = bool + default = true +} + +variable "cloudwatch_log_group_retention_in_days" { + description = "Number of days to retain log events" + type = number + default = 60 +} + +variable "cloudwatch_log_group_kms_key_id" { + description = "If a KMS Key ARN is set, this key will be used to encrypt the corresponding log group. Please be sure that the KMS Key has an appropriate key policy (https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/encrypt-log-data-kms.html)" + type = string + default = null +} + +variable "cloudwatch_log_group_skip_destroy" { + description = "Set to true if you do not wish the log group (and any logs it may contain) to be deleted at destroy time, and instead just remove the log group from the Terraform state" + type = bool + default = null +} + +variable "cloudwatch_log_group_class" { + description = "Specified the log class of the log group. Possible values are: STANDARD or INFREQUENT_ACCESS" + type = string + default = null +} + +variable "create_cloudwatch_log_resource_policy" { + description = "Determines whether a resource policy will be created for OpenSearch to log to CloudWatch" + type = bool + default = true +} + +variable "cloudwatch_log_resource_policy_name" { + description = "Name of the resource policy for OpenSearch to log to CloudWatch" + type = string + default = null +} + +################################################################################ +# Security Group +################################################################################ + +variable "create_security_group" { + description = "Determines if a security group is created" + type = bool + default = true +} + +variable "security_group_name" { + description = "Name to use on security group created" + type = string + default = null +} + +variable "security_group_use_name_prefix" { + description = "Determines whether the security group name (`security_group_name`) is used as a prefix" + type = bool + default = true +} + +variable "security_group_description" { + description = "Description of the security group created" + type = string + default = null +} + +variable "security_group_rules" { + description = "Security group ingress and egress rules to add to the security group created" + type = any + default = {} +} + +variable "security_group_tags" { + description = "A map of additional tags to add to the security group created" + type = map(string) + default = {} +} diff --git a/opensearch-variables.tf b/opensearch-variables.tf new file mode 100644 index 0000000..a6212ca --- /dev/null +++ b/opensearch-variables.tf @@ -0,0 +1,69 @@ +variable "es_application_name" { + type = string + description = "Unique name for the opensearch instance" + default = "" +} + +variable "es_instance_count" { + type = number + description = "Number of instances in the cluster" + default = 1 +} + +variable "es_dedicated_master_type" { + type = string + description = "Instance type of the dedicated main nodes in the cluster." +} + +variable "es_instance_type" { + type = string + description = "Instance type of data nodes in the cluster." + default = "" +} + +variable "es_volume_type" { + type = string + description = "Type of EBS volumes attached to data nodes." + default = "gp3" +} + +variable "es_volume_size" { + type = number + description = "Size of EBS volumes attached to data nodes (in GiB)." + default = 100 +} + +variable "es_ebs_iops" { + type = number + description = "Baseline input/output (I/O) performance of EBS volumes attached to data nodes" + default = 1000 +} + +variable "es_engine_version" { + type = string + description = "Version of Elasticsearch to deploy." +} + +variable "admin_principals" { + description = "Principals allowed to peform admin actions (default: current account)" + type = list(string) + default = null +} + +variable "read_principals" { + description = "Principals allowed to read the secret (default: current account)" + type = list(string) + default = null +} + +variable "elasticsearch_enabled" { + description = "Set to true to enable creation of the Elasticsearch database" + type = bool + default = false +} + +variable "tags" { + type = map(string) + description = "Tags to apply to the instance in AWS" + default = {} +} diff --git a/opensearch.tf b/opensearch.tf new file mode 100644 index 0000000..380f545 --- /dev/null +++ b/opensearch.tf @@ -0,0 +1,161 @@ +data "aws_availability_zones" "available" {} + +locals { + region = data.aws_region.current.name + name = "es-${var.es_application_name}" +} + +resource "aws_iam_service_linked_role" "elasticsearch" { + count = var.elasticsearch_enabled ? 1 : 0 + aws_service_name = "es.amazonaws.com" +} + +################################################################################ +# OpenSearch Module +################################################################################ + +module "opensearch" { + count = var.elasticsearch_enabled ? 1 : 0 + source = "./modules/opensearch" + + # Domain + advanced_options = { + "rest.action.multi.allow_explicit_index" = "true" + } + + advanced_security_options = { + enabled = false + anonymous_auth_enabled = true + internal_user_database_enabled = true + + master_user_options = { + master_user_name = "admin" + master_user_password = random_password.es.result + } + } + + auto_tune_options = { + desired_state = "ENABLED" + + maintenance_schedule = [ + { + start_at = "2028-05-13T07:44:12Z" + cron_expression_for_recurrence = "cron(0 0 ? * 1 *)" + duration = { + value = "2" + unit = "HOURS" + } + } + ] + + rollback_on_disable = "NO_ROLLBACK" + } + + cluster_config = { + instance_count = var.es_instance_count + dedicated_master_enabled = true + dedicated_master_type = var.es_dedicated_master_type + instance_type = coalesce(var.es_instance_type, var.es_dedicated_master_type) + + zone_awareness_config = { + availability_zone_count = 2 + } + + zone_awareness_enabled = true + } + + domain_endpoint_options = { + enforce_https = true + tls_security_policy = "Policy-Min-TLS-1-2-2019-07" + } + + domain_name = local.name + + ebs_options = { + ebs_enabled = true + iops = var.es_ebs_iops + throughput = 125 + volume_type = var.es_volume_type + volume_size = var.es_volume_size + } + + encrypt_at_rest = { + enabled = true + } + + engine_version = var.es_engine_version + + log_publishing_options = [ + { log_type = "INDEX_SLOW_LOGS" }, + { log_type = "SEARCH_SLOW_LOGS" }, + ] + + node_to_node_encryption = { + enabled = true + } + + vpc_options = { + subnet_ids = module.network.private_subnet_ids + } + + # Security Group rule example + security_group_rules = { + ingress_443 = { + type = "ingress" + description = "HTTPS access from VPC" + from_port = 443 + to_port = 443 + ip_protocol = "tcp" + cidr_ipv4 = module.network.vpc.cidr_block + } + } + + # Access policy + access_policy_statements = [ + { + effect = "Allow" + + principals = [{ + type = "*" + identifiers = ["*"] + }] + + actions = ["es:*"] + + condition = [{ + test = "IpAddress" + variable = "aws:SourceIp" + values = ["127.0.0.1/32"] + }] + } + ] + + tags = var.tags + + depends_on = [aws_iam_service_linked_role.elasticsearch] +} + +resource "random_password" "es" { + length = 128 + special = false +} + +module "secret" { + count = var.elasticsearch_enabled ? 1 : 0 + source = "github.com/thoughtbot/terraform-aws-secrets//secret?ref=v0.4.0" + + admin_principals = var.admin_principals + description = "Elastisearch password for: ${local.name}" + name = "${local.name}-secret" + read_principals = var.read_principals + resource_tags = var.tags + + initial_value = jsonencode({ + ES_ENDPOINT = module.opensearch[0].domain_endpoint + ES_DASHBOARD_ENDPOINT = module.opensearch[0].domain_dashboard_endpoint + DOMAIN_ID = module.opensearch[0].domain_id + PASSWORD = random_password.es.result + }) +} + +data "aws_region" "current" {} diff --git a/variables.tf b/variables.tf index 7c73c23..b2ed8f1 100644 --- a/variables.tf +++ b/variables.tf @@ -34,8 +34,8 @@ variable "readwrite_permission_sets" { variable "secret_permission_sets" { description = "AWS IAM permission sets allow to read and manage secrets" - type = list(string) - default = [] + type = list(string) + default = [] } variable "service_account_name" {