The aws:opensearch/domain:Domain resource, part of the Pulumi AWS provider, provisions an Amazon OpenSearch Service domain: the search cluster, its network placement, access controls, and encryption settings. This guide focuses on four capabilities: basic domain creation with engine versions, IP-based and VPC access controls, CloudWatch logging integration, and fine-grained access control enablement.
OpenSearch domains may reference VPC subnets, security groups, CloudWatch log groups, and IAM roles that must exist separately. The examples are intentionally small. Combine them with your own networking, IAM policies, and monitoring infrastructure.
Create a domain with instance type and version
Most deployments start by defining a domain name, selecting an engine version, and choosing an 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.opensearch.Domain("example", {
domainName: "example",
engineVersion: "Elasticsearch_7.10",
clusterConfig: {
instanceType: "r4.large.search",
},
tags: {
Domain: "TestDomain",
},
});
import pulumi
import pulumi_aws as aws
example = aws.opensearch.Domain("example",
domain_name="example",
engine_version="Elasticsearch_7.10",
cluster_config={
"instance_type": "r4.large.search",
},
tags={
"Domain": "TestDomain",
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/opensearch"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := opensearch.NewDomain(ctx, "example", &opensearch.DomainArgs{
DomainName: pulumi.String("example"),
EngineVersion: pulumi.String("Elasticsearch_7.10"),
ClusterConfig: &opensearch.DomainClusterConfigArgs{
InstanceType: pulumi.String("r4.large.search"),
},
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.OpenSearch.Domain("example", new()
{
DomainName = "example",
EngineVersion = "Elasticsearch_7.10",
ClusterConfig = new Aws.OpenSearch.Inputs.DomainClusterConfigArgs
{
InstanceType = "r4.large.search",
},
Tags =
{
{ "Domain", "TestDomain" },
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.opensearch.Domain;
import com.pulumi.aws.opensearch.DomainArgs;
import com.pulumi.aws.opensearch.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")
.engineVersion("Elasticsearch_7.10")
.clusterConfig(DomainClusterConfigArgs.builder()
.instanceType("r4.large.search")
.build())
.tags(Map.of("Domain", "TestDomain"))
.build());
}
}
resources:
example:
type: aws:opensearch:Domain
properties:
domainName: example
engineVersion: Elasticsearch_7.10
clusterConfig:
instanceType: r4.large.search
tags:
Domain: TestDomain
The domainName property sets a unique identifier for your cluster. The engineVersion specifies either OpenSearch or Elasticsearch (format: OpenSearch_X.Y or Elasticsearch_X.Y). The clusterConfig.instanceType determines compute capacity; OpenSearch instance types end in .search (e.g., r4.large.search), unlike Elasticsearch which uses .elasticsearch.
Restrict access by source IP address
Production domains typically restrict 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 = Promise.all([current, currentGetCallerIdentity]).then(([current, currentGetCallerIdentity]) => aws.iam.getPolicyDocument({
statements: [{
effect: "Allow",
principals: [{
type: "*",
identifiers: ["*"],
}],
actions: ["es:*"],
resources: [`arn:aws:es:${current.region}:${currentGetCallerIdentity.accountId}:domain/${domain}/*`],
conditions: [{
test: "IpAddress",
variable: "aws:SourceIp",
values: ["66.193.100.22/32"],
}],
}],
}));
const exampleDomain = new aws.opensearch.Domain("example", {
domainName: domain,
accessPolicies: example.then(example => example.json),
});
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.iam.get_policy_document(statements=[{
"effect": "Allow",
"principals": [{
"type": "*",
"identifiers": ["*"],
}],
"actions": ["es:*"],
"resources": [f"arn:aws:es:{current.region}:{current_get_caller_identity.account_id}:domain/{domain}/*"],
"conditions": [{
"test": "IpAddress",
"variable": "aws:SourceIp",
"values": ["66.193.100.22/32"],
}],
}])
example_domain = aws.opensearch.Domain("example",
domain_name=domain,
access_policies=example.json)
package main
import (
"fmt"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/iam"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/opensearch"
"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
}
example, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
Statements: []iam.GetPolicyDocumentStatement{
{
Effect: pulumi.StringRef("Allow"),
Principals: []iam.GetPolicyDocumentStatementPrincipal{
{
Type: "*",
Identifiers: []string{
"*",
},
},
},
Actions: []string{
"es:*",
},
Resources: []string{
fmt.Sprintf("arn:aws:es:%v:%v:domain/%v/*", current.Region, currentGetCallerIdentity.AccountId, domain),
},
Conditions: []iam.GetPolicyDocumentStatementCondition{
{
Test: "IpAddress",
Variable: "aws:SourceIp",
Values: []string{
"66.193.100.22/32",
},
},
},
},
},
}, nil)
if err != nil {
return err
}
_, err = opensearch.NewDomain(ctx, "example", &opensearch.DomainArgs{
DomainName: pulumi.String(domain),
AccessPolicies: pulumi.String(example.Json),
})
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 = Aws.Iam.GetPolicyDocument.Invoke(new()
{
Statements = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs
{
Effect = "Allow",
Principals = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalInputArgs
{
Type = "*",
Identifiers = new[]
{
"*",
},
},
},
Actions = new[]
{
"es:*",
},
Resources = new[]
{
$"arn:aws:es:{current.Apply(getRegionResult => getRegionResult.Region)}:{currentGetCallerIdentity.Apply(getCallerIdentityResult => getCallerIdentityResult.AccountId)}:domain/{domain}/*",
},
Conditions = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementConditionInputArgs
{
Test = "IpAddress",
Variable = "aws:SourceIp",
Values = new[]
{
"66.193.100.22/32",
},
},
},
},
},
});
var exampleDomain = new Aws.OpenSearch.Domain("example", new()
{
DomainName = domain,
AccessPolicies = example.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json),
});
});
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.iam.IamFunctions;
import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs;
import com.pulumi.aws.opensearch.Domain;
import com.pulumi.aws.opensearch.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());
final var example = IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder()
.statements(GetPolicyDocumentStatementArgs.builder()
.effect("Allow")
.principals(GetPolicyDocumentStatementPrincipalArgs.builder()
.type("*")
.identifiers("*")
.build())
.actions("es:*")
.resources(String.format("arn:aws:es:%s:%s:domain/%s/*", current.region(),currentGetCallerIdentity.accountId(),domain))
.conditions(GetPolicyDocumentStatementConditionArgs.builder()
.test("IpAddress")
.variable("aws:SourceIp")
.values("66.193.100.22/32")
.build())
.build())
.build());
var exampleDomain = new Domain("exampleDomain", DomainArgs.builder()
.domainName(domain)
.accessPolicies(example.json())
.build());
}
}
configuration:
domain:
type: string
default: tf-test
resources:
exampleDomain:
type: aws:opensearch:Domain
name: example
properties:
domainName: ${domain}
accessPolicies: ${example.json}
variables:
current:
fn::invoke:
function: aws:getRegion
arguments: {}
currentGetCallerIdentity:
fn::invoke:
function: aws:getCallerIdentity
arguments: {}
example:
fn::invoke:
function: aws:iam:getPolicyDocument
arguments:
statements:
- effect: Allow
principals:
- type: '*'
identifiers:
- '*'
actions:
- es:*
resources:
- arn:aws:es:${current.region}:${currentGetCallerIdentity.accountId}:domain/${domain}/*
conditions:
- test: IpAddress
variable: aws:SourceIp
values:
- 66.193.100.22/32
The accessPolicies property accepts an IAM policy document that controls who can access the domain. This example uses an IP address condition (aws:SourceIp) to limit access to a specific CIDR range. The policy applies to the domain’s public endpoint; replace the hardcoded IP with your own network range.
Send slow query logs to CloudWatch
Troubleshooting performance issues often requires analyzing slow queries and indexing operations 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.opensearch.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.opensearch.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/iam"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/opensearch"
"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 = opensearch.NewDomain(ctx, "example", &opensearch.DomainArgs{
LogPublishingOptions: opensearch.DomainLogPublishingOptionArray{
&opensearch.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.OpenSearch.Domain("example", new()
{
LogPublishingOptions = new[]
{
new Aws.OpenSearch.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.opensearch.Domain;
import com.pulumi.aws.opensearch.DomainArgs;
import com.pulumi.aws.opensearch.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:opensearch: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 domain logs to CloudWatch. Each entry specifies a logType (INDEX_SLOW_LOGS, SEARCH_SLOW_LOGS, or ES_APPLICATION_LOGS) and a cloudwatchLogGroupArn. OpenSearch needs IAM permissions to write logs; the LogResourcePolicy grants the es.amazonaws.com service principal access to PutLogEvents.
Deploy in a VPC with private subnets
Applications that need to keep search traffic private deploy domains inside VPCs, restricting access to resources within the same network.
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 example = aws.ec2.getVpc({
tags: {
Name: vpc,
},
});
const exampleGetSubnets = example.then(example => aws.ec2.getSubnets({
filters: [{
name: "vpc-id",
values: [example.id],
}],
tags: {
Tier: "private",
},
}));
const current = aws.getRegion({});
const currentGetCallerIdentity = aws.getCallerIdentity({});
const exampleSecurityGroup = new aws.ec2.SecurityGroup("example", {
name: `${vpc}-opensearch-${domain}`,
description: "Managed by Pulumi",
vpcId: example.then(example => example.id),
ingress: [{
fromPort: 443,
toPort: 443,
protocol: "tcp",
cidrBlocks: [example.then(example => example.cidrBlock)],
}],
});
const exampleServiceLinkedRole = new aws.iam.ServiceLinkedRole("example", {awsServiceName: "opensearchservice.amazonaws.com"});
const exampleGetPolicyDocument = Promise.all([current, currentGetCallerIdentity]).then(([current, currentGetCallerIdentity]) => aws.iam.getPolicyDocument({
statements: [{
effect: "Allow",
principals: [{
type: "*",
identifiers: ["*"],
}],
actions: ["es:*"],
resources: [`arn:aws:es:${current.region}:${currentGetCallerIdentity.accountId}:domain/${domain}/*`],
}],
}));
const exampleDomain = new aws.opensearch.Domain("example", {
domainName: domain,
engineVersion: "OpenSearch_1.0",
clusterConfig: {
instanceType: "m4.large.search",
zoneAwarenessEnabled: true,
},
vpcOptions: {
subnetIds: [
exampleGetSubnets.then(exampleGetSubnets => exampleGetSubnets.ids?.[0]),
exampleGetSubnets.then(exampleGetSubnets => exampleGetSubnets.ids?.[1]),
],
securityGroupIds: [exampleSecurityGroup.id],
},
advancedOptions: {
"rest.action.multi.allow_explicit_index": "true",
},
accessPolicies: exampleGetPolicyDocument.then(exampleGetPolicyDocument => exampleGetPolicyDocument.json),
tags: {
Domain: "TestDomain",
},
}, {
dependsOn: [exampleServiceLinkedRole],
});
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"
example = aws.ec2.get_vpc(tags={
"Name": vpc,
})
example_get_subnets = aws.ec2.get_subnets(filters=[{
"name": "vpc-id",
"values": [example.id],
}],
tags={
"Tier": "private",
})
current = aws.get_region()
current_get_caller_identity = aws.get_caller_identity()
example_security_group = aws.ec2.SecurityGroup("example",
name=f"{vpc}-opensearch-{domain}",
description="Managed by Pulumi",
vpc_id=example.id,
ingress=[{
"from_port": 443,
"to_port": 443,
"protocol": "tcp",
"cidr_blocks": [example.cidr_block],
}])
example_service_linked_role = aws.iam.ServiceLinkedRole("example", aws_service_name="opensearchservice.amazonaws.com")
example_get_policy_document = aws.iam.get_policy_document(statements=[{
"effect": "Allow",
"principals": [{
"type": "*",
"identifiers": ["*"],
}],
"actions": ["es:*"],
"resources": [f"arn:aws:es:{current.region}:{current_get_caller_identity.account_id}:domain/{domain}/*"],
}])
example_domain = aws.opensearch.Domain("example",
domain_name=domain,
engine_version="OpenSearch_1.0",
cluster_config={
"instance_type": "m4.large.search",
"zone_awareness_enabled": True,
},
vpc_options={
"subnet_ids": [
example_get_subnets.ids[0],
example_get_subnets.ids[1],
],
"security_group_ids": [example_security_group.id],
},
advanced_options={
"rest.action.multi.allow_explicit_index": "true",
},
access_policies=example_get_policy_document.json,
tags={
"Domain": "TestDomain",
},
opts = pulumi.ResourceOptions(depends_on=[example_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/iam"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/opensearch"
"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
}
example, err := ec2.LookupVpc(ctx, &ec2.LookupVpcArgs{
Tags: pulumi.StringMap{
"Name": vpc,
},
}, nil);
if err != nil {
return err
}
exampleGetSubnets, err := ec2.GetSubnets(ctx, &ec2.GetSubnetsArgs{
Filters: []ec2.GetSubnetsFilter{
{
Name: "vpc-id",
Values: interface{}{
example.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
}
exampleSecurityGroup, err := ec2.NewSecurityGroup(ctx, "example", &ec2.SecurityGroupArgs{
Name: pulumi.Sprintf("%v-opensearch-%v", vpc, domain),
Description: pulumi.String("Managed by Pulumi"),
VpcId: pulumi.String(example.Id),
Ingress: ec2.SecurityGroupIngressArray{
&ec2.SecurityGroupIngressArgs{
FromPort: pulumi.Int(443),
ToPort: pulumi.Int(443),
Protocol: pulumi.String("tcp"),
CidrBlocks: pulumi.StringArray{
pulumi.String(example.CidrBlock),
},
},
},
})
if err != nil {
return err
}
exampleServiceLinkedRole, err := iam.NewServiceLinkedRole(ctx, "example", &iam.ServiceLinkedRoleArgs{
AwsServiceName: pulumi.String("opensearchservice.amazonaws.com"),
})
if err != nil {
return err
}
exampleGetPolicyDocument, err := iam.GetPolicyDocument(ctx, &iam.GetPolicyDocumentArgs{
Statements: []iam.GetPolicyDocumentStatement{
{
Effect: pulumi.StringRef("Allow"),
Principals: []iam.GetPolicyDocumentStatementPrincipal{
{
Type: "*",
Identifiers: []string{
"*",
},
},
},
Actions: []string{
"es:*",
},
Resources: []string{
fmt.Sprintf("arn:aws:es:%v:%v:domain/%v/*", current.Region, currentGetCallerIdentity.AccountId, domain),
},
},
},
}, nil);
if err != nil {
return err
}
_, err = opensearch.NewDomain(ctx, "example", &opensearch.DomainArgs{
DomainName: pulumi.String(domain),
EngineVersion: pulumi.String("OpenSearch_1.0"),
ClusterConfig: &opensearch.DomainClusterConfigArgs{
InstanceType: pulumi.String("m4.large.search"),
ZoneAwarenessEnabled: pulumi.Bool(true),
},
VpcOptions: &opensearch.DomainVpcOptionsArgs{
SubnetIds: pulumi.StringArray{
pulumi.String(exampleGetSubnets.Ids[0]),
pulumi.String(exampleGetSubnets.Ids[1]),
},
SecurityGroupIds: pulumi.StringArray{
exampleSecurityGroup.ID(),
},
},
AdvancedOptions: pulumi.StringMap{
"rest.action.multi.allow_explicit_index": pulumi.String("true"),
},
AccessPolicies: pulumi.String(exampleGetPolicyDocument.Json),
Tags: pulumi.StringMap{
"Domain": pulumi.String("TestDomain"),
},
}, pulumi.DependsOn([]pulumi.Resource{
exampleServiceLinkedRole,
}))
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 example = Aws.Ec2.GetVpc.Invoke(new()
{
Tags =
{
{ "Name", vpc },
},
});
var exampleGetSubnets = Aws.Ec2.GetSubnets.Invoke(new()
{
Filters = new[]
{
new Aws.Ec2.Inputs.GetSubnetsFilterInputArgs
{
Name = "vpc-id",
Values = new[]
{
example.Apply(getVpcResult => getVpcResult.Id),
},
},
},
Tags =
{
{ "Tier", "private" },
},
});
var current = Aws.GetRegion.Invoke();
var currentGetCallerIdentity = Aws.GetCallerIdentity.Invoke();
var exampleSecurityGroup = new Aws.Ec2.SecurityGroup("example", new()
{
Name = $"{vpc}-opensearch-{domain}",
Description = "Managed by Pulumi",
VpcId = example.Apply(getVpcResult => getVpcResult.Id),
Ingress = new[]
{
new Aws.Ec2.Inputs.SecurityGroupIngressArgs
{
FromPort = 443,
ToPort = 443,
Protocol = "tcp",
CidrBlocks = new[]
{
example.Apply(getVpcResult => getVpcResult.CidrBlock),
},
},
},
});
var exampleServiceLinkedRole = new Aws.Iam.ServiceLinkedRole("example", new()
{
AwsServiceName = "opensearchservice.amazonaws.com",
});
var exampleGetPolicyDocument = Aws.Iam.GetPolicyDocument.Invoke(new()
{
Statements = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementInputArgs
{
Effect = "Allow",
Principals = new[]
{
new Aws.Iam.Inputs.GetPolicyDocumentStatementPrincipalInputArgs
{
Type = "*",
Identifiers = new[]
{
"*",
},
},
},
Actions = new[]
{
"es:*",
},
Resources = new[]
{
$"arn:aws:es:{current.Apply(getRegionResult => getRegionResult.Region)}:{currentGetCallerIdentity.Apply(getCallerIdentityResult => getCallerIdentityResult.AccountId)}:domain/{domain}/*",
},
},
},
});
var exampleDomain = new Aws.OpenSearch.Domain("example", new()
{
DomainName = domain,
EngineVersion = "OpenSearch_1.0",
ClusterConfig = new Aws.OpenSearch.Inputs.DomainClusterConfigArgs
{
InstanceType = "m4.large.search",
ZoneAwarenessEnabled = true,
},
VpcOptions = new Aws.OpenSearch.Inputs.DomainVpcOptionsArgs
{
SubnetIds = new[]
{
exampleGetSubnets.Apply(getSubnetsResult => getSubnetsResult.Ids[0]),
exampleGetSubnets.Apply(getSubnetsResult => getSubnetsResult.Ids[1]),
},
SecurityGroupIds = new[]
{
exampleSecurityGroup.Id,
},
},
AdvancedOptions =
{
{ "rest.action.multi.allow_explicit_index", "true" },
},
AccessPolicies = exampleGetPolicyDocument.Apply(getPolicyDocumentResult => getPolicyDocumentResult.Json),
Tags =
{
{ "Domain", "TestDomain" },
},
}, new CustomResourceOptions
{
DependsOn =
{
exampleServiceLinkedRole,
},
});
});
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.iam.IamFunctions;
import com.pulumi.aws.iam.inputs.GetPolicyDocumentArgs;
import com.pulumi.aws.opensearch.Domain;
import com.pulumi.aws.opensearch.DomainArgs;
import com.pulumi.aws.opensearch.inputs.DomainClusterConfigArgs;
import com.pulumi.aws.opensearch.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 example = Ec2Functions.getVpc(GetVpcArgs.builder()
.tags(Map.of("Name", vpc))
.build());
final var exampleGetSubnets = Ec2Functions.getSubnets(GetSubnetsArgs.builder()
.filters(GetSubnetsFilterArgs.builder()
.name("vpc-id")
.values(example.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 exampleSecurityGroup = new SecurityGroup("exampleSecurityGroup", SecurityGroupArgs.builder()
.name(String.format("%s-opensearch-%s", vpc,domain))
.description("Managed by Pulumi")
.vpcId(example.id())
.ingress(SecurityGroupIngressArgs.builder()
.fromPort(443)
.toPort(443)
.protocol("tcp")
.cidrBlocks(example.cidrBlock())
.build())
.build());
var exampleServiceLinkedRole = new ServiceLinkedRole("exampleServiceLinkedRole", ServiceLinkedRoleArgs.builder()
.awsServiceName("opensearchservice.amazonaws.com")
.build());
final var exampleGetPolicyDocument = IamFunctions.getPolicyDocument(GetPolicyDocumentArgs.builder()
.statements(GetPolicyDocumentStatementArgs.builder()
.effect("Allow")
.principals(GetPolicyDocumentStatementPrincipalArgs.builder()
.type("*")
.identifiers("*")
.build())
.actions("es:*")
.resources(String.format("arn:aws:es:%s:%s:domain/%s/*", current.region(),currentGetCallerIdentity.accountId(),domain))
.build())
.build());
var exampleDomain = new Domain("exampleDomain", DomainArgs.builder()
.domainName(domain)
.engineVersion("OpenSearch_1.0")
.clusterConfig(DomainClusterConfigArgs.builder()
.instanceType("m4.large.search")
.zoneAwarenessEnabled(true)
.build())
.vpcOptions(DomainVpcOptionsArgs.builder()
.subnetIds(
exampleGetSubnets.ids()[0],
exampleGetSubnets.ids()[1])
.securityGroupIds(exampleSecurityGroup.id())
.build())
.advancedOptions(Map.of("rest.action.multi.allow_explicit_index", "true"))
.accessPolicies(exampleGetPolicyDocument.json())
.tags(Map.of("Domain", "TestDomain"))
.build(), CustomResourceOptions.builder()
.dependsOn(exampleServiceLinkedRole)
.build());
}
}
configuration:
vpc:
type: dynamic
domain:
type: string
default: tf-test
resources:
exampleSecurityGroup:
type: aws:ec2:SecurityGroup
name: example
properties:
name: ${vpc}-opensearch-${domain}
description: Managed by Pulumi
vpcId: ${example.id}
ingress:
- fromPort: 443
toPort: 443
protocol: tcp
cidrBlocks:
- ${example.cidrBlock}
exampleServiceLinkedRole:
type: aws:iam:ServiceLinkedRole
name: example
properties:
awsServiceName: opensearchservice.amazonaws.com
exampleDomain:
type: aws:opensearch:Domain
name: example
properties:
domainName: ${domain}
engineVersion: OpenSearch_1.0
clusterConfig:
instanceType: m4.large.search
zoneAwarenessEnabled: true
vpcOptions:
subnetIds:
- ${exampleGetSubnets.ids[0]}
- ${exampleGetSubnets.ids[1]}
securityGroupIds:
- ${exampleSecurityGroup.id}
advancedOptions:
rest.action.multi.allow_explicit_index: 'true'
accessPolicies: ${exampleGetPolicyDocument.json}
tags:
Domain: TestDomain
options:
dependsOn:
- ${exampleServiceLinkedRole}
variables:
example:
fn::invoke:
function: aws:ec2:getVpc
arguments:
tags:
Name: ${vpc}
exampleGetSubnets:
fn::invoke:
function: aws:ec2:getSubnets
arguments:
filters:
- name: vpc-id
values:
- ${example.id}
tags:
Tier: private
current:
fn::invoke:
function: aws:getRegion
arguments: {}
currentGetCallerIdentity:
fn::invoke:
function: aws:getCallerIdentity
arguments: {}
exampleGetPolicyDocument:
fn::invoke:
function: aws:iam:getPolicyDocument
arguments:
statements:
- effect: Allow
principals:
- type: '*'
identifiers:
- '*'
actions:
- es:*
resources:
- arn:aws:es:${current.region}:${currentGetCallerIdentity.accountId}:domain/${domain}/*
The vpcOptions property places your domain in specified subnets with attached security groups, isolating traffic within your VPC. The example creates a security group allowing HTTPS (port 443) from the VPC CIDR block. OpenSearch requires the AWSServiceRoleForAmazonOpenSearchService service-linked role for VPC deployments; the example creates it explicitly via dependsOn.
Prepare domain for fine-grained access control
Fine-grained access control (FGAC) requires encryption at rest, node-to-node encryption, and HTTPS enforcement as prerequisites before it can be enabled.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const example = new aws.opensearch.Domain("example", {
domainName: "ggkitty",
engineVersion: "Elasticsearch_7.1",
clusterConfig: {
instanceType: "r5.large.search",
},
advancedSecurityOptions: {
enabled: false,
anonymousAuthEnabled: true,
internalUserDatabaseEnabled: true,
masterUserOptions: {
masterUserName: "example",
masterUserPassword: "Barbarbarbar1!",
},
},
encryptAtRest: {
enabled: true,
},
domainEndpointOptions: {
enforceHttps: true,
tlsSecurityPolicy: "Policy-Min-TLS-1-2-2019-07",
},
nodeToNodeEncryption: {
enabled: true,
},
ebsOptions: {
ebsEnabled: true,
volumeSize: 10,
},
});
import pulumi
import pulumi_aws as aws
example = aws.opensearch.Domain("example",
domain_name="ggkitty",
engine_version="Elasticsearch_7.1",
cluster_config={
"instance_type": "r5.large.search",
},
advanced_security_options={
"enabled": False,
"anonymous_auth_enabled": True,
"internal_user_database_enabled": True,
"master_user_options": {
"master_user_name": "example",
"master_user_password": "Barbarbarbar1!",
},
},
encrypt_at_rest={
"enabled": True,
},
domain_endpoint_options={
"enforce_https": True,
"tls_security_policy": "Policy-Min-TLS-1-2-2019-07",
},
node_to_node_encryption={
"enabled": True,
},
ebs_options={
"ebs_enabled": True,
"volume_size": 10,
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/opensearch"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := opensearch.NewDomain(ctx, "example", &opensearch.DomainArgs{
DomainName: pulumi.String("ggkitty"),
EngineVersion: pulumi.String("Elasticsearch_7.1"),
ClusterConfig: &opensearch.DomainClusterConfigArgs{
InstanceType: pulumi.String("r5.large.search"),
},
AdvancedSecurityOptions: &opensearch.DomainAdvancedSecurityOptionsArgs{
Enabled: pulumi.Bool(false),
AnonymousAuthEnabled: pulumi.Bool(true),
InternalUserDatabaseEnabled: pulumi.Bool(true),
MasterUserOptions: &opensearch.DomainAdvancedSecurityOptionsMasterUserOptionsArgs{
MasterUserName: pulumi.String("example"),
MasterUserPassword: pulumi.String("Barbarbarbar1!"),
},
},
EncryptAtRest: &opensearch.DomainEncryptAtRestArgs{
Enabled: pulumi.Bool(true),
},
DomainEndpointOptions: &opensearch.DomainDomainEndpointOptionsArgs{
EnforceHttps: pulumi.Bool(true),
TlsSecurityPolicy: pulumi.String("Policy-Min-TLS-1-2-2019-07"),
},
NodeToNodeEncryption: &opensearch.DomainNodeToNodeEncryptionArgs{
Enabled: pulumi.Bool(true),
},
EbsOptions: &opensearch.DomainEbsOptionsArgs{
EbsEnabled: pulumi.Bool(true),
VolumeSize: pulumi.Int(10),
},
})
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.OpenSearch.Domain("example", new()
{
DomainName = "ggkitty",
EngineVersion = "Elasticsearch_7.1",
ClusterConfig = new Aws.OpenSearch.Inputs.DomainClusterConfigArgs
{
InstanceType = "r5.large.search",
},
AdvancedSecurityOptions = new Aws.OpenSearch.Inputs.DomainAdvancedSecurityOptionsArgs
{
Enabled = false,
AnonymousAuthEnabled = true,
InternalUserDatabaseEnabled = true,
MasterUserOptions = new Aws.OpenSearch.Inputs.DomainAdvancedSecurityOptionsMasterUserOptionsArgs
{
MasterUserName = "example",
MasterUserPassword = "Barbarbarbar1!",
},
},
EncryptAtRest = new Aws.OpenSearch.Inputs.DomainEncryptAtRestArgs
{
Enabled = true,
},
DomainEndpointOptions = new Aws.OpenSearch.Inputs.DomainDomainEndpointOptionsArgs
{
EnforceHttps = true,
TlsSecurityPolicy = "Policy-Min-TLS-1-2-2019-07",
},
NodeToNodeEncryption = new Aws.OpenSearch.Inputs.DomainNodeToNodeEncryptionArgs
{
Enabled = true,
},
EbsOptions = new Aws.OpenSearch.Inputs.DomainEbsOptionsArgs
{
EbsEnabled = true,
VolumeSize = 10,
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.opensearch.Domain;
import com.pulumi.aws.opensearch.DomainArgs;
import com.pulumi.aws.opensearch.inputs.DomainClusterConfigArgs;
import com.pulumi.aws.opensearch.inputs.DomainAdvancedSecurityOptionsArgs;
import com.pulumi.aws.opensearch.inputs.DomainAdvancedSecurityOptionsMasterUserOptionsArgs;
import com.pulumi.aws.opensearch.inputs.DomainEncryptAtRestArgs;
import com.pulumi.aws.opensearch.inputs.DomainDomainEndpointOptionsArgs;
import com.pulumi.aws.opensearch.inputs.DomainNodeToNodeEncryptionArgs;
import com.pulumi.aws.opensearch.inputs.DomainEbsOptionsArgs;
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("ggkitty")
.engineVersion("Elasticsearch_7.1")
.clusterConfig(DomainClusterConfigArgs.builder()
.instanceType("r5.large.search")
.build())
.advancedSecurityOptions(DomainAdvancedSecurityOptionsArgs.builder()
.enabled(false)
.anonymousAuthEnabled(true)
.internalUserDatabaseEnabled(true)
.masterUserOptions(DomainAdvancedSecurityOptionsMasterUserOptionsArgs.builder()
.masterUserName("example")
.masterUserPassword("Barbarbarbar1!")
.build())
.build())
.encryptAtRest(DomainEncryptAtRestArgs.builder()
.enabled(true)
.build())
.domainEndpointOptions(DomainDomainEndpointOptionsArgs.builder()
.enforceHttps(true)
.tlsSecurityPolicy("Policy-Min-TLS-1-2-2019-07")
.build())
.nodeToNodeEncryption(DomainNodeToNodeEncryptionArgs.builder()
.enabled(true)
.build())
.ebsOptions(DomainEbsOptionsArgs.builder()
.ebsEnabled(true)
.volumeSize(10)
.build())
.build());
}
}
resources:
example:
type: aws:opensearch:Domain
properties:
domainName: ggkitty
engineVersion: Elasticsearch_7.1
clusterConfig:
instanceType: r5.large.search
advancedSecurityOptions:
enabled: false
anonymousAuthEnabled: true
internalUserDatabaseEnabled: true
masterUserOptions:
masterUserName: example
masterUserPassword: Barbarbarbar1!
encryptAtRest:
enabled: true
domainEndpointOptions:
enforceHttps: true
tlsSecurityPolicy: Policy-Min-TLS-1-2-2019-07
nodeToNodeEncryption:
enabled: true
ebsOptions:
ebsEnabled: true
volumeSize: 10
This configuration sets up the encryption requirements for FGAC. The advancedSecurityOptions block has enabled set to false initially. The encryptAtRest, nodeToNodeEncryption, and domainEndpointOptions blocks configure the required encryption settings. The ebsOptions block enables EBS storage, which is required for encryption at rest.
Enable fine-grained access control
After encryption is in place, you can enable fine-grained access control to manage user permissions at the index and document level.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const example = new aws.opensearch.Domain("example", {
domainName: "ggkitty",
engineVersion: "Elasticsearch_7.1",
clusterConfig: {
instanceType: "r5.large.search",
},
advancedSecurityOptions: {
enabled: true,
anonymousAuthEnabled: true,
internalUserDatabaseEnabled: true,
masterUserOptions: {
masterUserName: "example",
masterUserPassword: "Barbarbarbar1!",
},
},
encryptAtRest: {
enabled: true,
},
domainEndpointOptions: {
enforceHttps: true,
tlsSecurityPolicy: "Policy-Min-TLS-1-2-2019-07",
},
nodeToNodeEncryption: {
enabled: true,
},
ebsOptions: {
ebsEnabled: true,
volumeSize: 10,
},
});
import pulumi
import pulumi_aws as aws
example = aws.opensearch.Domain("example",
domain_name="ggkitty",
engine_version="Elasticsearch_7.1",
cluster_config={
"instance_type": "r5.large.search",
},
advanced_security_options={
"enabled": True,
"anonymous_auth_enabled": True,
"internal_user_database_enabled": True,
"master_user_options": {
"master_user_name": "example",
"master_user_password": "Barbarbarbar1!",
},
},
encrypt_at_rest={
"enabled": True,
},
domain_endpoint_options={
"enforce_https": True,
"tls_security_policy": "Policy-Min-TLS-1-2-2019-07",
},
node_to_node_encryption={
"enabled": True,
},
ebs_options={
"ebs_enabled": True,
"volume_size": 10,
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/opensearch"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := opensearch.NewDomain(ctx, "example", &opensearch.DomainArgs{
DomainName: pulumi.String("ggkitty"),
EngineVersion: pulumi.String("Elasticsearch_7.1"),
ClusterConfig: &opensearch.DomainClusterConfigArgs{
InstanceType: pulumi.String("r5.large.search"),
},
AdvancedSecurityOptions: &opensearch.DomainAdvancedSecurityOptionsArgs{
Enabled: pulumi.Bool(true),
AnonymousAuthEnabled: pulumi.Bool(true),
InternalUserDatabaseEnabled: pulumi.Bool(true),
MasterUserOptions: &opensearch.DomainAdvancedSecurityOptionsMasterUserOptionsArgs{
MasterUserName: pulumi.String("example"),
MasterUserPassword: pulumi.String("Barbarbarbar1!"),
},
},
EncryptAtRest: &opensearch.DomainEncryptAtRestArgs{
Enabled: pulumi.Bool(true),
},
DomainEndpointOptions: &opensearch.DomainDomainEndpointOptionsArgs{
EnforceHttps: pulumi.Bool(true),
TlsSecurityPolicy: pulumi.String("Policy-Min-TLS-1-2-2019-07"),
},
NodeToNodeEncryption: &opensearch.DomainNodeToNodeEncryptionArgs{
Enabled: pulumi.Bool(true),
},
EbsOptions: &opensearch.DomainEbsOptionsArgs{
EbsEnabled: pulumi.Bool(true),
VolumeSize: pulumi.Int(10),
},
})
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.OpenSearch.Domain("example", new()
{
DomainName = "ggkitty",
EngineVersion = "Elasticsearch_7.1",
ClusterConfig = new Aws.OpenSearch.Inputs.DomainClusterConfigArgs
{
InstanceType = "r5.large.search",
},
AdvancedSecurityOptions = new Aws.OpenSearch.Inputs.DomainAdvancedSecurityOptionsArgs
{
Enabled = true,
AnonymousAuthEnabled = true,
InternalUserDatabaseEnabled = true,
MasterUserOptions = new Aws.OpenSearch.Inputs.DomainAdvancedSecurityOptionsMasterUserOptionsArgs
{
MasterUserName = "example",
MasterUserPassword = "Barbarbarbar1!",
},
},
EncryptAtRest = new Aws.OpenSearch.Inputs.DomainEncryptAtRestArgs
{
Enabled = true,
},
DomainEndpointOptions = new Aws.OpenSearch.Inputs.DomainDomainEndpointOptionsArgs
{
EnforceHttps = true,
TlsSecurityPolicy = "Policy-Min-TLS-1-2-2019-07",
},
NodeToNodeEncryption = new Aws.OpenSearch.Inputs.DomainNodeToNodeEncryptionArgs
{
Enabled = true,
},
EbsOptions = new Aws.OpenSearch.Inputs.DomainEbsOptionsArgs
{
EbsEnabled = true,
VolumeSize = 10,
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.opensearch.Domain;
import com.pulumi.aws.opensearch.DomainArgs;
import com.pulumi.aws.opensearch.inputs.DomainClusterConfigArgs;
import com.pulumi.aws.opensearch.inputs.DomainAdvancedSecurityOptionsArgs;
import com.pulumi.aws.opensearch.inputs.DomainAdvancedSecurityOptionsMasterUserOptionsArgs;
import com.pulumi.aws.opensearch.inputs.DomainEncryptAtRestArgs;
import com.pulumi.aws.opensearch.inputs.DomainDomainEndpointOptionsArgs;
import com.pulumi.aws.opensearch.inputs.DomainNodeToNodeEncryptionArgs;
import com.pulumi.aws.opensearch.inputs.DomainEbsOptionsArgs;
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("ggkitty")
.engineVersion("Elasticsearch_7.1")
.clusterConfig(DomainClusterConfigArgs.builder()
.instanceType("r5.large.search")
.build())
.advancedSecurityOptions(DomainAdvancedSecurityOptionsArgs.builder()
.enabled(true)
.anonymousAuthEnabled(true)
.internalUserDatabaseEnabled(true)
.masterUserOptions(DomainAdvancedSecurityOptionsMasterUserOptionsArgs.builder()
.masterUserName("example")
.masterUserPassword("Barbarbarbar1!")
.build())
.build())
.encryptAtRest(DomainEncryptAtRestArgs.builder()
.enabled(true)
.build())
.domainEndpointOptions(DomainDomainEndpointOptionsArgs.builder()
.enforceHttps(true)
.tlsSecurityPolicy("Policy-Min-TLS-1-2-2019-07")
.build())
.nodeToNodeEncryption(DomainNodeToNodeEncryptionArgs.builder()
.enabled(true)
.build())
.ebsOptions(DomainEbsOptionsArgs.builder()
.ebsEnabled(true)
.volumeSize(10)
.build())
.build());
}
}
resources:
example:
type: aws:opensearch:Domain
properties:
domainName: ggkitty
engineVersion: Elasticsearch_7.1
clusterConfig:
instanceType: r5.large.search
advancedSecurityOptions:
enabled: true
anonymousAuthEnabled: true
internalUserDatabaseEnabled: true
masterUserOptions:
masterUserName: example
masterUserPassword: Barbarbarbar1!
encryptAtRest:
enabled: true
domainEndpointOptions:
enforceHttps: true
tlsSecurityPolicy: Policy-Min-TLS-1-2-2019-07
nodeToNodeEncryption:
enabled: true
ebsOptions:
ebsEnabled: true
volumeSize: 10
The only change from the previous example is advancedSecurityOptions.enabled is now true. This activates role-based access control. The masterUserOptions block defines the initial admin credentials. FGAC enablement is a two-step process: first apply encryption settings with enabled=false, then update to enabled=true.
Beyond these examples
These snippets focus on specific domain-level features: domain creation and engine versioning, access policies and VPC isolation, and fine-grained access control enablement. They’re intentionally minimal rather than full search deployments.
The examples may reference pre-existing infrastructure such as VPC subnets and security groups, CloudWatch log groups, and IAM service-linked roles (created in the VPC example). They focus on configuring the domain rather than provisioning everything around it.
To keep things focused, common domain patterns are omitted, including:
- Cluster sizing (instance count, dedicated masters, zone awareness)
- EBS volume configuration (size, type, IOPS)
- Auto-Tune and off-peak maintenance windows
- Cognito or Identity Center authentication
- Snapshot configuration (deprecated for OpenSearch 5.3+)
These omissions are intentional: the goal is to illustrate how each domain feature is wired, not provide drop-in search clusters. See the OpenSearch Domain resource reference for all available configuration options.
Let's create AWS OpenSearch Domains
Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.
Try Pulumi Cloud for FREEFrequently Asked Questions
Elasticsearch vs OpenSearch Differences
arn:aws:es:, IAM policy actions use es: prefix (like es:*), and assume role policies reference the Principal Service as es.amazonaws.com.OpenSearch_X.Y format (e.g., OpenSearch_1.0), while Elasticsearch uses Elasticsearch_X.Y format (e.g., Elasticsearch_7.10). Specify the appropriate format in engineVersion..search (e.g., t2.micro.search), while Elasticsearch types end in .elasticsearch (e.g., t2.micro.elasticsearch).AWSServiceRoleForAmazonOpenSearchService, while Elasticsearch uses AWSServiceRoleForAmazonElasticsearchService.Configuration Issues & Perpetual Diffs
advancedOptions values aren’t strings (wrapped in quotes), they cause perpetual diffs and force recreation. Always quote all advancedOptions values, even numbers and booleans.vpcOptions is immutable. Adding or removing VPC configuration forces a new resource (domain recreation). Plan VPC settings carefully before creation.snapshotOptions is deprecated for OpenSearch 5.3 and later. Modern versions take hourly automated snapshots automatically, making this setting irrelevant.Fine-Grained Access Control
advancedSecurityOptions.enabled set to false, then update it to true in a second apply.encryptAtRest, nodeToNodeEncryption, and set domainEndpointOptions.enforceHttps to true with tlsSecurityPolicy of Policy-Min-TLS-1-2-2019-07 or higher.VPC & Networking
vpcOptions with subnetIds and securityGroupIds. Ensure the service-linked role AWSServiceRoleForAmazonOpenSearchService exists first, using dependsOn if creating it in the same stack.domainName and vpcOptions are immutable. Changing either forces a new resource (domain recreation).Endpoints & IP Addressing
endpoint is IPv4-only, while endpointV2 works with both IPv4 and IPv6 addresses. Set ipAddressType to dualstack to enable IPv6 support.Using a different cloud?
Explore analytics guides for other cloud providers: