Create AWS OpenSearch Domains

The aws:opensearch/domain:Domain resource, part of the Pulumi AWS provider, provisions an Amazon OpenSearch Service domain: its engine version, cluster topology, networking, and access controls. This guide focuses on three capabilities: engine version and instance type selection, access policies and fine-grained access control, and VPC networking and CloudWatch log publishing.

OpenSearch domains may reference VPC infrastructure, CloudWatch log groups, IAM policies, and service-linked roles that must exist separately. The examples are intentionally small. Combine them with your own networking, logging, and authentication infrastructure.

Create a domain with engine version and instance type

Most deployments start by choosing an engine version and instance type that matches your search workload and data volume.

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 engineVersion property specifies either OpenSearch or Elasticsearch (up to 7.10). The clusterConfig block sets the instance type; note that OpenSearch instance types end in .search rather than .elasticsearch. The domainName must be globally unique within your AWS account and region.

Control access with IP-based policies

OpenSearch domains are publicly accessible by default. Teams restrict access to specific IP ranges or AWS principals using IAM policy documents.

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 IP-based conditions to allow access only from a specific CIDR block. The policy applies to the domain’s public endpoint; VPC-based domains use security groups instead.

Publish slow query logs to CloudWatch

OpenSearch streams index slow logs, search slow logs, and application logs to CloudWatch for centralized monitoring.

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 enables log streaming to CloudWatch. Each entry specifies a logType (INDEX_SLOW_LOGS, SEARCH_SLOW_LOGS, or ES_APPLICATION_LOGS) and a cloudwatchLogGroupArn. OpenSearch requires a CloudWatch Logs resource policy that grants the service permission to write logs.

Deploy into a VPC for private access

Production deployments often place domains inside VPCs to restrict access to internal networks.

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.require("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 block places the domain in specified subnets with attached security groups. VPC deployment is immutable; adding or removing vpcOptions forces resource replacement. The example creates a service-linked role (AWSServiceRoleForAmazonOpenSearchService) that OpenSearch uses to manage network interfaces. VPC-based domains use security group rules instead of IP-based access policies.

Enable fine-grained access control with encryption

Fine-grained access control provides user-level permissions within OpenSearch, but requires encryption at rest, node-to-node encryption, and HTTPS enforcement.

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

Fine-grained access control (advancedSecurityOptions) adds user authentication and role-based access to domain data. It requires three encryption settings: encryptAtRest, nodeToNodeEncryption, and enforceHttps via domainEndpointOptions. The masterUserOptions block creates an internal user database with a master username and password. This example shows the initial configuration with advancedSecurityOptions.enabled set to false; enabling it on an existing domain requires a second apply with enabled set to true.

Beyond these examples

These snippets focus on specific domain-level features: engine version and cluster sizing, access control (IP policies and fine-grained access), and VPC networking and CloudWatch logging. They’re intentionally minimal rather than full search deployments.

The examples may reference pre-existing infrastructure such as VPC subnets and security groups (for VPC deployments), CloudWatch log groups and IAM resource policies, and service-linked roles for OpenSearch. They focus on configuring the domain rather than provisioning everything around it.

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

  • Auto-Tune and off-peak maintenance windows
  • Cognito or IAM Identity Center authentication
  • EBS volume configuration and snapshot options
  • Custom endpoint configuration (domainEndpointOptions)
  • Machine learning features (aimlOptions)

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 FREE

Frequently Asked Questions

Configuration & Perpetual Diff
Why does my OpenSearch domain show a perpetual diff?
If advancedOptions values aren’t strings (wrapped in quotes), they may cause perpetual diff and trigger unwanted recreation. Always quote the values.
What properties are immutable and force resource replacement?
domainName and vpcOptions are immutable. Changing either forces creation of a new domain.
Elasticsearch vs OpenSearch Differences
Why do OpenSearch domains still use 'es' in ARNs and IAM actions?
OpenSearch Service is the successor to Elasticsearch Service. For backward compatibility, ARNs use arn:aws:es:, IAM actions use es:* prefix, and the principal service remains es.amazonaws.com.
What's the difference between OpenSearch and Elasticsearch instance types?
OpenSearch instance types end in .search (e.g., t2.micro.search), while Elasticsearch types end in .elasticsearch (e.g., t2.micro.elasticsearch).
How do I format engine versions for OpenSearch vs Elasticsearch?
Use Elasticsearch_7.10 format (with prefix) for both OpenSearch and legacy Elasticsearch. Legacy Elasticsearch used 7.10 (version only), but OpenSearch requires the prefix.
What's the service-linked role name for OpenSearch?
Use AWSServiceRoleForAmazonOpenSearchService for OpenSearch, not AWSServiceRoleForAmazonElasticsearchService (which was for Elasticsearch).
VPC & Networking
How do I connect an OpenSearch domain to a VPC?
Configure vpcOptions with subnetIds and securityGroupIds. Create the AWSServiceRoleForAmazonOpenSearchService service-linked role first and use dependsOn to ensure proper ordering.
Can I change VPC settings after creating a domain?
No, vpcOptions is immutable. Adding or removing VPC configuration forces creation of a new domain.
Security & Fine-Grained Access Control
How do I enable fine-grained access control on an existing domain?
First create the domain with advancedSecurityOptions.enabled set to false but with all prerequisites configured. Then update to set enabled to true.
What are the prerequisites for fine-grained access control?
You must enable encryptAtRest, nodeToNodeEncryption, set enforceHttps to true, and use tlsSecurityPolicy of Policy-Min-TLS-1-2-2019-07 or higher.
Snapshots & Backups
Do I need to configure snapshot options?
No, snapshotOptions is deprecated. OpenSearch 5.3 and later automatically takes hourly snapshots. Earlier versions take daily snapshots.

Using a different cloud?

Explore analytics guides for other cloud providers: