Terraform: створення AWS OpenSearch Service cluster та юзерів

Автор |  15/09/2025
 

В першій частині розібрались з основами AWS OpenSearch Service взагалі, і з типами інстансів для Data Nodes – AWS: знайомство з OpenSearch Service в ролі vector store.

В другій – з доступами, AWS: створення OpenSearch Service cluster та налаштування аутентифікації і авторизації.

Тепер напишемо Terraform code для створення кластера, юзерів та індексів.

Створювати кластер будемо в VPC, для аутентифікації використаємо internal user database.

А в VPC не можна… Бо – suprize! – AWS Bedrock вимагає OpenSeach Managed кластер саме Public, а не в VPC.

The OpenSearch Managed Cluster you provided is not supported because it is VPC protected. Your cluster must be behind a public network.

Писав в сапорт, сказали, що:

However, there is an ongoing product feature request (PFR) to have Bedrock KnowledgeBases support provisioned Open Search clusters in VPC.

І пропонують використати Amazon OpenSearch Serverless, з якого ми власне і тікаємо, бо ціни дурні.

Друга проблема, яка виявилась, коли я почав писати ресурси bedrockagent_knowledge_base – це те, що він не підтримує storage_configuration з type == OPENSEARCH_MANAGED, тільки Serverless.

Але Pull Request на це вже є, колись, може, замержать.

Отже, будемо робити OpenSearch Managed Service кластер, кластер буде один, з трьома індексами – Dev/Staging/Prod.

В кластері буде три маленькі дата-ноди, а в кожному індексі – 1 primary shard та 1 репліка, бо проект маленький, даних в нашому Production індексі на AWS OpenSearch Serverless, з якого ми хочемо переїхати на AWS OpenSearch Service – зараз всього 2 GiB, і навряд чи в майбутньому буде дуже багато.

Було б добре кластер зробити у власному Terraform модулі аби простіше створювати якісь тестові оточення, як в мене це зроблено для AWS EKS – але поки не дуже є на це час, тому робимо просто tf-файлами з окремим prod.tfvars для змінних.

Може, потім напишу окремо по переносу у власний модуль, бо це дійсно зручно.

І в наступній частині – поговоримо про моніторинг, бо наш Production вже разок падав 🙂

Структура Terraform файлів

Початкова схема файлів і директорій проекту така:

$ tree .
.
├── README.md
└── terraform
    ├── Makefile
    ├── backend.tf
    ├── data.tf
    ├── envs
    │   └── prod
    │       └── prod.tfvars
    ├── locals.tf
    ├── outputs.tf
    ├── providers.tf
    ├── variables.tf
    └── versions.tf

В providers.tf – налаштування провайдерів, тут поки тільки AWS, і через нього задаємо дефолтні теги:

provider "aws" {
  region = var.aws_region
  default_tags {
    tags = {
      component   = var.component
      created-by  = "terraform"
      environment = var.environment
    }
  }
}

В data.tf збираємо дані AWS Account ID, Availability Zones, VPC та приватні subnets, в яких будемо створювати кластер в яких колись потім будемо створювати кластер:

data "aws_caller_identity" "current" {}

data "aws_availability_zones" "available" {
  state = "available"
}

data "aws_vpc" "eks_vpc" {
  id = var.vpc_id
}

data "aws_subnets" "private" {
  filter {
    name   = "vpc-id"
    values = [var.vpc_id]
  }

  tags = {
    subnet-type = "private"
  }
}

Файл variables.tf з нашими дефолтними змінними, потім будемо додавати нові:

variable "aws_region" {
  type    = string
}

variable "project_name" {
  description = "A project name to be used in resources"
  type        = string
}

variable "component" {
  description = "A team using this project (backend, web, ios, data, devops)"
  type        = string
}

variable "environment" {
  description = "Dev/Prod, will be used in AWS resources Name tag, and resources names"
  type        = string
}

variable "vpc_id" {
  type        = string
  description = "A VPC ID to be used to create OpenSearch cluster and its Nodes"
}

Значення змінних передаємо через окремий prod.tfvars, потім, при потребі, можна буде створити нове оточення через файл типу envs/test/test.tfvars:

aws_region = "us-east-1"
project_name = "atlas-kb"
component = "backend"
environment = "prod"
vpc_id = "vpc-0fbaffe234c0d81ea"
dns_zone = "prod.example.co"

В Makefile – спрощуємо собі локальне життя:

############
### PROD ###
############

init-prod:
  terraform init -reconfigure -backend-config="key=prod/atlas-knowledge-base-prod.tfstate"

plan-prod:
  terraform plan -var-file=envs/prod/prod.tfvars

apply-prod:
  terraform apply -var-file=envs/prod/prod.tfvars

#destroy-prod:
#	terraform destroy -var-file=envs/prod/prod.tfvars

Які файли будуть далі?

У нас тут ще буде AWS Bedrock, якому треба буде налаштувати доступ – аде це зробимо через його IAM Role, і про Bedrock тут писати не буду – бо і тема окрема, і в Terraform поки що нема підтримки OPENSEARCH_MANAGED, тому ми зробили його руками, а потім виконаємо terraform import.

Індекси, юзерів для нашого Backend API та Bedrock IAM Role mappings будемо робити в internal database самого OpenSearch через Terraform OpenSearch Provider аби не морочитись з доступами до дашборди.

Планування проекту

Кластер можемо зробити просто з ресурсу aws_opensearch_domain.

А можна взяти готові модулі, наприклад opensearch від @Anton Babenko.

Давайте візьмемо модуль Антона, бо я багато де його модулі використовую, в принципі все працює чудово.

Створення кластера

Приклади – terraform-aws-opensearch/tree/master/examples.

До variables.tf додаємо змінну з параметрами кластеру:

...

variable "cluser_options" {
  description = "A map of options to configure the OpenSearch cluster"
  type = object({
    instance_type                 = string
    instance_count                = number
    volume_size                   = number
    volume_type                   = string
    engine_version                = string
    auto_software_update_enabled  = bool
  })
}

І значення в prod.tfvars:

...

cluser_options = {
  instance_type                = "t3.small.search"
  instance_count               = 3
  volume_size                  = 50
  volume_type                  = "gp3"
  engine_version               = "OpenSearch_2.19"
  auto_software_update_enabled = true
}

Інстанси t3.small.search – самі мінімальні, нам цього поки що вистачить, хоча для t3 є обмеження – наприклад не підтримується Auto-tune.

Ну і взагалі t3 не для Production use case. Див. також Operational best practices for Amazon OpenSearch Service, Current generation instance types і Amazon OpenSearch Service quotas.

Версію тут я задавав 2.9, але буквально на днях додали 3.1 – див. Supported versions of Elasticsearch and OpenSearch.

Беремо три ноди, аби кластер міг вибрати cluster manager node, якщо одна нода впаде, див. Dedicated master node distribution, Learning OpenSearch from scratch, part 2: Digging deeper і Enhance stability with dedicated cluster manager nodes using Amazon OpenSearch Service.

Зміст locals.tf:

locals {
  # 'atlas-kb-prod'
  env_name = "${var.project_name}-${var.environment}"
}

Більша частина locals буде саме тут, але деякі, які зовсім вже “локальні” до якогось коду – будуть у файлах з кодом ресурсів.

Додаємо файл opensearcth_users.tf – поки тут тільки рутовий юзер, пароль зберігаємо в AWS Parameter Store (замість AWS Secrets Manager – “так історично склалося”):

############
### ROOT ###
############

# generate root password
# waiting for write-only: https://github.com/hashicorp/terraform-provider-aws/pull/43621
# then will update it with the ephemeral type
resource "random_password" "os_master_password" {
  length  = 16
  special = true
}

# store the root password in AWS Parameter Store
resource "aws_ssm_parameter" "os_master_password" {
  name        = "/${var.environment}/${local.env_name}-root-password"
  description = "OpenSearch cluster master password"
  type        = "SecureString"
  value       = random_password.os_master_password.result
  overwrite   = true
  tier        = "Standard"

  lifecycle {
    ignore_changes = [value]  # to prevent diff every time password is regenerated
  }
}

data "aws_ssm_parameter" "os_master_password" {
  name            = "/${var.environment}/${local.env_name}-root-password"
  with_decryption = true

  depends_on = [aws_ssm_parameter.os_master_password]
}

Пишемо файл opensearch_cluster.tf.

Я тут залишив конфіг для VPC, і на майбутнє, і просто для прикладу, хоча перенести вже створений кластер у VPC не можна буде – доведеться створювати новий, див. Limitations в документації Launching your Amazon OpenSearch Service domains within a VPC:

module "opensearch" {
  source  = "terraform-aws-modules/opensearch/aws"
  version = "~> 2.0.0"  

  # enable Fine-grained access control
  # by using the internal user database, we'll simply access to the Dashboards
  # for backend API Kubernetes Pods, will use Kubernetes Secrets with username:password from AWS Parameter Store
  advanced_security_options = {
    enabled                        = true
    anonymous_auth_enabled         = false
    internal_user_database_enabled = true

    master_user_options = {
      master_user_name     = "os_root"
      master_user_password = data.aws_ssm_parameter.os_master_password.value
    }
  }

  # can't be used with t3 instances
  auto_tune_options = {
    desired_state = "DISABLED"
  }

  # have three data nodes - t3.small.search nodes in two AZs
  # will use 3 indexes - dev/stage/prod with 1 shard and 1 replica each
  cluster_config = {
    instance_count           = var.cluser_options.instance_count
    dedicated_master_enabled = false
    instance_type            = var.cluser_options.instance_type

    # put both data-nodes in different AZs
    zone_awareness_config = {
      availability_zone_count = 2
    }

    zone_awareness_enabled = true
  }

  # the cluster's name
  # 'atlas-kb-prod'
  domain_name = "${local.env_name}-cluster"

  # 50 GiB for each Data Node
  ebs_options = {
    ebs_enabled = true
    volume_type = var.cluser_options.volume_type
    volume_size = var.cluser_options.volume_size
  }

  encrypt_at_rest = {
    enabled = true
  }

  # latest for today:
  # https://docs.aws.amazon.com/opensearch-service/latest/developerguide/what-is.html#choosing-version
  engine_version = var.cluser_options.engine_version

  # enable CloudWatch logs for Index and Search slow logs
  # TODO: collect to VictoriaLogs or Loki, and create metrics and alerts
  log_publishing_options = [
    { log_type = "INDEX_SLOW_LOGS" },
    { log_type = "SEARCH_SLOW_LOGS" },
  ]

  ip_address_type = "ipv4"

  node_to_node_encryption = {
    enabled = true
  }

  # allow minor version updates automatically
  # will be performed during off-peak windows
  software_update_options = {
    auto_software_update_enabled = var.cluser_options.auto_software_update_enabled
  }

  # DO NOT use 'atlas-vpc-ops' VPC and its private subnets
  # > "The OpenSearch Managed Cluster you provided is not supported because it is VPC protected. Your cluster must be behind a public network."
  # vpc_options = {
  #   subnet_ids = data.aws_subnets.private.ids
  # }

  # # VPC endpoint to access from Kubernetes Pods
  # vpc_endpoints = {
  #   one = {
  #     subnet_ids = data.aws_subnets.private.ids
  #   }
  # }

  # Security Group rules to allow access from the VPC only
  # security_group_rules = {
  #   ingress_443 = {
  #     type        = "ingress"
  #     description = "HTTPS access from VPC"
  #     from_port   = 443
  #     to_port     = 443
  #     ip_protocol = "tcp"
  #     cidr_ipv4   = data.aws_vpc.ops_vpc.cidr_block
  #   }
  # }

  # Access policy
  # necessary to allow access for AWS user to the Dashboards
  access_policy_statements = [
    {
      effect = "Allow"

      principals = [{
        type        = "*"
        identifiers = ["*"]
      }]

      actions = ["es:*"]
    }
  ]

  # 'atlas-kb-ops-os-cluster'
  tags = {
    Name        = "${var.project_name}-${var.environment}-os-cluster"
  }
}

В принципі, тут все в коментах описано, але кратко:

  • включаємо fine-grained access control і локальну базу юзерів
  • три дата-ноди, кожна з 50 гіг дисків, в різних Availability Zones
  • включаємо логи в CloudWatch
  • кластер робимо в приватних сабнетах
  • в Domain Access Policy дозволяємо доступ для всіх
    • ну – поки так… Security Groups ми використати не можемо, бо не в VPC, а створити IP-Based policy – як? ми ж не знаємо CIDR Bedrock
    • в принципі, тут в principals.identifiers можна додати ліміт на наших IAM Users + Bedrock AIM Role, бо вона буде одна

