Create AWS OpenSearch Domains

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 FREE

Frequently Asked Questions

Elasticsearch vs OpenSearch Differences
Why do OpenSearch resources still use 'es' in ARNs and IAM actions?
For backward compatibility, both OpenSearch and Elasticsearch use the same naming: ARNs are prefaced with arn:aws:es:, IAM policy actions use es: prefix (like es:*), and assume role policies reference the Principal Service as es.amazonaws.com.
What's the difference between OpenSearch and Elasticsearch version formats?
OpenSearch uses 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.
How do OpenSearch and Elasticsearch instance types differ?
OpenSearch instance types end in .search (e.g., t2.micro.search), while Elasticsearch types end in .elasticsearch (e.g., t2.micro.elasticsearch).
What service-linked role does OpenSearch use?
OpenSearch uses AWSServiceRoleForAmazonOpenSearchService, while Elasticsearch uses AWSServiceRoleForAmazonElasticsearchService.
Configuration Issues & Perpetual Diffs
Why does my OpenSearch domain show a perpetual diff on every apply?
If advancedOptions values aren’t strings (wrapped in quotes), they cause perpetual diffs and force recreation. Always quote all advancedOptions values, even numbers and booleans.
Can I change VPC settings after creating my OpenSearch domain?
No, vpcOptions is immutable. Adding or removing VPC configuration forces a new resource (domain recreation). Plan VPC settings carefully before creation.
Should I configure snapshotOptions for my OpenSearch domain?
No, snapshotOptions is deprecated for OpenSearch 5.3 and later. Modern versions take hourly automated snapshots automatically, making this setting irrelevant.
Fine-Grained Access Control
Can I enable fine-grained access control when creating a new domain?
No, you must use a two-step process: create the domain with advancedSecurityOptions.enabled set to false, then update it to true in a second apply.
What are the prerequisites for enabling fine-grained access control?
You must enable encryptAtRest, nodeToNodeEncryption, and set domainEndpointOptions.enforceHttps to true with tlsSecurityPolicy of Policy-Min-TLS-1-2-2019-07 or higher.
VPC & Networking
How do I connect my OpenSearch domain to a VPC?
Configure vpcOptions with subnetIds and securityGroupIds. Ensure the service-linked role AWSServiceRoleForAmazonOpenSearchService exists first, using dependsOn if creating it in the same stack.
What properties are immutable after domain creation?
domainName and vpcOptions are immutable. Changing either forces a new resource (domain recreation).
Endpoints & IP Addressing
What's the difference between endpoint and endpointV2?
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: