Deploy with CDK

In this tutorial you will use the AWS Cloud Development Kit (CDK) to deploy an OpenSearch domain, create an OpenSearch UI Application, and automatically set up a sample workspace with dashboards from a single cdk deploy command.

Prerequisites

  • AWS CLI v2 configured with credentials
  • Node.js 18 or later
  • AWS CDK v2.110.0 or later (install with npm install -g aws-cdk)
  • Python 3.11 or later
  • Docker (used by CDK to bundle Lambda code)
  • An AWS account with admin permissions

Bootstrap CDK (one-time per account/region):

cdk bootstrap aws://YOUR_ACCOUNT_ID/us-east-1

Architecture Overview

The CDK stack creates these resources in order:

  1. IAM Role — Lambda execution role with opensearch and es permissions
  2. OpenSearch Domain — single-node r6g.large with 100GB GP3
  3. OpenSearch UI Application — connected to the domain
  4. Lambda Function — Python handler for dashboard automation
  5. Custom Resource — triggers Lambda after deployment

The Lambda generates 50 sample HTTP metrics, ingests them, creates a workspace, and builds a pie chart dashboard.

Clone and Install

git clone https://github.com/aws-samples/sample-automate-opensearch-ui-dashboards-deployment.git
cd sample-automate-opensearch-ui-dashboards-deployment/cdk
npm install

Repository structure:

cdk/
  bin/app.ts              # CDK app entry point
  lib/dashboard-stack.ts  # All AWS resource definitions
  package.json            # Node.js dependencies
lambda/
  dashboard_automation.py # Main Lambda handler
  sigv4_signer.py         # SigV4 signing utility
  requirements.txt        # Python dependencies

Preview

npx cdk diff -c masterUserArn=arn:aws:iam::YOUR_ACCOUNT_ID:role/YOUR_ROLE

Shows all resources that will be created without creating them.

Deploy

npx cdk deploy -c masterUserArn=arn:aws:iam::YOUR_ACCOUNT_ID:role/YOUR_ROLE

Type y to confirm. Takes 20-25 minutes (domain creation is the slow part).

Outputs include the OpenSearch UI endpoint, domain endpoint, workspace ID, and IDC status.

Understanding the Code

The main file is cdk/lib/dashboard-stack.ts.

Step 1 — IAM Role (created first because the UI app references its ARN):

const dashboardRole = new iam.Role(this, 'DashboardLambdaRole', {
  assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'),
  inlinePolicies: {
    OpenSearchAccess: new iam.PolicyDocument({
      statements: [
        new iam.PolicyStatement({
          actions: ['opensearch:ApplicationAccessAll'],
          resources: ['*']
        }),
        new iam.PolicyStatement({
          actions: ['es:ESHttpPost', 'es:ESHttpPut', 'es:ESHttpGet'],
          resources: [`arn:aws:es:${this.region}:${this.account}:domain/*`]
        })
      ]
    })
  }
});

Two permission sets: opensearch:ApplicationAccessAll for UI APIs and es:ESHttp* for domain data ingestion.

Step 2 — OpenSearch Domain:

const opensearchDomain = new opensearch.Domain(this, 'OpenSearchDomain', {
  version: opensearch.EngineVersion.OPENSEARCH_2_11,
  capacity: { dataNodes: 1, dataNodeInstanceType: 'r6g.large.search' },
  ebs: { enabled: true, volumeSize: 100, volumeType: ec2.EbsDeviceVolumeType.GP3 },
  encryptionAtRest: { enabled: true },
  nodeToNodeEncryption: true,
  enforceHttps: true,
});

Step 3 — OpenSearch UI Application:

const openSearchUI = new opensearch.CfnApplication(this, 'OpenSearchUI', {
  appConfigs: [
    { key: 'opensearchDashboards.dashboardAdmin.users', value: '["*"]' },
    { key: 'opensearchDashboards.dashboardAdmin.groups', value: `["${dashboardRole.roleArn}"]` }
  ],
  dataSources: [{ dataSourceArn: opensearchDomain.domainArn }],
  name: appName
});

CfnApplication maps directly to the CloudFormation AWS::OpenSearch::Application resource.

Step 4 — Lambda + Custom Resource:

const dashboardFn = new lambda.Function(this, 'DashboardSetup', {
  runtime: lambda.Runtime.PYTHON_3_11,
  handler: 'dashboard_automation.handler',
  code: lambda.Code.fromAsset('../lambda'),
  timeout: cdk.Duration.minutes(5),
  role: dashboardRole
});
 
new cdk.CustomResource(this, 'DashboardSetupResource', {
  serviceToken: provider.serviceToken,
  properties: {
    opensearchUIEndpoint: openSearchUIEndpoint,
    domainEndpoint: opensearchDomain.domainEndpoint,
    workspaceName: 'workspace-demo',
    region: this.region
  }
});

The Custom Resource tells CloudFormation to invoke the Lambda after all infrastructure is ready.

How the Lambda Works

The handler runs these steps sequentially:

  1. Finds the data source by calling GET /api/saved_objects/_find?type=data-source
  2. Creates workspace workspace-demo via POST /api/workspaces
  3. Generates 50 sample HTTP metrics and bulk-ingests them into the domain
  4. Creates an index pattern for application-metrics-*
  5. Creates a pie chart visualization for HTTP status code distribution
  6. Creates a dashboard containing the pie chart

All UI API calls use SigV4 signing with service name opensearch. Domain API calls use service name es.

Verify

  1. Open the OpenSearch Service console
  2. Click OpenSearch UI (Dashboards) in the left nav
  3. Click your application (app-demo)
  4. Switch to workspace-demo
  5. Open the Application Metrics dashboard — you should see a pie chart

Optional: VPC Access

npx cdk deploy -c masterUserArn=YOUR_ARN -c enableVpc=true

Creates a VPC, security group, and authorizes the VPC endpoint.

Optional: IAM Identity Center

npx cdk deploy -c masterUserArn=YOUR_ARN -c idcInstanceArn=arn:aws:sso:::instance/ssoins-YOUR_ID

Creates an IAM role for IDC and configures the app for SSO.

Troubleshooting

IssueFix
Data source not foundRetry deploy — timing issue with data source registration
ExpiredTokenRefresh credentials, then redeploy
Docker not foundInstall Docker or use --no-bundling
403 on presigned URLUse opensearch as SigV4 service name, not es

Cleanup

npx cdk destroy

Type y to confirm. Removes all resources. The r6g.large instance is the main cost — always destroy when done.

Next Steps