mirror of
https://github.com/ghndrx/terraform-foundation.git
synced 2026-02-10 06:45:06 +00:00
Enterprise-grade multi-tenant AWS cloud foundation. Modules: - GitHub OIDC for keyless CI/CD authentication - IAM account settings and security baseline - AWS Config Rules for compliance - ABAC (Attribute-Based Access Control) - SCPs (Service Control Policies) Features: - Multi-account architecture - Cost optimization patterns - Security best practices - Comprehensive documentation Tech: Terraform, AWS Organizations, IAM Identity Center
459 lines
13 KiB
HCL
459 lines
13 KiB
HCL
################################################################################
|
|
# Workload: OpenSearch (Elasticsearch)
|
|
#
|
|
# Search and analytics with:
|
|
# - Serverless or provisioned clusters
|
|
# - Fine-grained access control
|
|
# - VPC or public access
|
|
# - Cognito authentication
|
|
# - UltraWarm for cost-effective storage
|
|
# - Cross-cluster search
|
|
#
|
|
# Use cases: Log analytics, full-text search, observability
|
|
################################################################################
|
|
|
|
terraform {
|
|
required_version = ">= 1.5"
|
|
|
|
required_providers {
|
|
aws = {
|
|
source = "hashicorp/aws"
|
|
version = ">= 5.0"
|
|
}
|
|
}
|
|
|
|
backend "s3" {
|
|
key = "05-workloads/<TENANT>-<NAME>-opensearch/terraform.tfstate"
|
|
}
|
|
}
|
|
|
|
################################################################################
|
|
# Configuration - UPDATE THESE
|
|
################################################################################
|
|
|
|
locals {
|
|
# Naming
|
|
tenant = "<TENANT>"
|
|
name = "<NAME>"
|
|
env = "prod"
|
|
|
|
domain_name = "${local.tenant}-${local.name}-${local.env}"
|
|
|
|
# Engine
|
|
engine_version = "OpenSearch_2.11"
|
|
|
|
# Cluster sizing
|
|
cluster = {
|
|
# Data nodes
|
|
instance_type = "t3.medium.search" # t3.small.search for dev
|
|
instance_count = 2
|
|
|
|
# Dedicated master nodes (recommended for production)
|
|
dedicated_master_enabled = local.env == "prod"
|
|
dedicated_master_type = "t3.medium.search"
|
|
dedicated_master_count = 3
|
|
|
|
# Multi-AZ
|
|
zone_awareness_enabled = local.env == "prod"
|
|
availability_zone_count = local.env == "prod" ? 2 : 1
|
|
}
|
|
|
|
# Storage
|
|
storage = {
|
|
type = "gp3"
|
|
size_gb = 100
|
|
iops = 3000
|
|
throughput = 125
|
|
}
|
|
|
|
# UltraWarm (cost-effective warm storage)
|
|
ultrawarm = {
|
|
enabled = false
|
|
type = "ultrawarm1.medium.search"
|
|
count = 2
|
|
}
|
|
|
|
# Network
|
|
# Option 1: VPC (private, more secure)
|
|
vpc_enabled = true
|
|
vpc_id = "" # data.terraform_remote_state.network.outputs.vpc_id
|
|
private_subnet_ids = [] # data.terraform_remote_state.network.outputs.private_subnet_ids
|
|
|
|
# Option 2: Public (set vpc_enabled = false)
|
|
# Uses IP-based access policy
|
|
|
|
# Access control
|
|
enable_fine_grained_access = true
|
|
master_user_name = "admin"
|
|
|
|
# Cognito authentication (optional, for Dashboards)
|
|
cognito = {
|
|
enabled = false
|
|
user_pool_id = ""
|
|
identity_pool_id = ""
|
|
role_arn = ""
|
|
}
|
|
|
|
# Encryption
|
|
encrypt_at_rest = true
|
|
node_to_node_encryption = true
|
|
|
|
# Logging
|
|
log_types = ["INDEX_SLOW_LOGS", "SEARCH_SLOW_LOGS", "ES_APPLICATION_LOGS"]
|
|
|
|
# Auto-tune
|
|
auto_tune_enabled = true
|
|
}
|
|
|
|
################################################################################
|
|
# Variables
|
|
################################################################################
|
|
|
|
variable "region" {
|
|
type = string
|
|
default = "us-east-1"
|
|
}
|
|
|
|
variable "state_bucket" {
|
|
type = string
|
|
}
|
|
|
|
################################################################################
|
|
# Provider
|
|
################################################################################
|
|
|
|
provider "aws" {
|
|
region = var.region
|
|
|
|
default_tags {
|
|
tags = {
|
|
Tenant = local.tenant
|
|
App = local.name
|
|
Environment = local.env
|
|
ManagedBy = "terraform"
|
|
}
|
|
}
|
|
}
|
|
|
|
################################################################################
|
|
# Data Sources
|
|
################################################################################
|
|
|
|
data "aws_caller_identity" "current" {}
|
|
data "aws_region" "current" {}
|
|
|
|
################################################################################
|
|
# Random Password for Master User
|
|
################################################################################
|
|
|
|
resource "random_password" "master" {
|
|
count = local.enable_fine_grained_access ? 1 : 0
|
|
length = 24
|
|
special = true
|
|
override_special = "!#$%&*()-_=+[]{}<>:?"
|
|
}
|
|
|
|
################################################################################
|
|
# Secrets Manager
|
|
################################################################################
|
|
|
|
resource "aws_secretsmanager_secret" "opensearch" {
|
|
count = local.enable_fine_grained_access ? 1 : 0
|
|
name = "${local.tenant}/${local.env}/${local.name}/opensearch"
|
|
description = "OpenSearch master credentials"
|
|
|
|
tags = { Name = "${local.domain_name}-credentials" }
|
|
}
|
|
|
|
resource "aws_secretsmanager_secret_version" "opensearch" {
|
|
count = local.enable_fine_grained_access ? 1 : 0
|
|
secret_id = aws_secretsmanager_secret.opensearch[0].id
|
|
secret_string = jsonencode({
|
|
username = local.master_user_name
|
|
password = random_password.master[0].result
|
|
endpoint = aws_opensearch_domain.main.endpoint
|
|
})
|
|
}
|
|
|
|
################################################################################
|
|
# CloudWatch Log Groups
|
|
################################################################################
|
|
|
|
resource "aws_cloudwatch_log_group" "opensearch" {
|
|
for_each = toset(local.log_types)
|
|
name = "/aws/opensearch/${local.domain_name}/${lower(each.key)}"
|
|
retention_in_days = 30
|
|
|
|
tags = { Name = "${local.domain_name}-${lower(each.key)}" }
|
|
}
|
|
|
|
resource "aws_cloudwatch_log_resource_policy" "opensearch" {
|
|
policy_name = "${local.domain_name}-logs"
|
|
|
|
policy_document = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [{
|
|
Effect = "Allow"
|
|
Principal = {
|
|
Service = "es.amazonaws.com"
|
|
}
|
|
Action = [
|
|
"logs:PutLogEvents",
|
|
"logs:CreateLogStream"
|
|
]
|
|
Resource = "arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/opensearch/${local.domain_name}/*"
|
|
}]
|
|
})
|
|
}
|
|
|
|
################################################################################
|
|
# Security Group (VPC mode)
|
|
################################################################################
|
|
|
|
resource "aws_security_group" "opensearch" {
|
|
count = local.vpc_enabled && length(local.vpc_id) > 0 ? 1 : 0
|
|
name = "${local.domain_name}-opensearch"
|
|
vpc_id = local.vpc_id
|
|
|
|
ingress {
|
|
description = "HTTPS from VPC"
|
|
from_port = 443
|
|
to_port = 443
|
|
protocol = "tcp"
|
|
cidr_blocks = ["10.0.0.0/8"]
|
|
}
|
|
|
|
egress {
|
|
description = "All outbound"
|
|
from_port = 0
|
|
to_port = 0
|
|
protocol = "-1"
|
|
cidr_blocks = ["0.0.0.0/0"]
|
|
}
|
|
|
|
tags = { Name = "${local.domain_name}-opensearch" }
|
|
}
|
|
|
|
################################################################################
|
|
# IAM Service-Linked Role
|
|
################################################################################
|
|
|
|
resource "aws_iam_service_linked_role" "opensearch" {
|
|
count = local.vpc_enabled ? 1 : 0
|
|
aws_service_name = "opensearchservice.amazonaws.com"
|
|
}
|
|
|
|
################################################################################
|
|
# OpenSearch Domain
|
|
################################################################################
|
|
|
|
resource "aws_opensearch_domain" "main" {
|
|
domain_name = local.domain_name
|
|
engine_version = local.engine_version
|
|
|
|
# Cluster configuration
|
|
cluster_config {
|
|
instance_type = local.cluster.instance_type
|
|
instance_count = local.cluster.instance_count
|
|
|
|
dedicated_master_enabled = local.cluster.dedicated_master_enabled
|
|
dedicated_master_type = local.cluster.dedicated_master_enabled ? local.cluster.dedicated_master_type : null
|
|
dedicated_master_count = local.cluster.dedicated_master_enabled ? local.cluster.dedicated_master_count : null
|
|
|
|
zone_awareness_enabled = local.cluster.zone_awareness_enabled
|
|
|
|
dynamic "zone_awareness_config" {
|
|
for_each = local.cluster.zone_awareness_enabled ? [1] : []
|
|
content {
|
|
availability_zone_count = local.cluster.availability_zone_count
|
|
}
|
|
}
|
|
|
|
# UltraWarm
|
|
warm_enabled = local.ultrawarm.enabled
|
|
warm_type = local.ultrawarm.enabled ? local.ultrawarm.type : null
|
|
warm_count = local.ultrawarm.enabled ? local.ultrawarm.count : null
|
|
}
|
|
|
|
# Storage
|
|
ebs_options {
|
|
ebs_enabled = true
|
|
volume_type = local.storage.type
|
|
volume_size = local.storage.size_gb
|
|
iops = local.storage.type == "gp3" ? local.storage.iops : null
|
|
throughput = local.storage.type == "gp3" ? local.storage.throughput : null
|
|
}
|
|
|
|
# VPC configuration
|
|
dynamic "vpc_options" {
|
|
for_each = local.vpc_enabled && length(local.private_subnet_ids) > 0 ? [1] : []
|
|
content {
|
|
subnet_ids = slice(local.private_subnet_ids, 0, local.cluster.availability_zone_count)
|
|
security_group_ids = [aws_security_group.opensearch[0].id]
|
|
}
|
|
}
|
|
|
|
# Encryption
|
|
encrypt_at_rest {
|
|
enabled = local.encrypt_at_rest
|
|
}
|
|
|
|
node_to_node_encryption {
|
|
enabled = local.node_to_node_encryption
|
|
}
|
|
|
|
domain_endpoint_options {
|
|
enforce_https = true
|
|
tls_security_policy = "Policy-Min-TLS-1-2-2019-07"
|
|
}
|
|
|
|
# Fine-grained access control
|
|
advanced_security_options {
|
|
enabled = local.enable_fine_grained_access
|
|
internal_user_database_enabled = local.enable_fine_grained_access
|
|
|
|
dynamic "master_user_options" {
|
|
for_each = local.enable_fine_grained_access ? [1] : []
|
|
content {
|
|
master_user_name = local.master_user_name
|
|
master_user_password = random_password.master[0].result
|
|
}
|
|
}
|
|
}
|
|
|
|
# Cognito authentication
|
|
dynamic "cognito_options" {
|
|
for_each = local.cognito.enabled ? [1] : []
|
|
content {
|
|
enabled = true
|
|
user_pool_id = local.cognito.user_pool_id
|
|
identity_pool_id = local.cognito.identity_pool_id
|
|
role_arn = local.cognito.role_arn
|
|
}
|
|
}
|
|
|
|
# Logging
|
|
dynamic "log_publishing_options" {
|
|
for_each = local.log_types
|
|
content {
|
|
cloudwatch_log_group_arn = aws_cloudwatch_log_group.opensearch[log_publishing_options.value].arn
|
|
log_type = log_publishing_options.value
|
|
}
|
|
}
|
|
|
|
# Auto-tune
|
|
auto_tune_options {
|
|
desired_state = local.auto_tune_enabled ? "ENABLED" : "DISABLED"
|
|
rollback_on_disable = "NO_ROLLBACK"
|
|
}
|
|
|
|
# Access policy (for non-VPC or fine-grained access)
|
|
access_policies = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [
|
|
{
|
|
Effect = "Allow"
|
|
Principal = {
|
|
AWS = "*"
|
|
}
|
|
Action = "es:*"
|
|
Resource = "arn:aws:es:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:domain/${local.domain_name}/*"
|
|
Condition = local.vpc_enabled ? {} : {
|
|
IpAddress = {
|
|
"aws:SourceIp" = ["0.0.0.0/0"] # Restrict in production!
|
|
}
|
|
}
|
|
}
|
|
]
|
|
})
|
|
|
|
tags = { Name = local.domain_name }
|
|
|
|
depends_on = [
|
|
aws_iam_service_linked_role.opensearch,
|
|
aws_cloudwatch_log_resource_policy.opensearch
|
|
]
|
|
}
|
|
|
|
################################################################################
|
|
# IAM Policy for Application Access
|
|
################################################################################
|
|
|
|
resource "aws_iam_policy" "opensearch_access" {
|
|
name = "${local.domain_name}-access"
|
|
description = "Access to ${local.domain_name} OpenSearch domain"
|
|
|
|
policy = jsonencode({
|
|
Version = "2012-10-17"
|
|
Statement = [
|
|
{
|
|
Sid = "OpenSearchAccess"
|
|
Effect = "Allow"
|
|
Action = [
|
|
"es:ESHttpGet",
|
|
"es:ESHttpHead",
|
|
"es:ESHttpPost",
|
|
"es:ESHttpPut",
|
|
"es:ESHttpDelete"
|
|
]
|
|
Resource = "${aws_opensearch_domain.main.arn}/*"
|
|
}
|
|
]
|
|
})
|
|
|
|
tags = { Name = "${local.domain_name}-access" }
|
|
}
|
|
|
|
################################################################################
|
|
# Outputs
|
|
################################################################################
|
|
|
|
output "domain_endpoint" {
|
|
value = aws_opensearch_domain.main.endpoint
|
|
description = "OpenSearch domain endpoint"
|
|
}
|
|
|
|
output "dashboard_endpoint" {
|
|
value = aws_opensearch_domain.main.dashboard_endpoint
|
|
description = "OpenSearch Dashboards endpoint"
|
|
}
|
|
|
|
output "domain_arn" {
|
|
value = aws_opensearch_domain.main.arn
|
|
description = "Domain ARN"
|
|
}
|
|
|
|
output "domain_id" {
|
|
value = aws_opensearch_domain.main.domain_id
|
|
description = "Domain ID"
|
|
}
|
|
|
|
output "secret_arn" {
|
|
value = length(aws_secretsmanager_secret.opensearch) > 0 ? aws_secretsmanager_secret.opensearch[0].arn : null
|
|
description = "Secrets Manager ARN for credentials"
|
|
}
|
|
|
|
output "access_policy_arn" {
|
|
value = aws_iam_policy.opensearch_access.arn
|
|
description = "IAM policy for application access"
|
|
}
|
|
|
|
output "kibana_url" {
|
|
value = "https://${aws_opensearch_domain.main.dashboard_endpoint}/_dashboards"
|
|
description = "OpenSearch Dashboards URL"
|
|
}
|
|
|
|
output "curl_example" {
|
|
value = local.enable_fine_grained_access ? <<-EOF
|
|
# Get credentials from Secrets Manager
|
|
SECRET=$(aws secretsmanager get-secret-value --secret-id ${aws_secretsmanager_secret.opensearch[0].arn} --query SecretString --output text)
|
|
USER=$(echo $SECRET | jq -r .username)
|
|
PASS=$(echo $SECRET | jq -r .password)
|
|
|
|
# Query cluster health
|
|
curl -u "$USER:$PASS" "https://${aws_opensearch_domain.main.endpoint}/_cluster/health?pretty"
|
|
EOF
|
|
: null
|
|
description = "Example curl commands"
|
|
}
|