Create AWS Elasticsearch Domains

The aws:elasticsearch/domain:Domain resource, part of the Pulumi AWS provider, provisions an Elasticsearch domain: the cluster configuration, access policies, and optional VPC placement. This guide focuses on four capabilities: instance type and version selection, IP-based access policies, CloudWatch Logs integration, and VPC deployment for private access.

Elasticsearch domains can run with public endpoints or inside VPCs. VPC deployments require existing subnets and security groups; log publishing requires CloudWatch log groups and IAM policies. The examples are intentionally small. Combine them with your own networking, encryption, and authentication configuration.

Create a domain with instance type and version

Most deployments start by specifying a domain name, Elasticsearch version, and instance type that matches your workload’s memory and compute needs.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const example = new aws.elasticsearch.Domain("example", {
    domainName: "example",
    elasticsearchVersion: "7.10",
    clusterConfig: {
        instanceType: "r4.large.elasticsearch",
    },
    tags: {
        Domain: "TestDomain",
    },
});
import pulumi
import pulumi_aws as aws

example = aws.elasticsearch.Domain("example",
    domain_name="example",
    elasticsearch_version="7.10",
    cluster_config={
        "instance_type": "r4.large.elasticsearch",
    },
    tags={
        "Domain": "TestDomain",
    })
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/elasticsearch"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := elasticsearch.NewDomain(ctx, "example", &elasticsearch.DomainArgs{
			DomainName:           pulumi.String("example"),
			ElasticsearchVersion: pulumi.String("7.10"),
			ClusterConfig: &elasticsearch.DomainClusterConfigArgs{
				InstanceType: pulumi.String("r4.large.elasticsearch"),
			},
			Tags: pulumi.StringMap{
				"Domain": pulumi.String("TestDomain"),
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var example = new Aws.ElasticSearch.Domain("example", new()
    {
        DomainName = "example",
        ElasticsearchVersion = "7.10",
        ClusterConfig = new Aws.ElasticSearch.Inputs.DomainClusterConfigArgs
        {
            InstanceType = "r4.large.elasticsearch",
        },
        Tags = 
        {
            { "Domain", "TestDomain" },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.elasticsearch.Domain;
import com.pulumi.aws.elasticsearch.DomainArgs;
import com.pulumi.aws.elasticsearch.inputs.DomainClusterConfigArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var example = new Domain("example", DomainArgs.builder()
            .domainName("example")
            .elasticsearchVersion("7.10")
            .clusterConfig(DomainClusterConfigArgs.builder()
                .instanceType("r4.large.elasticsearch")
                .build())
            .tags(Map.of("Domain", "TestDomain"))
            .build());

    }
}
resources:
  example:
    type: aws:elasticsearch:Domain
    properties:
      domainName: example
      elasticsearchVersion: '7.10'
      clusterConfig:
        instanceType: r4.large.elasticsearch
      tags:
        Domain: TestDomain

The domainName property sets a unique identifier for the cluster. The elasticsearchVersion determines which Elasticsearch features are available. The clusterConfig block specifies the instanceType, which controls memory, CPU, and storage capacity per node.

Restrict access by source IP address

Production domains typically limit access to specific IP ranges rather than allowing public access.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const domain = config.get("domain") || "tf-test";
const current = aws.getRegion({});
const currentGetCallerIdentity = aws.getCallerIdentity({});
const example = new aws.elasticsearch.Domain("example", {
    domainName: domain,
    accessPolicies: Promise.all([current, currentGetCallerIdentity]).then(([current, currentGetCallerIdentity]) => `{
  \"Version\": \"2012-10-17\",
  \"Statement\": [
    {
      \"Action\": \"es:*\",
      \"Principal\": \"*\",
      \"Effect\": \"Allow\",
      \"Resource\": \"arn:aws:es:${current.region}:${currentGetCallerIdentity.accountId}:domain/${domain}/*\",
      \"Condition\": {
        \"IpAddress\": {\"aws:SourceIp\": [\"66.193.100.22/32\"]}
      }
    }
  ]
}
`),
});
import pulumi
import pulumi_aws as aws

config = pulumi.Config()
domain = config.get("domain")
if domain is None:
    domain = "tf-test"
current = aws.get_region()
current_get_caller_identity = aws.get_caller_identity()
example = aws.elasticsearch.Domain("example",
    domain_name=domain,
    access_policies=f"""{{
  \"Version\": \"2012-10-17\",
  \"Statement\": [
    {{
      \"Action\": \"es:*\",
      \"Principal\": \"*\",
      \"Effect\": \"Allow\",
      \"Resource\": \"arn:aws:es:{current.region}:{current_get_caller_identity.account_id}:domain/{domain}/*\",
      \"Condition\": {{
        \"IpAddress\": {{\"aws:SourceIp\": [\"66.193.100.22/32\"]}}
      }}
    }}
  ]
}}
""")
package main

import (
	"fmt"

	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/elasticsearch"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		cfg := config.New(ctx, "")
		domain := "tf-test"
		if param := cfg.Get("domain"); param != "" {
			domain = param
		}
		current, err := aws.GetRegion(ctx, &aws.GetRegionArgs{}, nil)
		if err != nil {
			return err
		}
		currentGetCallerIdentity, err := aws.GetCallerIdentity(ctx, &aws.GetCallerIdentityArgs{}, nil)
		if err != nil {
			return err
		}
		_, err = elasticsearch.NewDomain(ctx, "example", &elasticsearch.DomainArgs{
			DomainName: pulumi.String(domain),
			AccessPolicies: pulumi.Any(fmt.Sprintf(`{
  \"Version\": \"2012-10-17\",
  \"Statement\": [
    {
      \"Action\": \"es:*\",
      \"Principal\": \"*\",
      \"Effect\": \"Allow\",
      \"Resource\": \"arn:aws:es:%v:%v:domain/%v/*\",
      \"Condition\": {
        \"IpAddress\": {\"aws:SourceIp\": [\"66.193.100.22/32\"]}
      }
    }
  ]
}
`, current.Region, currentGetCallerIdentity.AccountId, domain)),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var config = new Config();
    var domain = config.Get("domain") ?? "tf-test";
    var current = Aws.GetRegion.Invoke();

    var currentGetCallerIdentity = Aws.GetCallerIdentity.Invoke();

    var example = new Aws.ElasticSearch.Domain("example", new()
    {
        DomainName = domain,
        AccessPolicies = Output.Tuple(current, currentGetCallerIdentity).Apply(values =>
        {
            var current = values.Item1;
            var currentGetCallerIdentity = values.Item2;
            return @$"{{
  \""Version\"": \""2012-10-17\"",
  \""Statement\"": [
    {{
      \""Action\"": \""es:*\"",
      \""Principal\"": \""*\"",
      \""Effect\"": \""Allow\"",
      \""Resource\"": \""arn:aws:es:{current.Apply(getRegionResult => getRegionResult.Region)}:{currentGetCallerIdentity.Apply(getCallerIdentityResult => getCallerIdentityResult.AccountId)}:domain/{domain}/*\"",
      \""Condition\"": {{
        \""IpAddress\"": {{\""aws:SourceIp\"": [\""66.193.100.22/32\""]}}
      }}
    }}
  ]
}}
";
        }),
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.AwsFunctions;
import com.pulumi.aws.inputs.GetRegionArgs;
import com.pulumi.aws.inputs.GetCallerIdentityArgs;
import com.pulumi.aws.elasticsearch.Domain;
import com.pulumi.aws.elasticsearch.DomainArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        final var config = ctx.config();
        final var domain = config.get("domain").orElse("tf-test");
        final var current = AwsFunctions.getRegion(GetRegionArgs.builder()
            .build());

        final var currentGetCallerIdentity = AwsFunctions.getCallerIdentity(GetCallerIdentityArgs.builder()
            .build());

        var example = new Domain("example", DomainArgs.builder()
            .domainName(domain)
            .accessPolicies("""
{
  \"Version\": \"2012-10-17\",
  \"Statement\": [
    {
      \"Action\": \"es:*\",
      \"Principal\": \"*\",
      \"Effect\": \"Allow\",
      \"Resource\": \"arn:aws:es:%s:%s:domain/%s/*\",
      \"Condition\": {
        \"IpAddress\": {\"aws:SourceIp\": [\"66.193.100.22/32\"]}
      }
    }
  ]
}
", current.region(),currentGetCallerIdentity.accountId(),domain))
            .build());

    }
}
configuration:
  domain:
    type: string
    default: tf-test
resources:
  example:
    type: aws:elasticsearch:Domain
    properties:
      domainName: ${domain}
      accessPolicies: |
        {
          \"Version\": \"2012-10-17\",
          \"Statement\": [
            {
              \"Action\": \"es:*\",
              \"Principal\": \"*\",
              \"Effect\": \"Allow\",
              \"Resource\": \"arn:aws:es:${current.region}:${currentGetCallerIdentity.accountId}:domain/${domain}/*\",
              \"Condition\": {
                \"IpAddress\": {\"aws:SourceIp\": [\"66.193.100.22/32\"]}
              }
            }
          ]
        }        
variables:
  current:
    fn::invoke:
      function: aws:getRegion
      arguments: {}
  currentGetCallerIdentity:
    fn::invoke:
      function: aws:getCallerIdentity
      arguments: {}

The accessPolicies property accepts an IAM policy document that controls who can access the domain. This example uses a Condition block with aws:SourceIp to restrict access to a single IP address. The policy grants es:* permissions, allowing all Elasticsearch operations from the specified source.

Send slow query logs to CloudWatch

Troubleshooting search performance often requires analyzing slow queries, which Elasticsearch can publish to CloudWatch.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const exampleLogGroup = new aws.cloudwatch.LogGroup("example", {name: "example"});
const example = aws.iam.getPolicyDocument({
    statements: [{
        effect: "Allow",
        principals: [{
            type: "Service",
            identifiers: ["es.amazonaws.com"],
        }],
        actions: [
            "logs:PutLogEvents",
            "logs:PutLogEventsBatch",
            "logs:CreateLogStream",
        ],
        resources: ["arn:aws:logs:*"],
    }],
});
const exampleLogResourcePolicy = new aws.cloudwatch.LogResourcePolicy("example", {
    policyName: "example",
    policyDocument: example.then(example => example.json),
});
const exampleDomain = new aws.elasticsearch.Domain("example", {logPublishingOptions: [{
    cloudwatchLogGroupArn: exampleLogGroup.arn,
    logType: "INDEX_SLOW_LOGS",
}]});
import pulumi
import pulumi_aws as aws

example_log_group = aws.cloudwatch.LogGroup("example", name="example")
example = aws.iam.get_policy_document(statements=[{
    "effect": "Allow",
    "principals": [{
        "type": "Service",
        "identifiers": ["es.amazonaws.com"],
    }],
    "actions": [
        "logs:PutLogEvents",
        "logs:PutLogEventsBatch",
        "logs:CreateLogStream",
    ],
    "resources": ["arn:aws:logs:*"],
}])
example_log_resource_policy = aws.cloudwatch.LogResourcePolicy("example",
    policy_name="example",
    policy_document=example.json)
example_domain = aws.elasticsearch.Domain("example", log_publishing_options=[{
    "cloudwatch_log_group_arn": example_log_group.arn,
    "log_type": "INDEX_SLOW_LOGS",
}])
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudwatch"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/elasticsearch"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		exampleLogGroup, err := cloudwatch.NewLogGroup(ctx, "example", &cloudwatch.LogGroupArgs{
			Name: pulumi.String("example"),
		})
		if err != nil {
			return err
		}
		example, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
			Statements: []iam.GetPolicyDocumentStatement{
				{
					Effect: pulumi.StringRef("Allow"),
					Principals: []iam.GetPolicyDocumentStatementPrincipal{
						{
							Type: "Service",
							Identifiers: []string{
								"es.amazonaws.com",
							},
						},
					},
					Actions: []string{
						"logs:PutLogEvents",
						"logs:PutLogEventsBatch",
						"logs:CreateLogStream",
					},
					Resources: []string{
						"arn:aws:logs:*",
					},
				},
			},
		}, nil)
		if err != nil {
			return err
		}
		_, err = cloudwatch.NewLogResourcePolicy(ctx, "example", &cloudwatch.LogResourcePolicyArgs{
			PolicyName:     pulumi.String("example"),
			PolicyDocument: pulumi.String(example.Json),
		})
		if err != nil {
			return err
		}
		_, err = elasticsearch.NewDomain(ctx, "example", &elasticsearch.DomainArgs{
			LogPublishingOptions: elasticsearch.DomainLogPublishingOptionArray{
				&elasticsearch.DomainLogPublishingOptionArgs{
					CloudwatchLogGroupArn: exampleLogGroup.Arn,
					LogType:               pulumi.String("INDEX_SLOW_LOGS"),
				},
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var exampleLogGroup = new Aws.CloudWatch.LogGroup("example", new()
    {
        Name = "example",
    });

    var example = Aws.Iam.GetPolicyDocument.Invoke(new()
    {
        Statements = new[]
        {
            new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs
            {
                Effect = "Allow",
                Principals = new[]
                {
                    new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalInputArgs
                    {
                        Type = "Service",
                        Identifiers = new[]
                        {
                            "es.amazonaws.com",
                        },
                    },
                },
                Actions = new[]
                {
                    "logs:PutLogEvents",
                    "logs:PutLogEventsBatch",
                    "logs:CreateLogStream",
                },
                Resources = new[]
                {
                    "arn:aws:logs:*",
                },
            },
        },
    });

    var exampleLogResourcePolicy = new Aws.CloudWatch.LogResourcePolicy("example", new()
    {
        PolicyName = "example",
        PolicyDocument = example.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json),
    });

    var exampleDomain = new Aws.ElasticSearch.Domain("example", new()
    {
        LogPublishingOptions = new[]
        {
            new Aws.ElasticSearch.Inputs.DomainLogPublishingOptionArgs
            {
                CloudwatchLogGroupArn = exampleLogGroup.Arn,
                LogType = "INDEX_SLOW_LOGS",
            },
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.cloudwatch.LogGroup;
import com.pulumi.aws.cloudwatch.LogGroupArgs;
import com.pulumi.aws.iam.IamFunctions;
import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs;
import com.pulumi.aws.cloudwatch.LogResourcePolicy;
import com.pulumi.aws.cloudwatch.LogResourcePolicyArgs;
import com.pulumi.aws.elasticsearch.Domain;
import com.pulumi.aws.elasticsearch.DomainArgs;
import com.pulumi.aws.elasticsearch.inputs.DomainLogPublishingOptionArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        var exampleLogGroup = new LogGroup("exampleLogGroup", LogGroupArgs.builder()
            .name("example")
            .build());

        final var example = IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder()
            .statements(GetPolicyDocumentStatementArgs.builder()
                .effect("Allow")
                .principals(GetPolicyDocumentStatementPrincipalArgs.builder()
                    .type("Service")
                    .identifiers("es.amazonaws.com")
                    .build())
                .actions(                
                    "logs:PutLogEvents",
                    "logs:PutLogEventsBatch",
                    "logs:CreateLogStream")
                .resources("arn:aws:logs:*")
                .build())
            .build());

        var exampleLogResourcePolicy = new LogResourcePolicy("exampleLogResourcePolicy", LogResourcePolicyArgs.builder()
            .policyName("example")
            .policyDocument(example.json())
            .build());

        var exampleDomain = new Domain("exampleDomain", DomainArgs.builder()
            .logPublishingOptions(DomainLogPublishingOptionArgs.builder()
                .cloudwatchLogGroupArn(exampleLogGroup.arn())
                .logType("INDEX_SLOW_LOGS")
                .build())
            .build());

    }
}
resources:
  exampleLogGroup:
    type: aws:cloudwatch:LogGroup
    name: example
    properties:
      name: example
  exampleLogResourcePolicy:
    type: aws:cloudwatch:LogResourcePolicy
    name: example
    properties:
      policyName: example
      policyDocument: ${example.json}
  exampleDomain:
    type: aws:elasticsearch:Domain
    name: example
    properties:
      logPublishingOptions:
        - cloudwatchLogGroupArn: ${exampleLogGroup.arn}
          logType: INDEX_SLOW_LOGS
variables:
  example:
    fn::invoke:
      function: aws:iam:getPolicyDocument
      arguments:
        statements:
          - effect: Allow
            principals:
              - type: Service
                identifiers:
                  - es.amazonaws.com
            actions:
              - logs:PutLogEvents
              - logs:PutLogEventsBatch
              - logs:CreateLogStream
            resources:
              - arn:aws:logs:*

The logPublishingOptions array configures log streams. Each entry specifies a logType (INDEX_SLOW_LOGS, SEARCH_SLOW_LOGS, ES_APPLICATION_LOGS, or AUDIT_LOGS) and a cloudwatchLogGroupArn. The example creates a CloudWatch log group and IAM policy that grants Elasticsearch permission to write logs.

Deploy into a VPC for private access

Applications in private subnets need Elasticsearch domains deployed into the same VPC to avoid exposing search traffic to the internet.

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const vpc = config.requireObject<any>("vpc");
const domain = config.get("domain") || "tf-test";
const selected = aws.ec2.getVpc({
    tags: {
        Name: vpc,
    },
});
const selectedGetSubnets = selected.then(selected => aws.ec2.getSubnets({
    filters: [{
        name: "vpc-id",
        values: [selected.id],
    }],
    tags: {
        Tier: "private",
    },
}));
const current = aws.getRegion({});
const currentGetCallerIdentity = aws.getCallerIdentity({});
const es = new aws.ec2.SecurityGroup("es", {
    name: `${vpc}-elasticsearch-${domain}`,
    description: "Managed by Pulumi",
    vpcId: selected.then(selected => selected.id),
    ingress: [{
        fromPort: 443,
        toPort: 443,
        protocol: "tcp",
        cidrBlocks: [selected.then(selected => selected.cidrBlock)],
    }],
});
const esServiceLinkedRole = new aws.iam.ServiceLinkedRole("es", {awsServiceName: "opensearchservice.amazonaws.com"});
const esDomain = new aws.elasticsearch.Domain("es", {
    domainName: domain,
    elasticsearchVersion: "6.3",
    clusterConfig: {
        instanceType: "m4.large.elasticsearch",
        zoneAwarenessEnabled: true,
    },
    vpcOptions: {
        subnetIds: [
            selectedGetSubnets.then(selectedGetSubnets => selectedGetSubnets.ids?.[0]),
            selectedGetSubnets.then(selectedGetSubnets => selectedGetSubnets.ids?.[1]),
        ],
        securityGroupIds: [es.id],
    },
    advancedOptions: {
        "rest.action.multi.allow_explicit_index": "true",
    },
    accessPolicies: Promise.all([current, currentGetCallerIdentity]).then(([current, currentGetCallerIdentity]) => `{
\t\"Version\": \"2012-10-17\",
\t\"Statement\": [
\t\t{
\t\t\t\"Action\": \"es:*\",
\t\t\t\"Principal\": \"*\",
\t\t\t\"Effect\": \"Allow\",
\t\t\t\"Resource\": \"arn:aws:es:${current.region}:${currentGetCallerIdentity.accountId}:domain/${domain}/*\"
\t\t}
\t]
}
`),
    tags: {
        Domain: "TestDomain",
    },
}, {
    dependsOn: [esServiceLinkedRole],
});
import pulumi
import pulumi_aws as aws

config = pulumi.Config()
vpc = config.require_object("vpc")
domain = config.get("domain")
if domain is None:
    domain = "tf-test"
selected = aws.ec2.get_vpc(tags={
    "Name": vpc,
})
selected_get_subnets = aws.ec2.get_subnets(filters=[{
        "name": "vpc-id",
        "values": [selected.id],
    }],
    tags={
        "Tier": "private",
    })
current = aws.get_region()
current_get_caller_identity = aws.get_caller_identity()
es = aws.ec2.SecurityGroup("es",
    name=f"{vpc}-elasticsearch-{domain}",
    description="Managed by Pulumi",
    vpc_id=selected.id,
    ingress=[{
        "from_port": 443,
        "to_port": 443,
        "protocol": "tcp",
        "cidr_blocks": [selected.cidr_block],
    }])
es_service_linked_role = aws.iam.ServiceLinkedRole("es", aws_service_name="opensearchservice.amazonaws.com")
es_domain = aws.elasticsearch.Domain("es",
    domain_name=domain,
    elasticsearch_version="6.3",
    cluster_config={
        "instance_type": "m4.large.elasticsearch",
        "zone_awareness_enabled": True,
    },
    vpc_options={
        "subnet_ids": [
            selected_get_subnets.ids[0],
            selected_get_subnets.ids[1],
        ],
        "security_group_ids": [es.id],
    },
    advanced_options={
        "rest.action.multi.allow_explicit_index": "true",
    },
    access_policies=f"""{{
\t\"Version\": \"2012-10-17\",
\t\"Statement\": [
\t\t{{
\t\t\t\"Action\": \"es:*\",
\t\t\t\"Principal\": \"*\",
\t\t\t\"Effect\": \"Allow\",
\t\t\t\"Resource\": \"arn:aws:es:{current.region}:{current_get_caller_identity.account_id}:domain/{domain}/*\"
\t\t}}
\t]
}}
""",
    tags={
        "Domain": "TestDomain",
    },
    opts = pulumi.ResourceOptions(depends_on=[es_service_linked_role]))
package main

import (
	"fmt"

	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ec2"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/elasticsearch"
	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
cfg := config.New(ctx, "")
vpc := cfg.RequireObject("vpc")
domain := "tf-test";
if param := cfg.Get("domain"); param != ""{
domain = param
}
selected, err := ec2.LookupVpc(ctx, &ec2.LookupVpcArgs{
Tags: pulumi.StringMap{
"Name": vpc,
},
}, nil);
if err != nil {
return err
}
selectedGetSubnets, err := ec2.GetSubnets(ctx, &ec2.GetSubnetsArgs{
Filters: []ec2.GetSubnetsFilter{
{
Name: "vpc-id",
Values: interface{}{
selected.Id,
},
},
},
Tags: map[string]interface{}{
"Tier": "private",
},
}, nil);
if err != nil {
return err
}
current, err := aws.GetRegion(ctx, &aws.GetRegionArgs{
}, nil);
if err != nil {
return err
}
currentGetCallerIdentity, err := aws.GetCallerIdentity(ctx, &aws.GetCallerIdentityArgs{
}, nil);
if err != nil {
return err
}
es, err := ec2.NewSecurityGroup(ctx, "es", &ec2.SecurityGroupArgs{
Name: pulumi.Sprintf("%v-elasticsearch-%v", vpc, domain),
Description: pulumi.String("Managed by Pulumi"),
VpcId: pulumi.String(selected.Id),
Ingress: ec2.SecurityGroupIngressArray{
&ec2.SecurityGroupIngressArgs{
FromPort: pulumi.Int(443),
ToPort: pulumi.Int(443),
Protocol: pulumi.String("tcp"),
CidrBlocks: pulumi.StringArray{
pulumi.String(selected.CidrBlock),
},
},
},
})
if err != nil {
return err
}
esServiceLinkedRole, err := iam.NewServiceLinkedRole(ctx, "es", &iam.ServiceLinkedRoleArgs{
AwsServiceName: pulumi.String("opensearchservice.amazonaws.com"),
})
if err != nil {
return err
}
_, err = elasticsearch.NewDomain(ctx, "es", &elasticsearch.DomainArgs{
DomainName: pulumi.String(domain),
ElasticsearchVersion: pulumi.String("6.3"),
ClusterConfig: &elasticsearch.DomainClusterConfigArgs{
InstanceType: pulumi.String("m4.large.elasticsearch"),
ZoneAwarenessEnabled: pulumi.Bool(true),
},
VpcOptions: &elasticsearch.DomainVpcOptionsArgs{
SubnetIds: pulumi.StringArray{
pulumi.String(selectedGetSubnets.Ids[0]),
pulumi.String(selectedGetSubnets.Ids[1]),
},
SecurityGroupIds: pulumi.StringArray{
es.ID(),
},
},
AdvancedOptions: pulumi.StringMap{
"rest.action.multi.allow_explicit_index": pulumi.String("true"),
},
AccessPolicies: pulumi.Any(fmt.Sprintf(`{
\t\"Version\": \"2012-10-17\",
\t\"Statement\": [
\t\t{
\t\t\t\"Action\": \"es:*\",
\t\t\t\"Principal\": \"*\",
\t\t\t\"Effect\": \"Allow\",
\t\t\t\"Resource\": \"arn:aws:es:%v:%v:domain/%v/*\"
\t\t}
\t]
}
`, current.Region, currentGetCallerIdentity.AccountId, domain)),
Tags: pulumi.StringMap{
"Domain": pulumi.String("TestDomain"),
},
}, pulumi.DependsOn([]pulumi.Resource{
esServiceLinkedRole,
}))
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;

return await Deployment.RunAsync(() => 
{
    var config = new Config();
    var vpc = config.RequireObject<dynamic>("vpc");
    var domain = config.Get("domain") ?? "tf-test";
    var selected = Aws.Ec2.GetVpc.Invoke(new()
    {
        Tags = 
        {
            { "Name", vpc },
        },
    });

    var selectedGetSubnets = Aws.Ec2.GetSubnets.Invoke(new()
    {
        Filters = new[]
        {
            new Aws.Ec2.Inputs.GetSubnetsFilterInputArgs
            {
                Name = "vpc-id",
                Values = new[]
                {
                    selected.Apply(getVpcResult => getVpcResult.Id),
                },
            },
        },
        Tags = 
        {
            { "Tier", "private" },
        },
    });

    var current = Aws.GetRegion.Invoke();

    var currentGetCallerIdentity = Aws.GetCallerIdentity.Invoke();

    var es = new Aws.Ec2.SecurityGroup("es", new()
    {
        Name = $"{vpc}-elasticsearch-{domain}",
        Description = "Managed by Pulumi",
        VpcId = selected.Apply(getVpcResult => getVpcResult.Id),
        Ingress = new[]
        {
            new Aws.Ec2.Inputs.SecurityGroupIngressArgs
            {
                FromPort = 443,
                ToPort = 443,
                Protocol = "tcp",
                CidrBlocks = new[]
                {
                    selected.Apply(getVpcResult => getVpcResult.CidrBlock),
                },
            },
        },
    });

    var esServiceLinkedRole = new Aws.Iam.ServiceLinkedRole("es", new()
    {
        AwsServiceName = "opensearchservice.amazonaws.com",
    });

    var esDomain = new Aws.ElasticSearch.Domain("es", new()
    {
        DomainName = domain,
        ElasticsearchVersion = "6.3",
        ClusterConfig = new Aws.ElasticSearch.Inputs.DomainClusterConfigArgs
        {
            InstanceType = "m4.large.elasticsearch",
            ZoneAwarenessEnabled = true,
        },
        VpcOptions = new Aws.ElasticSearch.Inputs.DomainVpcOptionsArgs
        {
            SubnetIds = new[]
            {
                selectedGetSubnets.Apply(getSubnetsResult => getSubnetsResult.Ids[0]),
                selectedGetSubnets.Apply(getSubnetsResult => getSubnetsResult.Ids[1]),
            },
            SecurityGroupIds = new[]
            {
                es.Id,
            },
        },
        AdvancedOptions = 
        {
            { "rest.action.multi.allow_explicit_index", "true" },
        },
        AccessPolicies = Output.Tuple(current, currentGetCallerIdentity).Apply(values =>
        {
            var current = values.Item1;
            var currentGetCallerIdentity = values.Item2;
            return @$"{{
\t\""Version\"": \""2012-10-17\"",
\t\""Statement\"": [
\t\t{{
\t\t\t\""Action\"": \""es:*\"",
\t\t\t\""Principal\"": \""*\"",
\t\t\t\""Effect\"": \""Allow\"",
\t\t\t\""Resource\"": \""arn:aws:es:{current.Apply(getRegionResult => getRegionResult.Region)}:{currentGetCallerIdentity.Apply(getCallerIdentityResult => getCallerIdentityResult.AccountId)}:domain/{domain}/*\""
\t\t}}
\t]
}}
";
        }),
        Tags = 
        {
            { "Domain", "TestDomain" },
        },
    }, new CustomResourceOptions
    {
        DependsOn =
        {
            esServiceLinkedRole,
        },
    });

});
package generated_program;

import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ec2.Ec2Functions;
import com.pulumi.aws.ec2.inputs.GetVpcArgs;
import com.pulumi.aws.ec2.inputs.GetSubnetsArgs;
import com.pulumi.aws.AwsFunctions;
import com.pulumi.aws.inputs.GetRegionArgs;
import com.pulumi.aws.inputs.GetCallerIdentityArgs;
import com.pulumi.aws.ec2.SecurityGroup;
import com.pulumi.aws.ec2.SecurityGroupArgs;
import com.pulumi.aws.ec2.inputs.SecurityGroupIngressArgs;
import com.pulumi.aws.iam.ServiceLinkedRole;
import com.pulumi.aws.iam.ServiceLinkedRoleArgs;
import com.pulumi.aws.elasticsearch.Domain;
import com.pulumi.aws.elasticsearch.DomainArgs;
import com.pulumi.aws.elasticsearch.inputs.DomainClusterConfigArgs;
import com.pulumi.aws.elasticsearch.inputs.DomainVpcOptionsArgs;
import com.pulumi.resources.CustomResourceOptions;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;

public class App {
    public static void main(String[] args) {
        Pulumi.run(App::stack);
    }

    public static void stack(Context ctx) {
        final var config = ctx.config();
        final var vpc = config.get("vpc");
        final var domain = config.get("domain").orElse("tf-test");
        final var selected = Ec2Functions.getVpc(GetVpcArgs.builder()
            .tags(Map.of("Name", vpc))
            .build());

        final var selectedGetSubnets = Ec2Functions.getSubnets(GetSubnetsArgs.builder()
            .filters(GetSubnetsFilterArgs.builder()
                .name("vpc-id")
                .values(selected.id())
                .build())
            .tags(Map.of("Tier", "private"))
            .build());

        final var current = AwsFunctions.getRegion(GetRegionArgs.builder()
            .build());

        final var currentGetCallerIdentity = AwsFunctions.getCallerIdentity(GetCallerIdentityArgs.builder()
            .build());

        var es = new SecurityGroup("es", SecurityGroupArgs.builder()
            .name(String.format("%s-elasticsearch-%s", vpc,domain))
            .description("Managed by Pulumi")
            .vpcId(selected.id())
            .ingress(SecurityGroupIngressArgs.builder()
                .fromPort(443)
                .toPort(443)
                .protocol("tcp")
                .cidrBlocks(selected.cidrBlock())
                .build())
            .build());

        var esServiceLinkedRole = new ServiceLinkedRole("esServiceLinkedRole", ServiceLinkedRoleArgs.builder()
            .awsServiceName("opensearchservice.amazonaws.com")
            .build());

        var esDomain = new Domain("esDomain", DomainArgs.builder()
            .domainName(domain)
            .elasticsearchVersion("6.3")
            .clusterConfig(DomainClusterConfigArgs.builder()
                .instanceType("m4.large.elasticsearch")
                .zoneAwarenessEnabled(true)
                .build())
            .vpcOptions(DomainVpcOptionsArgs.builder()
                .subnetIds(                
                    selectedGetSubnets.ids()[0],
                    selectedGetSubnets.ids()[1])
                .securityGroupIds(es.id())
                .build())
            .advancedOptions(Map.of("rest.action.multi.allow_explicit_index", "true"))
            .accessPolicies("""
{
\t\"Version\": \"2012-10-17\",
\t\"Statement\": [
\t\t{
\t\t\t\"Action\": \"es:*\",
\t\t\t\"Principal\": \"*\",
\t\t\t\"Effect\": \"Allow\",
\t\t\t\"Resource\": \"arn:aws:es:%s:%s:domain/%s/*\"
\t\t}
\t]
}
", current.region(),currentGetCallerIdentity.accountId(),domain))
            .tags(Map.of("Domain", "TestDomain"))
            .build(), CustomResourceOptions.builder()
                .dependsOn(esServiceLinkedRole)
                .build());

    }
}
configuration:
  vpc:
    type: dynamic
  domain:
    type: string
    default: tf-test
resources:
  es:
    type: aws:ec2:SecurityGroup
    properties:
      name: ${vpc}-elasticsearch-${domain}
      description: Managed by Pulumi
      vpcId: ${selected.id}
      ingress:
        - fromPort: 443
          toPort: 443
          protocol: tcp
          cidrBlocks:
            - ${selected.cidrBlock}
  esServiceLinkedRole:
    type: aws:iam:ServiceLinkedRole
    name: es
    properties:
      awsServiceName: opensearchservice.amazonaws.com
  esDomain:
    type: aws:elasticsearch:Domain
    name: es
    properties:
      domainName: ${domain}
      elasticsearchVersion: '6.3'
      clusterConfig:
        instanceType: m4.large.elasticsearch
        zoneAwarenessEnabled: true
      vpcOptions:
        subnetIds:
          - ${selectedGetSubnets.ids[0]}
          - ${selectedGetSubnets.ids[1]}
        securityGroupIds:
          - ${es.id}
      advancedOptions:
        rest.action.multi.allow_explicit_index: 'true'
      accessPolicies: |
        {
        \t\"Version\": \"2012-10-17\",
        \t\"Statement\": [
        \t\t{
        \t\t\t\"Action\": \"es:*\",
        \t\t\t\"Principal\": \"*\",
        \t\t\t\"Effect\": \"Allow\",
        \t\t\t\"Resource\": \"arn:aws:es:${current.region}:${currentGetCallerIdentity.accountId}:domain/${domain}/*\"
        \t\t}
        \t]
        }        
      tags:
        Domain: TestDomain
    options:
      dependsOn:
        - ${esServiceLinkedRole}
variables:
  selected:
    fn::invoke:
      function: aws:ec2:getVpc
      arguments:
        tags:
          Name: ${vpc}
  selectedGetSubnets:
    fn::invoke:
      function: aws:ec2:getSubnets
      arguments:
        filters:
          - name: vpc-id
            values:
              - ${selected.id}
        tags:
          Tier: private
  current:
    fn::invoke:
      function: aws:getRegion
      arguments: {}
  currentGetCallerIdentity:
    fn::invoke:
      function: aws:getCallerIdentity
      arguments: {}

The vpcOptions block places the domain in specified subnets with attached security groups, enabling private connectivity from VPC resources. Setting zoneAwarenessEnabled to true distributes nodes across multiple availability zones for higher availability. This configuration extends the basic example by adding VPC networking and multi-AZ deployment.

Beyond these examples

These snippets focus on specific domain-level features: instance sizing and version selection, IP-based and VPC access control, and CloudWatch Logs integration. They’re intentionally minimal rather than full search deployments.

The examples may reference pre-existing infrastructure such as VPCs, subnets, and security groups for VPC deployment, and CloudWatch log groups and IAM policies for log publishing. They focus on configuring the domain rather than provisioning everything around it.

To keep things focused, common domain patterns are omitted, including:

  • EBS volume configuration (ebsOptions)
  • Encryption at rest and node-to-node encryption
  • Fine-grained access control (advancedSecurityOptions)
  • Cognito authentication for Kibana
  • Custom domain endpoints (domainEndpointOptions)
  • Auto-Tune and snapshot configuration

These omissions are intentional: the goal is to illustrate how each domain feature is wired, not provide drop-in search clusters. See the Elasticsearch Domain resource reference for all available configuration options.

Let's create AWS Elasticsearch Domains

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Configuration & Perpetual Diffs
Why does my Elasticsearch domain keep showing changes on every apply?
Values in advancedOptions must be strings (wrapped in quotes). Non-string values cause a perpetual diff, triggering domain recreation on every apply.
What properties are immutable after creation?
Both domainName and vpcOptions cannot be changed after creation. Adding or removing VPC configuration forces a new resource.
VPC & Networking
How do I set up VPC access for my Elasticsearch domain?
Configure vpcOptions with subnetIds and securityGroupIds. For zone awareness, use multiple subnets across availability zones. The security group should allow port 443 from your VPC CIDR block.
Why does the VPC example depend on a service-linked role?
VPC-enabled domains require an IAM service-linked role for opensearchservice.amazonaws.com. Use dependsOn to ensure the role exists before domain creation.
Security & Access Control
How do I restrict access to my domain by IP address?
Use accessPolicies with a Condition block containing IpAddress and aws:SourceIp. The policy should specify the domain ARN as the Resource.
Can I use encryption at rest with any instance type?
No, encryption at rest is only available for certain instance types. Check AWS documentation for supported instance types.
What's fine-grained access control?
Configure advancedSecurityOptions to enable fine-grained access control, which provides additional security features beyond IAM policies.
Logging & Monitoring
How do I publish logs to CloudWatch?
Configure logPublishingOptions with cloudwatchLogGroupArn and logType (e.g., INDEX_SLOW_LOGS). You’ll also need a CloudWatch log resource policy granting es.amazonaws.com permissions for logs:PutLogEvents, logs:PutLogEventsBatch, and logs:CreateLogStream.
Deprecated Features
Should I still configure snapshot options?
No, snapshotOptions is deprecated. For Elasticsearch 5.3 and later, Amazon ES automatically takes hourly snapshots, making this configuration irrelevant.

Using a different cloud?

Explore analytics guides for other cloud providers: