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 FREEFrequently Asked Questions
Configuration & Perpetual Diffs
advancedOptions must be strings (wrapped in quotes). Non-string values cause a perpetual diff, triggering domain recreation on every apply.domainName and vpcOptions cannot be changed after creation. Adding or removing VPC configuration forces a new resource.VPC & Networking
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.opensearchservice.amazonaws.com. Use dependsOn to ensure the role exists before domain creation.Security & Access Control
accessPolicies with a Condition block containing IpAddress and aws:SourceIp. The policy should specify the domain ARN as the Resource.advancedSecurityOptions to enable fine-grained access control, which provides additional security features beyond IAM policies.Logging & Monitoring
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
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: