TerraformPilot

Terraform

AWS OpenSearch with Terraform - Deploy Elasticsearch Clusters

Deploy AWS OpenSearch Service with Terraform. Domain configuration, VPC access, fine-grained access control, encryption, and index management.

LLuca Berton1 min read

Quick Answer

#
resource "aws_opensearch_domain" "main" {
  domain_name    = "my-search"
  engine_version = "OpenSearch_2.11"
 
  cluster_config {
    instance_type  = "t3.small.search"
    instance_count = 1
  }
 
  ebs_options {
    ebs_enabled = true
    volume_size = 20
    volume_type = "gp3"
  }
}

Production Domain

#
resource "aws_opensearch_domain" "main" {
  domain_name    = "${var.project}-search"
  engine_version = "OpenSearch_2.11"
 
  cluster_config {
    instance_type          = "r6g.large.search"
    instance_count         = 3
    zone_awareness_enabled = true
 
    zone_awareness_config {
      availability_zone_count = 3
    }
 
    dedicated_master_enabled = true
    dedicated_master_type    = "m6g.large.search"
    dedicated_master_count   = 3
  }
 
  ebs_options {
    ebs_enabled = true
    volume_size = 100
    volume_type = "gp3"
    iops        = 3000
    throughput  = 125
  }
 
  vpc_options {
    subnet_ids         = aws_subnet.private[*].id
    security_group_ids = [aws_security_group.opensearch.id]
  }
 
  encrypt_at_rest {
    enabled    = true
    kms_key_id = aws_kms_key.opensearch.arn
  }
 
  node_to_node_encryption {
    enabled = true
  }
 
  domain_endpoint_options {
    enforce_https       = true
    tls_security_policy = "Policy-Min-TLS-1-2-PFS-2023-10"
  }
 
  advanced_security_options {
    enabled                        = true
    internal_user_database_enabled = true
    master_user_options {
      master_user_name     = var.opensearch_master_user
      master_user_password = var.opensearch_master_password
    }
  }
 
  auto_tune_options {
    desired_state       = "ENABLED"
    rollback_on_disable = "NO_ROLLBACK"
  }
 
  log_publishing_options {
    cloudwatch_log_group_arn = aws_cloudwatch_log_group.opensearch.arn
    log_type                 = "INDEX_SLOW_LOGS"
  }
 
  log_publishing_options {
    cloudwatch_log_group_arn = aws_cloudwatch_log_group.opensearch.arn
    log_type                 = "SEARCH_SLOW_LOGS"
  }
 
  tags = { Environment = var.environment }
 
  timeouts {
    create = "60m"
    update = "60m"
    delete = "30m"
  }
}

Security Group

#
resource "aws_security_group" "opensearch" {
  name   = "${var.project}-opensearch"
  vpc_id = aws_vpc.main.id
 
  ingress {
    from_port       = 443
    to_port         = 443
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }
 
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Access Policy

#
resource "aws_opensearch_domain_policy" "main" {
  domain_name = aws_opensearch_domain.main.domain_name
 
  access_policies = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { AWS = aws_iam_role.app.arn }
      Action    = "es:*"
      Resource  = "${aws_opensearch_domain.main.arn}/*"
    }]
  })
}

Serverless (Pay-per-Use)

#
resource "aws_opensearchserverless_collection" "logs" {
  name = "${var.project}-logs"
  type = "SEARCH"
}
 
resource "aws_opensearchserverless_security_policy" "encryption" {
  name = "${var.project}-encryption"
  type = "encryption"
  policy = jsonencode({
    Rules = [{
      ResourceType = "collection"
      Resource     = ["collection/${var.project}-logs"]
    }]
    AWSOwnedKey = true
  })
}
 
resource "aws_opensearchserverless_security_policy" "network" {
  name = "${var.project}-network"
  type = "network"
  policy = jsonencode([{
    Rules = [{
      ResourceType = "collection"
      Resource     = ["collection/${var.project}-logs"]
    }]
    AllowFromPublic = false
    SourceVPCEs     = [aws_opensearchserverless_vpc_endpoint.main.id]
  }])
}

Instance Types

#
TypevCPURAMUse Case
t3.small.search22 GBDev/test
m6g.large.search28 GBGeneral purpose
r6g.large.search216 GBMemory-intensive
r6g.xlarge.search432 GBLarge datasets

Outputs

#
output "opensearch_endpoint" {
  value = aws_opensearch_domain.main.endpoint
}
 
output "opensearch_dashboard" {
  value = "${aws_opensearch_domain.main.endpoint}/_dashboards"
}
#

Conclusion

#

Use 3 data nodes across 3 AZs with dedicated master nodes for production. Enable fine-grained access control, encryption at rest and in transit, and VPC access. OpenSearch domains take 15-45 minutes to create/modify — set generous timeouts. Use Serverless for variable workloads to avoid capacity planning.

#Terraform#AWS#OpenSearch#Elasticsearch#Infrastructure as Code

Share this article