Запускаємо створення кластера і йдемо пити чай.

Налаштування Custom endpoint

Після створення кластеру перевіряємо доступ до дашборди, якщо все ОК – то додаємо Custom endpoint.

Note: з Custom endpoint свої приколи: в Terraform OpenSearch Provider треба використовувати саме Custom endpoint URL, але в AWS Bedrock Knowledge Base – дефолтний URL кластеру

Для цього нам треба зробити сертифікат в AWS Certificate Manager і додати новий запис в Route53.

Я тут очікував можливу проблему куриця і яйця, бо налаштування Custom Endpoint залежать від AWS ACM і запису в AWS Route53, а запис в AWS Route53 буде залежати від кластеру – бо використовує його ендпоінт.

Але ні, якщо робити новий кластер з налаштуваннями, які описав нижче – все нормально створюється: спочатку сертифікат в AWS ACM, потім кластер з Custom Endpoint, потім запис в Route53 з CNAME на cluster default URL.

Додаємо нову localos_custom_domain_name:

locals {
  # 'atlas-kb-prod'
  env_name = "${var.project_name}-${var.environment}"
  # 'opensearch.prod.example.co'
  os_custom_domain_name = "opensearch.${var.dns_zone}"
}

Додаємо отримання даних про Route53 зону до data.tf:

...

data "aws_route53_zone" "zone" {
  name = var.dns_zone
}

Додаємо створення сертифіката і запис у Route53 до opensearch_cluster.tf:

# TLS for the Custom Domain
module "prod_opensearch_acm" {
  source  = "terraform-aws-modules/acm/aws"
  version = "~> 6.0"

  # 'opensearch.example.co'
  domain_name = local.os_custom_domain_name
  zone_id     = data.aws_route53_zone.zone.zone_id

  validation_method = "DNS"
  wait_for_validation = true

  tags = {
    Name = local.os_custom_domain_name
  }
}

resource "aws_route53_record" "opensearch_domain_endpoint" {
  zone_id = data.aws_route53_zone.zone.zone_id
  name    = local.os_custom_domain_name
  type    = "CNAME"
  ttl     = 300
  records = [module.opensearch.domain_endpoint]
}

...

І в module "opensearch" додаємо налаштування custom ендпоінту:

...
  domain_endpoint_options = {
    custom_endpoint_certificate_arn = module.prod_opensearch_acm.acm_certificate_arn
    custom_endpoint_enabled         = true
    custom_endpoint                 = local.os_custom_domain_name
    tls_security_policy             = "Policy-Min-TLS-1-2-2019-07"
  }
...

Виконуємо terrform init та terrform apply, перевіряємо налаштування:

І перевіряємо доступ до дашборд.

Terraform Outputs

Додамо трохи аутуптів.

Поки просто для себе, потім, можливо, будемо використовувати в імпортах інших проектів, див. Terraform: terraform_remote_state – отримання outputs інших state-файлів:

output "vpc_id" {
  value = var.vpc_id
}

output "cluster_arn" {
  value = module.opensearch.domain_arn
}

output "opensearch_domain_endpoint_cluster" {
  value = "https://${module.opensearch.domain_endpoint}"
}

output "opensearch_domain_endpoint_custom" {
  value = "https://${local.os_custom_domain_name}"
}

output "opensearch_root_username" {
  value = "os_root"
}

output "opensearch_root_user_password_secret_name" {
  value = "/${var.environment}/${local.env_name}-root-password"
}

Створення OpenSearch Users

Власне, що нам залишилось – це користувачі і індекси.

Юзерів у нас буде два типи:

  • звичайні юзери з OpenSearch internal database – для нашого Backend API в Kubernetes (насправді, потім ми все ж перейшли на IAM Roles, які мапляться в поди Backend через EKS Pod Identities)
  • і юзери (IAM Role) для Bedrock – там буде три Knowledge Bases, кожна зі своєю IAM Role, для якої треба буде додати OpenSearch Role і зробити mapping на IAM-ролі

Почнемо зі звичайних юзерів.

Додаємо провайдера, в мене це у файлі versions.tf:

terraform {

  required_version = "~> 1.6"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
    opensearch = {
      source  = "opensearch-project/opensearch"
      version = "~> 2.3"
    }
  }
}

В файлі providers.tf описуємо доступ до кластеру:

...

provider "opensearch" {
  url         = "https://${local.os_custom_domain_name}"
  username    = "os_root"
  password    = data.aws_ssm_parameter.os_master_password.value
  healthcheck = false
}

Error: elastic: Error 403 (Forbidden)

Тут важливий момент з url в конфігурації провайдеру, писав про це вище, тепер – як воно виглядає.

Спершу в provider.url задав як outputs модуля, тобто module.opensearch.domain_endpoint.

І через це ловив 403, коли намагався створити юзерів:

...
opensearch_user.os_kraken_dev_user: Creating...
opensearch_role.os_kraken_dev_role: Creating...
╷
│ Error: elastic: Error 403 (Forbidden)
│ 
│   with opensearch_user.os_kraken_dev_user,
│   on opensearch_users.tf line 23, in resource "opensearch_user" "os_kraken_dev_user":
│   23: resource "opensearch_user" "os_kraken_dev_user" {
│ 
╵
╷
│ Error: elastic: Error 403 (Forbidden)
│ 
│   with opensearch_role.os_kraken_dev_role,
│   on opensearch_users.tf line 30, in resource "opensearch_role" "os_kraken_dev_role":
│   30: resource "opensearch_role" "os_kraken_dev_role" {

Власне, задаємо URL саме у вигляді FQDN, який робили для Custom Endpoint, щось типу "url = https://opensearch.exmaple.com" – і з ним все працює.

Створення Internal юзерів

Тепер самі юзери.

Їх буде три – dev, staging, prod, кожен з доступом до відповідного індексу.

Тут використаємо opensearch_user.

Якщо кластер всеж створений в VPC – то потрібен підключений VPN, аби провайдер зміг підключитись до кластеру.

До variables.tf додаємо list() зі списком оточень:

...

variable "app_environments" {
  type        = list(string)
  description = "The Application's environments, to be used to created Dev/Staging/Prod DynamoDB tables, etc"
}

І значення в prod.tfvars:

...

app_environments = [
  "dev",
  "staging",
  "prod"
]

Internal database users

Спершу я планував просто використовувати локальних юзерів, і в цей пост записав такий варіант – нехай буде. Далі покажу, як все ж зробили потім – з IAM Users та IAM Roles.

У файлі opensearch_users.tf додаємо в циклах три паролі, трьох юзерів, і три ролі, на які мапимо юзерів – кожна роль з доступом до власного індексу:

...

##############
### KRAKEN ###
##############

resource "random_password" "os_kraken_password" {
  for_each = toset(var.app_environments)
  length  = 16
  special = true
}

# store the root password in AWS Parameter Store
resource "aws_ssm_parameter" "os_kraken_password" {
  for_each = toset(var.app_environments)

  name        = "/${var.environment}/${local.env_name}-kraken-${each.key}-password"
  description = "OpenSearch cluster Backend Dev password"
  type        = "SecureString"
  value       = random_password.os_kraken_password[each.key].result
  overwrite   = true
  tier        = "Standard"

  lifecycle {
    ignore_changes = [value]  # to prevent diff every time password is regenerated
  }
}

# Create a user
resource "opensearch_user" "os_kraken_user" {
  for_each = toset(var.app_environments)

  username    = "os_kraken_${each.key}"
  password    = random_password.os_kraken_password[each.key].result
  description = "Backend EKS ${each.key} user"

  depends_on = [module.opensearch]
}

# And a full user, role and role mapping example:
resource "opensearch_role" "os_kraken_role" {
  for_each = toset(var.app_environments)

  role_name   = "os_kraken_${each.key}_role"
  description = "Backend EKS ${each.key} role"

  cluster_permissions = [
    "indices:data/read/msearch",
    "indices:data/write/bulk*",
   "indices:data/read/mget*"
  ]
  index_permissions {
    index_patterns  = ["kraken-kb-index-${each.key}"]
    allowed_actions = ["*"]
  }

  depends_on = [module.opensearch]
}

В cluster_permissions додаємо дозволи, які потрібні і для index level, і для cluster level, бо Bedrock без них не працював, див. Cluster wide index permissions.

Деплоїмо, перевіряємо в Dashboards:

Додавання IAM Users

Тут ідея така сама, просто замість звичайних юзерів з логіном:паролем для аутентифікації використовується IAM та його Users && Roles.

Про роль для Bedrock далі, а зараз додамо мапінг юзерів.

Що нам треба – це взяти список наших Backend team юзерів, дати їм IAM Policy з доступом до OpenSearch, а потім в OpnSearch internal users database додати мапінг на локальну роль.

Локальну роль поки можна взяти all_access, хоча краще потім все ж написати власну. Див. Predefined roles та About the master user.

Додаємо нову змінну в variables.tf:

...

variable "backend_team_users_arns" {
  type        = list(string)
}

Її значення в prod.tfvars:

...

backend_team_users_arns = [
  "arn:aws:iam::492***148:user/arseny",
  "arn:aws:iam::492***148:user/misha",
  "arn:aws:iam::492***148:user/oleksii",
  "arn:aws:iam::492***148:user/vladimir",
  "os_root"
]

Тут довелося костиляти з юзером os_root, бо інакше його випилює з ролі.

Тому таки краще зробити нормальні ролі – але для MVP міжна і так.

І додаємо мапінг цих IAM Users до ролі all_access:

...

####################
### BACKEND TEAM ###
####################

resource "opensearch_roles_mapping" "all_access_mapping" {
  role_name = "all_access"

  users = var.backend_team_users_arns
}

Деплоїмо, перевіряємо роль all_access:

Note: ChatGPT вперто казав додавати IAM Users в Backend Roles, але ні, і це явно вказано в документації – додавати треба в Users, див. Additional master users.

І всім IAM Users треба додати IAM-політику з доступом.

Знов-таки для MVP можна просто взяти голову policy AmazonOpenSearchServiceFullAccess, яка підключена до IAM Group:

Створення AWS Bedrock IAM Roles та OpenSearch Role mappings

Bedrock у нас вже є, треба просто створити нові IAM Roles і замапити їх до OpenSeach Roles.

Додаємо файл iam.tf – описуємо IAM Role та IAM Policy (Identity-based Policy для доступу до OpenSearch), тут також в циклі по кожному з var.app_environmetns:

#####################################
### MAIN ROLE FOR KNOWLEDGE BASE ###
#####################################

# grants permissions for AWS Bedrock to interact with other AWS services
resource "aws_iam_role" "knowledge_base_role" {
  for_each = toset(var.app_environments)
  name     = "${var.project_name}-role-${each.key}-managed"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "bedrock.amazonaws.com"
        }
        Condition = {
          StringEquals = {
            "aws:SourceAccount" = data.aws_caller_identity.current.account_id
          }
          ArnLike = {
            # restricts the role to be assumed only by Bedrock knowledge base in the specified region
            "aws:SourceArn" = "arn:aws:bedrock:${var.aws_region}:${data.aws_caller_identity.current.account_id}:knowledge-base/*"
          }
        }
      }
    ]
  })
}

# IAM policy for Knowledge Base to access OpenSearch Managed
resource "aws_iam_policy" "knowledge_base_opensearch_policy" {
  for_each = toset(var.app_environments)
  name     = "${var.project_name}-kb-opensearch-policy-${each.key}-managed"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "es:*",
        ]
        Resource = [
          module.opensearch.domain_arn,
          "${module.opensearch.domain_arn}/*"
        ]
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "knowledge_base_opensearch" {
  for_each   = toset(var.app_environments)
  role       = aws_iam_role.knowledge_base_role[each.key].name
  policy_arn = aws_iam_policy.knowledge_base_opensearch_policy[each.key].arn
}

Далі в opensearch_users.tf створимо:

  • opensearch_role: з cluster_permissions та index_permissions на кожен індекс
  • locals з усіма IAM Roles, які створили вище
  • і opensearch_roles_mapping для кожної opensearch_role.os_bedrock_roles, які через backend_roles додаємо до кожної opensearch_role

Виглядає якось так:

...

#################
#### BEDROCK ####
#################

resource "opensearch_role" "os_bedrock_roles" {
  for_each = toset(var.app_environments)
  role_name   = "os_bedrock_${each.key}_role"
  description = "Backend Bedrock KB ${each.key} role"

  cluster_permissions = [
    "indices:data/read/msearch",
    "indices:data/write/bulk*",
    "indices:data/read/mget*"
    ]

  index_permissions {
    index_patterns  = ["kraken-kb-index-${each.key}"]
    allowed_actions = ["*"]
  }

  depends_on = [module.opensearch]
}

# 'aws_iam_role' is defined in iam.tf
locals {
  knowledge_base_role_arns = {
    for env, role in aws_iam_role.knowledge_base_role :
    env => role.arn
  }
}

resource "opensearch_roles_mapping" "os_bedrock_role_mappings" {
  for_each  = toset(var.app_environments)
  role_name = opensearch_role.os_bedrock_roles[each.key].role_name

  backend_roles = [
    local.knowledge_base_role_arns[each.key]
  ]

  depends_on = [module.opensearch]
}

Власне, саме тут зіткнулись з помилками доступу Bedrock, через які довелось додавати cluster_permissions:

The knowledge base storage configuration provided is invalid… Request failed: [security_exception] no permissions for [indices:data/read/msearch] and User [name=arn:aws:iam::492***148:role/kraken-kb-role-dev, backend_roles=[arn:aws:iam::492***148:role/kraken-kb-role-dev], requestedTenant=null]

Деплоїмо, перевіряємо:

Створення OpenSearch індексів

Провайдер вже є, ресурс беремо opensearch_index.

В locals записуємо шаблон індексу – я його просто взяв у девелоперів зі старого конфігу:

locals {
  # 'atlas-kb-prod'
  env_name = "${var.project_name}-${var.environment}"
  # 'opensearch.prod.example.co'
  os_custom_domain_name = "opensearch.${var.dns_zone}"

  # index mappings

  os_index_mappings = <<-EOF
    {
      "dynamic_templates": [
        {
          "strings": {
            "match_mapping_type": "string",
            "mapping": {
              "fields": {
                "keyword": {
                  "ignore_above": 8192,
                  "type": "keyword"
                }
              },
              "type": "text"
            }
          }
        }
      ],
      "properties": {
        "bedrock-knowledge-base-default-vector": {
          "type": "knn_vector",
          "dimension": 1024,
          "method": {
            "name": "hnsw",
            "engine": "faiss",
            "parameters": {
              "m": 16,
              "ef_construction": 512
            },
            "space_type": "l2"
          }
        },
        "AMAZON_BEDROCK_METADATA": {
          "type": "text",
          "index": false
        },
        "AMAZON_BEDROCK_TEXT_CHUNK": {
          "type": "text",
          "index": true
        }
      }
    }
EOF
}

Створюємо файл opensearch_indexes.tf. І додаємо сам індекси – тут я все ж вирішив без циклу, прямо створити окремі Dev/Staging/Prod:

# Dev Index
resource "opensearch_index" "kb_vector_index_dev" {
  name = "kraken-kb-index-dev"
  
  # enable approximate nearest neighbor search by setting index_knn to true
  index_knn                      = true
  index_knn_algo_param_ef_search = "512"
  number_of_shards               = "1"
  number_of_replicas = "1"
  mappings                       = local.os_index_mappings

  # When new documents are ingested into the Knowledge Base,
  # OpenSearch automatically creates field mappings for new metadata fields under
  # AMAZON_BEDROCK_METADATA. Since these fields are created outside of TF resource definitions,
  # TF detects them as configuration drift and attempts to recreate the index to match its
  # known state.
  #
  # This lifecycle rule prevents unnecessary index recreation by ignoring mapping changes
  # that occur after initial deployment.
  lifecycle {
    ignore_changes = [mappings]
  }
}

...

Деплоїмо і перевіряємо:

 

Власне, на цьому і все.

Bedrock вже підключили, все працює.

Але трохи погемороїтись довелось.

І впевнений, що не останній раз 🙂