The aws:elasticsearch/domain:Domain resource, part of the Pulumi AWS provider, provisions an AWS Elasticsearch domain: the cluster configuration, network placement, and access controls. This guide focuses on four capabilities: instance type and version selection, IP-based access policies, CloudWatch Logs integration, and VPC deployment.
Elasticsearch domains can run with public endpoints or within a VPC. VPC-based domains require existing subnets and security groups; logging requires CloudWatch log groups and IAM policies. The examples are intentionally small. Combine them with your own VPC infrastructure, encryption settings, and authentication mechanisms.
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 defines the instance type; larger instance types provide more memory for indexing and query operations.
Restrict access by IP address with IAM policies
Production domains typically limit access to specific IP ranges or AWS principals to prevent unauthorized queries.
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 IpAddress to allow requests only from a specific IP. The policy references the domain ARN constructed from region, account ID, and domain name.
Send slow query logs to CloudWatch
Troubleshooting performance issues often requires analyzing slow queries captured in CloudWatch Logs.
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 sends Elasticsearch logs to CloudWatch. Each entry specifies a logType (INDEX_SLOW_LOGS, SEARCH_SLOW_LOGS, or ES_APPLICATION_LOGS) and a cloudwatchLogGroupArn. The LogResourcePolicy grants Elasticsearch permission to write logs; without it, log delivery fails silently.
Deploy in a VPC for private network access
Applications in private subnets need Elasticsearch domains deployed within the same VPC to avoid exposing data over the public 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.require("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. The security group must allow inbound HTTPS (port 443) from your application’s CIDR range. VPC-based domains require a ServiceLinkedRole for opensearchservice.amazonaws.com; the dependsOn ensures the role exists before domain creation. Using multiple subnets enables zone awareness for high availability.
Beyond these examples
These snippets focus on specific Elasticsearch domain features: instance sizing and version selection, IP-based access control, CloudWatch Logs integration, and VPC networking. They’re intentionally minimal rather than full search cluster deployments.
The examples may reference pre-existing infrastructure such as VPC, subnets, and security groups (for VPC example), and CloudWatch log groups and IAM policies (for logging example). 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
- Auto-Tune and snapshot options
- Custom domain endpoints (domainEndpointOptions)
These omissions are intentional: the goal is to illustrate how each domain feature is wired, not provide drop-in search cluster modules. 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 FREEFrequently Asked Questions
Configuration & Immutability
advancedOptions, ensure all values are strings wrapped in quotes. Non-string values cause a perpetual diff, triggering domain recreation on every apply.vpcOptions forces creation of a new resource. Plan your VPC configuration before initial deployment, as changes require domain replacement.domainName and vpcOptions are immutable. Changing either requires replacing the domain.Security & Access Control
accessPolicies with a Condition block containing IpAddress and aws:SourceIp, as shown in the access policy example.encryptAtRest) is only available for certain instance types. Check AWS documentation for supported instance types.accessPolicies provides domain-level IAM access control, while advancedSecurityOptions enables fine-grained access control for more granular permissions.VPC & Networking
vpcOptions with subnetIds and securityGroupIds. The VPC example shows setting up zone awareness with multiple subnets and a security group allowing HTTPS (port 443) access.ServiceLinkedRole being created first for proper VPC setup. Without this dependency, the domain creation may fail.Logging & Monitoring
logPublishingOptions with cloudwatchLogGroupArn and logType (e.g., INDEX_SLOW_LOGS). You’ll also need to create a CloudWatch LogResourcePolicy granting the Elasticsearch service permissions to write logs.Storage & Snapshots
snapshotOptions is deprecated. For Elasticsearch 5.3 and later, AWS automatically takes hourly snapshots, making this setting irrelevant.ebsOptions may be required based on your chosen instance size. Check AWS Elasticsearch pricing documentation for instance-specific storage requirements.Using a different cloud?
Explore analytics guides for other cloud providers: