first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
# Bifrost Terraform Module
Single entry point for deploying [Bifrost](https://github.com/maximhq/bifrost) on AWS, GCP, Azure, or any Kubernetes cluster. This module handles configuration merging, image resolution, and routes to the appropriate cloud-provider sub-module based on your `cloud_provider` and `service` selections.
## Usage
```hcl
module "bifrost" {
source = "github.com/maximhq/bifrost//terraform/modules/bifrost"
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json_file = "${path.module}/config.json"
# Override specific config sections via variables
encryption_key = var.encryption_key
auth_config = {
admin_username = "admin"
admin_password = var.admin_password
is_enabled = true
}
providers_config = {
openai = {
api_key = var.openai_api_key
}
}
# Infrastructure
cpu = 1024
memory = 2048
desired_count = 2
create_load_balancer = true
enable_autoscaling = true
max_capacity = 5
tags = {
Environment = "production"
}
}
```
## Provider Configuration
You only need to configure the Terraform providers for the cloud you are deploying to. Unused cloud modules are skipped automatically (`count = 0`).
**AWS (ECS / EKS):**
```hcl
provider "aws" {
region = "us-east-1"
}
# EKS also requires the kubernetes provider
provider "kubernetes" {
host = module.bifrost.cluster_endpoint
cluster_ca_certificate = base64decode(module.bifrost.cluster_ca)
token = data.aws_eks_cluster_auth.this.token
}
```
**GCP (GKE / Cloud Run):**
```hcl
provider "google" {
project = "my-project-id"
region = "us-central1"
}
```
**Azure (AKS / ACI):**
```hcl
provider "azurerm" {
features {}
}
```
**Generic Kubernetes:**
```hcl
provider "kubernetes" {
config_path = "~/.kube/config"
}
```
## Supported Deployments
| Cloud Provider | Service | Description |
|----------------|--------------|------------------------------------|
| `aws` | `ecs` | AWS Elastic Container Service |
| `aws` | `eks` | AWS Elastic Kubernetes Service |
| `gcp` | `gke` | Google Kubernetes Engine |
| `gcp` | `cloud-run` | Google Cloud Run |
| `azure` | `aks` | Azure Kubernetes Service |
| `azure` | `aci` | Azure Container Instances |
| `kubernetes` | `deployment` | Any existing Kubernetes cluster |
Invalid combinations (e.g. `cloud_provider = "aws"` with `service = "gke"`) are rejected at plan time with a clear error message.
## Configuration Merging
The module supports three ways to provide Bifrost configuration, which are merged in order of precedence:
1. **Base config file** (`config_json_file`) -- path to a `config.json` file on disk.
2. **Base config string** (`config_json`) -- complete JSON config as a string (used if no file is provided).
3. **Individual variables** (`encryption_key`, `auth_config`, `providers_config`, etc.) -- override matching top-level keys from the base config.
Individual variables always take precedence over the base config. This lets you keep secrets out of your config file and inject them via Terraform variables or a secrets manager.
### Configurable Sections
All 18 top-level properties from the [Bifrost config schema](../../../transports/config.schema.json) are exposed as Terraform variables:
| Variable | Schema Key | Description |
|----------------------|----------------------|------------------------------------------------|
| `encryption_key` | `encryption_key` | Encryption key for sensitive data (Argon2id) |
| `auth_config` | `auth_config` | Authentication (admin credentials, SSO) |
| `client` | `client` | Client settings (CORS, logging, body size) |
| `framework` | `framework` | Framework settings (pricing) |
| `providers_config` | `providers` | LLM provider configurations |
| `governance` | `governance` | Budgets, rate limits, virtual keys, teams |
| `mcp` | `mcp` | Model Context Protocol settings |
| `vector_store` | `vector_store` | Vector database configuration |
| `config_store` | `config_store` | Config storage (SQLite/Postgres) |
| `logs_store` | `logs_store` | Logging storage (SQLite/Postgres) |
| `cluster_config` | `cluster_config` | Cluster mode (peers, gossip, discovery) |
| `scim_config` | `scim_config` | SCIM/SSO (Okta, Entra) |
| `load_balancer_config` | `load_balancer_config` | Intelligent load balancer |
| `guardrails_config` | `guardrails_config` | Guardrails (rules, providers) |
| `plugins` | `plugins` | Plugin configuration array |
| `audit_logs` | `audit_logs` | Audit logging (disabled, hmac_key) |
| `websocket` | `websocket` | WebSocket gateway tuning |
For `scim_config` with `provider = "okta"`, set `config.issuerUrl`, `config.clientId`, `config.clientSecret`, and `config.apiToken`.
## Outputs
| Output | Description |
|--------------------|--------------------------------------------------|
| `service_url` | URL to access the deployed Bifrost service |
| `health_check_url` | URL to the `/health` endpoint |
| `config_json` | Resolved configuration JSON (sensitive, for debugging) |
## Testing
Tests use Terraform's native test framework (requires Terraform >= 1.7) with `mock_provider` — no cloud credentials needed.
```bash
cd terraform/modules/bifrost
terraform init
terraform test
```
Test files are in `tests/` and cover all 7 deployment targets:
| File | Coverage |
|-----------------------------|--------------------------------------------------|
| `root_validation.tftest.hcl`| Valid/invalid cloud_provider + service combos |
| `config_merging.tftest.hcl` | Config precedence, schema URL injection |
| `aws_ecs.tftest.hcl` | ECS: ALB, autoscaling, private subnets |
| `aws_eks.tftest.hcl` | EKS: cluster, HPA, ingress, HTTPS, nodes |
| `aws_shared.tftest.hcl` | VPC/SG creation vs existing, ECS isolation |
| `gcp_gke.tftest.hcl` | GKE: cluster, HPA, ingress, nodes, volumes |
| `gcp_cloudrun.tftest.hcl` | Cloud Run: public access, domain, scaling |
| `azure_aks.tftest.hcl` | AKS: cluster, HPA, ingress, resource groups |
| `azure_aci.tftest.hcl` | ACI: compute, resource groups, VNets |
| `kubernetes.tftest.hcl` | Generic K8s: storage class, ingress, annotations |
## Examples
See the [`examples/`](../../examples/) directory for complete deployment examples for each cloud provider and service combination.

View File

@@ -0,0 +1,320 @@
# =============================================================================
# AWS Platform Module — shared infrastructure for ECS and EKS
# =============================================================================
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
# --- Data sources for existing resources ---
data "aws_availability_zones" "available" {
state = "available"
}
data "aws_region" "current" {}
# --- Locals ---
locals {
is_ecs = var.service == "ecs"
create_vpc = var.existing_vpc_id == null
create_subnets = var.existing_subnet_ids == null
create_security_groups = var.existing_security_group_ids == null
vpc_id = local.create_vpc ? aws_vpc.this[0].id : var.existing_vpc_id
subnet_ids = local.create_subnets ? aws_subnet.public[*].id : var.existing_subnet_ids
security_group_ids = local.create_security_groups ? [aws_security_group.this[0].id] : var.existing_security_group_ids
azs = slice(data.aws_availability_zones.available.names, 0, 2)
}
# =============================================================================
# VPC (optional — created only when existing_vpc_id is not provided)
# =============================================================================
resource "aws_vpc" "this" {
count = local.create_vpc ? 1 : 0
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = merge(var.tags, {
Name = "${var.name_prefix}-vpc"
})
}
resource "aws_internet_gateway" "this" {
count = local.create_vpc ? 1 : 0
vpc_id = aws_vpc.this[0].id
tags = merge(var.tags, {
Name = "${var.name_prefix}-igw"
})
}
resource "aws_subnet" "public" {
count = local.create_subnets ? length(local.azs) : 0
vpc_id = local.vpc_id
cidr_block = cidrsubnet("10.0.0.0/16", 8, count.index)
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = merge(var.tags, {
Name = "${var.name_prefix}-public-${local.azs[count.index]}"
})
}
resource "aws_route_table" "public" {
count = local.create_vpc ? 1 : 0
vpc_id = aws_vpc.this[0].id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this[0].id
}
tags = merge(var.tags, {
Name = "${var.name_prefix}-public-rt"
})
}
resource "aws_route_table_association" "public" {
count = local.create_subnets && local.create_vpc ? length(local.azs) : 0
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public[0].id
}
# =============================================================================
# Security Group (optional — created only when existing IDs not provided)
# =============================================================================
resource "aws_security_group" "this" {
count = local.create_security_groups ? 1 : 0
name = "${var.name_prefix}-sg"
description = "Security group for Bifrost ${var.service} service"
vpc_id = local.vpc_id
tags = merge(var.tags, {
Name = "${var.name_prefix}-sg"
})
}
resource "aws_vpc_security_group_ingress_rule" "container_port" {
count = local.create_security_groups ? 1 : 0
security_group_id = aws_security_group.this[0].id
description = "Allow inbound traffic on container port"
cidr_ipv4 = var.allowed_cidr
from_port = var.container_port
to_port = var.container_port
ip_protocol = "tcp"
tags = var.tags
}
resource "aws_vpc_security_group_egress_rule" "all_outbound" {
count = local.create_security_groups ? 1 : 0
security_group_id = aws_security_group.this[0].id
description = "Allow all outbound traffic"
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
tags = var.tags
}
# =============================================================================
# Secrets Manager — stores Bifrost config.json (ECS only)
# EKS uses Kubernetes secrets directly and does not need Secrets Manager.
# =============================================================================
resource "aws_secretsmanager_secret" "bifrost_config" {
count = local.is_ecs ? 1 : 0
name = "${var.name_prefix}/config"
description = "Bifrost configuration (config.json)"
recovery_window_in_days = var.secret_recovery_window_days
tags = merge(var.tags, {
Name = "${var.name_prefix}-config"
})
}
resource "aws_secretsmanager_secret_version" "bifrost_config" {
count = local.is_ecs ? 1 : 0
secret_id = aws_secretsmanager_secret.bifrost_config[0].id
secret_string = var.config_json
}
# =============================================================================
# IAM — ECS task execution role (ECS only)
# =============================================================================
resource "aws_iam_role" "ecs_execution" {
count = local.is_ecs ? 1 : 0
name = "${var.name_prefix}-ecs-execution"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}]
})
tags = merge(var.tags, {
Name = "${var.name_prefix}-ecs-execution"
})
}
resource "aws_iam_role_policy_attachment" "ecs_execution_policy" {
count = local.is_ecs ? 1 : 0
role = aws_iam_role.ecs_execution[0].name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role_policy" "bifrost_secrets" {
count = local.is_ecs ? 1 : 0
name = "${var.name_prefix}-secrets-access"
role = aws_iam_role.ecs_execution[0].id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowGetBifrostSecret"
Effect = "Allow"
Action = ["secretsmanager:GetSecretValue"]
Resource = [aws_secretsmanager_secret.bifrost_config[0].arn]
},
{
Sid = "AllowCloudWatchLogs"
Effect = "Allow"
Action = ["logs:CreateLogStream", "logs:PutLogEvents"]
Resource = ["${aws_cloudwatch_log_group.bifrost[0].arn}:*"]
}
]
})
}
# =============================================================================
# CloudWatch Log Group (ECS only)
# =============================================================================
resource "aws_cloudwatch_log_group" "bifrost" {
count = local.is_ecs ? 1 : 0
name = "/ecs/${var.name_prefix}"
retention_in_days = 30
tags = merge(var.tags, {
Name = "${var.name_prefix}-logs"
})
}
# =============================================================================
# Service Modules — route to ECS or EKS
# =============================================================================
module "ecs" {
source = "./services/ecs"
count = var.service == "ecs" ? 1 : 0
# Container
image = var.image
container_port = var.container_port
health_check_path = var.health_check_path
# Infrastructure
name_prefix = var.name_prefix
region = var.region
tags = var.tags
# Compute
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
# Networking
vpc_id = local.vpc_id
subnet_ids = local.subnet_ids
security_group_ids = local.security_group_ids
# IAM & secrets
execution_role_arn = aws_iam_role.ecs_execution[0].arn
secret_arn = aws_secretsmanager_secret.bifrost_config[0].arn
log_group_name = aws_cloudwatch_log_group.bifrost[0].name
# Features
create_cluster = var.create_cluster
create_load_balancer = var.create_load_balancer
assign_public_ip = var.assign_public_ip
enable_autoscaling = var.enable_autoscaling
min_capacity = var.min_capacity
max_capacity = var.max_capacity
autoscaling_cpu_threshold = var.autoscaling_cpu_threshold
autoscaling_memory_threshold = var.autoscaling_memory_threshold
}
module "eks" {
source = "./services/eks"
count = var.service == "eks" ? 1 : 0
# Container
image = var.image
container_port = var.container_port
health_check_path = var.health_check_path
# Infrastructure
name_prefix = var.name_prefix
region = var.region
tags = var.tags
# Compute
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
# Networking
subnet_ids = local.subnet_ids
security_group_ids = local.security_group_ids
# Config
config_json = var.config_json
# Domain
domain_name = var.domain_name
certificate_arn = var.certificate_arn
# K8s-specific
create_cluster = var.create_cluster
kubernetes_namespace = var.kubernetes_namespace
node_count = var.node_count
node_machine_type = var.node_machine_type
volume_size_gb = var.volume_size_gb
# Features
create_load_balancer = var.create_load_balancer
enable_autoscaling = var.enable_autoscaling
min_capacity = var.min_capacity
max_capacity = var.max_capacity
autoscaling_cpu_threshold = var.autoscaling_cpu_threshold
autoscaling_memory_threshold = var.autoscaling_memory_threshold
}

View File

@@ -0,0 +1,17 @@
output "service_url" {
description = "URL to access the Bifrost service."
value = try(
module.ecs[0].service_url,
module.eks[0].service_url,
null,
)
}
output "health_check_url" {
description = "URL to the Bifrost health check endpoint."
value = try(
module.ecs[0].health_check_url,
module.eks[0].health_check_url,
null,
)
}

View File

@@ -0,0 +1,236 @@
# =============================================================================
# ECS Fargate Service Module
# =============================================================================
# --- ECS Cluster (optional) ---
resource "aws_ecs_cluster" "this" {
count = var.create_cluster ? 1 : 0
name = "${var.name_prefix}-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
tags = merge(var.tags, {
Name = "${var.name_prefix}-cluster"
})
}
data "aws_caller_identity" "current" {}
locals {
cluster_arn = var.create_cluster ? aws_ecs_cluster.this[0].arn : "arn:aws:ecs:${var.region}:${data.aws_caller_identity.current.account_id}:cluster/${var.name_prefix}-cluster"
cluster_name = var.create_cluster ? aws_ecs_cluster.this[0].name : "${var.name_prefix}-cluster"
}
# --- ECS Task Definition ---
resource "aws_ecs_task_definition" "bifrost" {
family = "${var.name_prefix}-task"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = tostring(var.cpu)
memory = tostring(var.memory)
execution_role_arn = var.execution_role_arn
container_definitions = jsonencode([
{
name = "bifrost"
image = var.image
essential = true
entryPoint = ["/bin/sh", "-c"]
command = [
"if [ -n \"$BIFROST_CONFIG\" ]; then printf '%s' \"$BIFROST_CONFIG\" > /app/data/config.json; else echo \"ERROR: BIFROST_CONFIG not set\" >&2 && exit 1; fi && exec /app/docker-entrypoint.sh /app/main"
]
portMappings = [
{
containerPort = var.container_port
protocol = "tcp"
}
]
secrets = [
{
name = "BIFROST_CONFIG"
valueFrom = var.secret_arn
}
]
healthCheck = {
command = ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:${var.container_port}${var.health_check_path} || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = var.log_group_name
"awslogs-region" = var.region
"awslogs-stream-prefix" = "bifrost"
}
}
}
])
tags = merge(var.tags, {
Name = "${var.name_prefix}-task"
})
}
# --- ECS Service ---
resource "aws_ecs_service" "bifrost" {
name = "${var.name_prefix}-service"
cluster = local.cluster_arn
task_definition = aws_ecs_task_definition.bifrost.arn
desired_count = var.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = var.subnet_ids
security_groups = var.security_group_ids
assign_public_ip = var.assign_public_ip
}
dynamic "load_balancer" {
for_each = var.create_load_balancer ? [1] : []
content {
target_group_arn = aws_lb_target_group.bifrost[0].arn
container_name = "bifrost"
container_port = var.container_port
}
}
health_check_grace_period_seconds = var.create_load_balancer ? 60 : null
depends_on = [
aws_ecs_task_definition.bifrost,
]
tags = merge(var.tags, {
Name = "${var.name_prefix}-service"
})
}
# =============================================================================
# Application Load Balancer (optional)
# =============================================================================
resource "aws_lb" "bifrost" {
count = var.create_load_balancer ? 1 : 0
name = "${var.name_prefix}-alb"
internal = false
load_balancer_type = "application"
security_groups = var.security_group_ids
subnets = var.subnet_ids
tags = merge(var.tags, {
Name = "${var.name_prefix}-alb"
})
}
resource "aws_lb_target_group" "bifrost" {
count = var.create_load_balancer ? 1 : 0
name = "${var.name_prefix}-tg"
port = var.container_port
protocol = "HTTP"
vpc_id = var.vpc_id
target_type = "ip"
health_check {
enabled = true
path = var.health_check_path
port = "traffic-port"
protocol = "HTTP"
healthy_threshold = 3
unhealthy_threshold = 3
timeout = 5
interval = 30
matcher = "200"
}
tags = merge(var.tags, {
Name = "${var.name_prefix}-tg"
})
}
resource "aws_lb_listener" "http" {
count = var.create_load_balancer ? 1 : 0
load_balancer_arn = aws_lb.bifrost[0].arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.bifrost[0].arn
}
tags = var.tags
}
# =============================================================================
# Auto Scaling (optional)
# =============================================================================
resource "aws_appautoscaling_target" "bifrost" {
count = var.enable_autoscaling ? 1 : 0
max_capacity = var.max_capacity
min_capacity = var.min_capacity
resource_id = "service/${local.cluster_name}/${aws_ecs_service.bifrost.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "cpu" {
count = var.enable_autoscaling ? 1 : 0
name = "${var.name_prefix}-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.bifrost[0].resource_id
scalable_dimension = aws_appautoscaling_target.bifrost[0].scalable_dimension
service_namespace = aws_appautoscaling_target.bifrost[0].service_namespace
target_tracking_scaling_policy_configuration {
target_value = var.autoscaling_cpu_threshold
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
resource "aws_appautoscaling_policy" "memory" {
count = var.enable_autoscaling ? 1 : 0
name = "${var.name_prefix}-memory-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.bifrost[0].resource_id
scalable_dimension = aws_appautoscaling_target.bifrost[0].scalable_dimension
service_namespace = aws_appautoscaling_target.bifrost[0].service_namespace
target_tracking_scaling_policy_configuration {
target_value = var.autoscaling_memory_threshold
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageMemoryUtilization"
}
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}

View File

@@ -0,0 +1,29 @@
output "cluster_name" {
description = "Name of the ECS cluster."
value = local.cluster_name
}
output "service_name" {
description = "Name of the ECS service."
value = aws_ecs_service.bifrost.name
}
output "task_definition_arn" {
description = "ARN of the ECS task definition."
value = aws_ecs_task_definition.bifrost.arn
}
output "alb_dns_name" {
description = "DNS name of the Application Load Balancer (if created)."
value = var.create_load_balancer ? aws_lb.bifrost[0].dns_name : null
}
output "service_url" {
description = "URL to access the Bifrost service."
value = var.create_load_balancer ? "http://${aws_lb.bifrost[0].dns_name}" : null
}
output "health_check_url" {
description = "URL to the Bifrost health check endpoint."
value = var.create_load_balancer ? "http://${aws_lb.bifrost[0].dns_name}${var.health_check_path}" : null
}

View File

@@ -0,0 +1,122 @@
# --- Container ---
variable "image" {
description = "Full Docker image reference (repository:tag)."
type = string
}
variable "container_port" {
description = "Port the Bifrost container listens on."
type = number
}
variable "health_check_path" {
description = "HTTP path for health checks."
type = string
}
# --- Infrastructure ---
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
}
variable "region" {
description = "AWS region."
type = string
}
variable "tags" {
description = "Tags to apply to all resources."
type = map(string)
}
# --- Compute ---
variable "desired_count" {
description = "Number of ECS tasks."
type = number
}
variable "cpu" {
description = "CPU units for the Fargate task (256-4096)."
type = number
}
variable "memory" {
description = "Memory in MB for the Fargate task."
type = number
}
# --- Networking ---
variable "vpc_id" {
description = "VPC ID for the ECS service."
type = string
}
variable "subnet_ids" {
description = "Subnet IDs for the ECS service network configuration."
type = list(string)
}
variable "security_group_ids" {
description = "Security group IDs for the ECS service network configuration."
type = list(string)
}
# --- IAM & Secrets ---
variable "execution_role_arn" {
description = "ARN of the ECS task execution IAM role."
type = string
}
variable "secret_arn" {
description = "ARN of the Secrets Manager secret containing config.json."
type = string
}
variable "log_group_name" {
description = "CloudWatch log group name."
type = string
}
# --- Cluster ---
variable "create_cluster" {
description = "Create a new ECS cluster. Set to false to use an existing cluster."
type = bool
}
# --- Features ---
variable "create_load_balancer" {
description = "Create an Application Load Balancer."
type = bool
}
variable "enable_autoscaling" {
description = "Enable autoscaling for the ECS service."
type = bool
}
variable "min_capacity" {
description = "Minimum number of tasks when autoscaling is enabled."
type = number
}
variable "max_capacity" {
description = "Maximum number of tasks when autoscaling is enabled."
type = number
}
variable "autoscaling_cpu_threshold" {
description = "Target CPU utilization percentage for autoscaling."
type = number
}
variable "autoscaling_memory_threshold" {
description = "Target memory utilization percentage for autoscaling."
type = number
}
variable "assign_public_ip" {
description = "Assign a public IP to the ECS task. Set to false for private subnet deployments."
type = bool
default = true
}

View File

@@ -0,0 +1,567 @@
# =============================================================================
# AWS EKS Service Module
# =============================================================================
#
# Deploys Bifrost on Amazon EKS with:
# - IAM roles for EKS cluster and node group
# - EKS cluster + managed node group (optional)
# - Kubernetes namespace, secret, deployment, service
# - EBS-backed persistent volume for SQLite data
# - Horizontal Pod Autoscaler (optional)
# - Ingress (optional, when domain_name is set)
# =============================================================================
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.20"
}
}
}
# --- Locals ---
locals {
service_name = "${var.name_prefix}-service"
instance_type = coalesce(var.node_machine_type, "t3.medium")
availability_zone = "${var.region}a"
common_labels = merge(var.tags, {
app = local.service_name
"app.kubernetes.io/name" = local.service_name
"app.kubernetes.io/instance" = var.name_prefix
})
}
# =============================================================================
# IAM — EKS Cluster Role
# =============================================================================
data "aws_iam_policy_document" "eks_cluster_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["eks.amazonaws.com"]
}
}
}
resource "aws_iam_role" "eks_cluster" {
count = var.create_cluster ? 1 : 0
name = "${var.name_prefix}-eks-cluster-role"
assume_role_policy = data.aws_iam_policy_document.eks_cluster_assume_role.json
tags = merge(var.tags, {
Name = "${var.name_prefix}-eks-cluster-role"
})
}
resource "aws_iam_role_policy_attachment" "eks_cluster_policy" {
count = var.create_cluster ? 1 : 0
role = aws_iam_role.eks_cluster[0].name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
}
# =============================================================================
# IAM — EKS Node Group Role
# =============================================================================
data "aws_iam_policy_document" "eks_node_assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
resource "aws_iam_role" "eks_node" {
count = var.create_cluster ? 1 : 0
name = "${var.name_prefix}-eks-node-role"
assume_role_policy = data.aws_iam_policy_document.eks_node_assume_role.json
tags = merge(var.tags, {
Name = "${var.name_prefix}-eks-node-role"
})
}
resource "aws_iam_role_policy_attachment" "eks_worker_node_policy" {
count = var.create_cluster ? 1 : 0
role = aws_iam_role.eks_node[0].name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy"
}
resource "aws_iam_role_policy_attachment" "eks_cni_policy" {
count = var.create_cluster ? 1 : 0
role = aws_iam_role.eks_node[0].name
policy_arn = "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"
}
resource "aws_iam_role_policy_attachment" "eks_ecr_read_only" {
count = var.create_cluster ? 1 : 0
role = aws_iam_role.eks_node[0].name
policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
}
# =============================================================================
# EKS Cluster (optional — created only when create_cluster = true)
# =============================================================================
resource "aws_eks_cluster" "this" {
count = var.create_cluster ? 1 : 0
name = "${var.name_prefix}-cluster"
role_arn = aws_iam_role.eks_cluster[0].arn
vpc_config {
subnet_ids = var.subnet_ids
security_group_ids = var.security_group_ids
endpoint_public_access = true
endpoint_private_access = true
}
tags = merge(var.tags, {
Name = "${var.name_prefix}-cluster"
})
depends_on = [
aws_iam_role_policy_attachment.eks_cluster_policy,
]
}
# =============================================================================
# EKS Managed Node Group
# =============================================================================
resource "aws_eks_node_group" "this" {
count = var.create_cluster ? 1 : 0
cluster_name = aws_eks_cluster.this[0].name
node_group_name = "${var.name_prefix}-node-group"
node_role_arn = aws_iam_role.eks_node[0].arn
subnet_ids = var.subnet_ids
instance_types = [local.instance_type]
disk_size = 50
scaling_config {
desired_size = var.node_count
min_size = var.node_count
max_size = var.node_count
}
tags = merge(var.tags, {
Name = "${var.name_prefix}-node-group"
})
depends_on = [
aws_iam_role_policy_attachment.eks_worker_node_policy,
aws_iam_role_policy_attachment.eks_cni_policy,
aws_iam_role_policy_attachment.eks_ecr_read_only,
]
}
# =============================================================================
# Kubernetes Namespace
# =============================================================================
resource "kubernetes_namespace" "bifrost" {
metadata {
name = var.kubernetes_namespace
labels = local.common_labels
}
depends_on = [
aws_eks_cluster.this,
aws_eks_node_group.this,
]
}
# =============================================================================
# Kubernetes Secret — config.json
# =============================================================================
resource "kubernetes_secret" "bifrost_config" {
metadata {
name = "${var.name_prefix}-config"
namespace = kubernetes_namespace.bifrost.metadata[0].name
}
data = {
"config.json" = var.config_json
}
type = "Opaque"
depends_on = [kubernetes_namespace.bifrost]
}
# =============================================================================
# EBS Volume + Persistent Volume + PVC — SQLite persistence
# =============================================================================
resource "aws_ebs_volume" "bifrost_data" {
availability_zone = local.availability_zone
size = var.volume_size_gb
type = "gp3"
encrypted = true
tags = merge(var.tags, {
Name = "${var.name_prefix}-data"
})
lifecycle {
ignore_changes = [tags]
}
}
resource "kubernetes_persistent_volume" "bifrost_data" {
metadata {
name = "${var.name_prefix}-data-pv"
}
spec {
capacity = {
storage = "${var.volume_size_gb}Gi"
}
access_modes = ["ReadWriteOnce"]
persistent_volume_reclaim_policy = "Retain"
storage_class_name = "gp3"
persistent_volume_source {
aws_elastic_block_store {
volume_id = aws_ebs_volume.bifrost_data.id
fs_type = "ext4"
}
}
node_affinity {
required {
node_selector_term {
match_expressions {
key = "topology.kubernetes.io/zone"
operator = "In"
values = [local.availability_zone]
}
}
}
}
}
depends_on = [aws_ebs_volume.bifrost_data]
lifecycle {
prevent_destroy = false
}
}
resource "kubernetes_persistent_volume_claim" "bifrost_data" {
metadata {
name = "${var.name_prefix}-data-pvc"
namespace = kubernetes_namespace.bifrost.metadata[0].name
}
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "${var.volume_size_gb}Gi"
}
}
storage_class_name = "gp3"
volume_name = kubernetes_persistent_volume.bifrost_data.metadata[0].name
}
depends_on = [kubernetes_persistent_volume.bifrost_data]
}
# =============================================================================
# Kubernetes Deployment
# =============================================================================
resource "kubernetes_deployment" "bifrost" {
metadata {
name = local.service_name
namespace = kubernetes_namespace.bifrost.metadata[0].name
labels = local.common_labels
}
spec {
replicas = var.desired_count
selector {
match_labels = {
app = local.service_name
}
}
template {
metadata {
labels = merge(local.common_labels, {
app = local.service_name
})
}
spec {
# --- Pod-level security context ---
security_context {
fs_group = 1000
fs_group_change_policy = "OnRootMismatch"
}
# --- Init container: fix volume permissions ---
init_container {
name = "fix-permissions"
image = "busybox:latest"
command = ["sh", "-c", "chown -R 1000:1000 /app/data && chmod -R 755 /app/data"]
security_context {
run_as_user = 0
}
volume_mount {
name = "bifrost-volume"
mount_path = "/app/data"
}
}
# --- Main container ---
container {
name = "bifrost"
image = var.image
port {
container_port = var.container_port
name = "http"
}
security_context {
run_as_user = 1000
run_as_group = 1000
run_as_non_root = true
allow_privilege_escalation = false
}
resources {
requests = {
cpu = "${var.cpu}m"
memory = "${var.memory}Mi"
}
limits = {
cpu = "${var.cpu * 2}m"
memory = "${var.memory * 2}Mi"
}
}
# Data volume
volume_mount {
name = "bifrost-volume"
mount_path = "/app/data"
}
# Config secret (mounted as a single file via sub_path)
volume_mount {
name = "config-volume"
mount_path = "/app/data/config.json"
sub_path = "config.json"
}
# Liveness probe
liveness_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 30
period_seconds = 10
timeout_seconds = 5
failure_threshold = 3
}
# Readiness probe
readiness_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 10
period_seconds = 5
timeout_seconds = 3
failure_threshold = 3
}
}
# --- Volumes ---
volume {
name = "bifrost-volume"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.bifrost_data.metadata[0].name
}
}
volume {
name = "config-volume"
secret {
secret_name = kubernetes_secret.bifrost_config.metadata[0].name
}
}
}
}
}
depends_on = [
kubernetes_secret.bifrost_config,
kubernetes_persistent_volume_claim.bifrost_data,
]
}
# =============================================================================
# Kubernetes Service (ClusterIP)
# =============================================================================
resource "kubernetes_service" "bifrost" {
metadata {
name = local.service_name
namespace = kubernetes_namespace.bifrost.metadata[0].name
labels = local.common_labels
}
spec {
selector = {
app = local.service_name
}
port {
name = "http"
port = 80
target_port = var.container_port
protocol = "TCP"
}
type = "ClusterIP"
}
depends_on = [kubernetes_deployment.bifrost]
}
# =============================================================================
# Horizontal Pod Autoscaler (optional)
# =============================================================================
resource "kubernetes_horizontal_pod_autoscaler_v2" "bifrost" {
count = var.enable_autoscaling ? 1 : 0
metadata {
name = "${local.service_name}-hpa"
namespace = kubernetes_namespace.bifrost.metadata[0].name
}
spec {
scale_target_ref {
api_version = "apps/v1"
kind = "Deployment"
name = kubernetes_deployment.bifrost.metadata[0].name
}
min_replicas = var.min_capacity
max_replicas = var.max_capacity
metric {
type = "Resource"
resource {
name = "cpu"
target {
type = "Utilization"
average_utilization = var.autoscaling_cpu_threshold
}
}
}
metric {
type = "Resource"
resource {
name = "memory"
target {
type = "Utilization"
average_utilization = var.autoscaling_memory_threshold
}
}
}
}
}
# =============================================================================
# Kubernetes Ingress (optional — created when create_load_balancer is true)
#
# PREREQUISITE: AWS Load Balancer Controller must be installed in the EKS
# cluster for the ALB Ingress annotations to work. See:
# https://docs.aws.amazon.com/eks/latest/userguide/aws-load-balancer-controller.html
# =============================================================================
resource "kubernetes_ingress_v1" "bifrost" {
count = var.create_load_balancer ? 1 : 0
metadata {
name = "${local.service_name}-ingress"
namespace = kubernetes_namespace.bifrost.metadata[0].name
labels = local.common_labels
annotations = merge({
"kubernetes.io/ingress.class" = "alb"
"alb.ingress.kubernetes.io/scheme" = "internet-facing"
"alb.ingress.kubernetes.io/target-type" = "ip"
"alb.ingress.kubernetes.io/listen-ports" = var.certificate_arn != null ? "[{\"HTTPS\":443}]" : "[{\"HTTP\":80}]"
"alb.ingress.kubernetes.io/healthcheck-path" = var.health_check_path
}, var.certificate_arn != null ? {
"alb.ingress.kubernetes.io/certificate-arn" = var.certificate_arn
} : {})
}
spec {
rule {
host = var.domain_name
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service.bifrost.metadata[0].name
port {
number = 80
}
}
}
}
}
}
}
depends_on = [kubernetes_service.bifrost]
}

View File

@@ -0,0 +1,33 @@
# =============================================================================
# AWS EKS Service Module — Outputs
# =============================================================================
output "cluster_name" {
description = "Name of the EKS cluster."
value = var.create_cluster ? aws_eks_cluster.this[0].name : null
}
output "cluster_endpoint" {
description = "Endpoint URL of the EKS cluster API server."
value = var.create_cluster ? aws_eks_cluster.this[0].endpoint : null
}
output "namespace" {
description = "Kubernetes namespace where Bifrost is deployed."
value = kubernetes_namespace.bifrost.metadata[0].name
}
output "service_name" {
description = "Name of the Kubernetes service exposing Bifrost."
value = kubernetes_service.bifrost.metadata[0].name
}
output "service_url" {
description = "Internal cluster URL for the Bifrost service."
value = "http://${kubernetes_service.bifrost.metadata[0].name}.${kubernetes_namespace.bifrost.metadata[0].name}.svc.cluster.local"
}
output "health_check_url" {
description = "Internal cluster URL for the Bifrost health check endpoint."
value = "http://${kubernetes_service.bifrost.metadata[0].name}.${kubernetes_namespace.bifrost.metadata[0].name}.svc.cluster.local${var.health_check_path}"
}

View File

@@ -0,0 +1,150 @@
# =============================================================================
# AWS EKS Service Module — Variables
# =============================================================================
# --- Infrastructure ---
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
}
variable "region" {
description = "AWS region."
type = string
}
variable "tags" {
description = "Tags to apply to all resources."
type = map(string)
}
# --- Container ---
variable "image" {
description = "Full Docker image reference (repository:tag)."
type = string
}
variable "container_port" {
description = "Port the Bifrost container listens on."
type = number
}
variable "health_check_path" {
description = "HTTP path for health checks."
type = string
}
# --- Bifrost config ---
variable "config_json" {
description = "Complete Bifrost config.json as a string, stored as a Kubernetes secret."
type = string
sensitive = true
}
# --- Compute ---
variable "desired_count" {
description = "Number of Kubernetes pod replicas."
type = number
}
variable "cpu" {
description = "CPU allocation in millicores (e.g. 500)."
type = number
}
variable "memory" {
description = "Memory allocation in MB."
type = number
}
# --- Networking ---
variable "subnet_ids" {
description = "Subnet IDs for the EKS cluster."
type = list(string)
}
variable "security_group_ids" {
description = "Security group IDs for the EKS cluster."
type = list(string)
}
# --- EKS cluster ---
variable "create_cluster" {
description = "Create a new EKS cluster. Set to false to use an existing cluster."
type = bool
}
variable "kubernetes_namespace" {
description = "Kubernetes namespace to deploy into."
type = string
}
variable "node_count" {
description = "Number of nodes in the EKS node group."
type = number
}
variable "node_machine_type" {
description = "EC2 instance type for EKS nodes (e.g. t3.medium)."
type = string
default = null
}
variable "volume_size_gb" {
description = "Persistent volume size in GB for SQLite storage."
type = number
}
# --- Load balancer ---
variable "create_load_balancer" {
description = "Create an AWS Load Balancer Controller ingress."
type = bool
}
# --- Autoscaling ---
variable "enable_autoscaling" {
description = "Enable horizontal pod autoscaling."
type = bool
}
variable "min_capacity" {
description = "Minimum number of replicas when autoscaling is enabled."
type = number
}
variable "max_capacity" {
description = "Maximum number of replicas when autoscaling is enabled."
type = number
}
variable "autoscaling_cpu_threshold" {
description = "Target CPU utilization percentage for autoscaling."
type = number
}
variable "autoscaling_memory_threshold" {
description = "Target memory utilization percentage for autoscaling."
type = number
}
# --- Domain ---
variable "domain_name" {
description = "Custom domain name for the service (optional). When set, a Kubernetes Ingress is created."
type = string
default = null
}
variable "certificate_arn" {
description = "ACM certificate ARN for HTTPS on the ALB. Required when using HTTPS."
type = string
default = null
}

View File

@@ -0,0 +1,168 @@
# --- Deployment target ---
variable "service" {
description = "AWS service to deploy on (ecs or eks)."
type = string
}
# --- Bifrost config ---
variable "config_json" {
description = "Complete Bifrost config.json as a string."
type = string
sensitive = true
}
# --- Image ---
variable "image" {
description = "Full Docker image reference (repository:tag)."
type = string
}
# --- Container ---
variable "container_port" {
description = "Port the Bifrost container listens on."
type = number
}
variable "health_check_path" {
description = "HTTP path for health checks."
type = string
}
# --- Infrastructure ---
variable "region" {
description = "AWS region."
type = string
}
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
}
variable "tags" {
description = "Tags to apply to all resources."
type = map(string)
}
# --- Compute ---
variable "desired_count" {
description = "Number of replicas (ECS tasks / K8s pods)."
type = number
}
variable "cpu" {
description = "CPU allocation (ECS: CPU units 256-4096, K8s: millicores)."
type = number
}
variable "memory" {
description = "Memory allocation in MB."
type = number
}
# --- Networking ---
variable "existing_vpc_id" {
description = "Existing VPC ID. If null, a new VPC will be created."
type = string
default = null
}
variable "existing_subnet_ids" {
description = "Existing subnet IDs. If null, new subnets will be created."
type = list(string)
default = null
}
variable "allowed_cidr" {
description = "CIDR block allowed for ingress traffic."
type = string
default = "0.0.0.0/0"
}
variable "existing_security_group_ids" {
description = "Existing security group IDs. If null, a new one will be created."
type = list(string)
default = null
}
# --- Optional features ---
variable "create_load_balancer" {
description = "Create an Application Load Balancer."
type = bool
}
variable "assign_public_ip" {
description = "Assign a public IP to the ECS Fargate task. Set to false for private subnet deployments."
type = bool
default = true
}
variable "enable_autoscaling" {
description = "Enable autoscaling for the service."
type = bool
}
variable "min_capacity" {
description = "Minimum number of replicas when autoscaling is enabled."
type = number
}
variable "max_capacity" {
description = "Maximum number of replicas when autoscaling is enabled."
type = number
}
variable "autoscaling_cpu_threshold" {
description = "Target CPU utilization percentage for autoscaling."
type = number
}
variable "autoscaling_memory_threshold" {
description = "Target memory utilization percentage for autoscaling."
type = number
}
variable "secret_recovery_window_days" {
description = "Number of days to retain deleted secrets before permanent deletion. Set to 0 for immediate deletion (useful for dev/testing)."
type = number
default = 0
}
variable "domain_name" {
description = "Custom domain name for the service (optional)."
type = string
default = null
}
variable "certificate_arn" {
description = "ACM certificate ARN for HTTPS on the ALB Ingress (EKS). Required when using HTTPS."
type = string
default = null
}
# --- K8s-specific (EKS) ---
variable "create_cluster" {
description = "Create a new EKS cluster. Set to false to use an existing cluster."
type = bool
}
variable "kubernetes_namespace" {
description = "Kubernetes namespace to deploy into."
type = string
}
variable "node_count" {
description = "Number of nodes in the EKS node group."
type = number
}
variable "node_machine_type" {
description = "EC2 instance type for EKS nodes (e.g. t3.medium)."
type = string
default = null
}
variable "volume_size_gb" {
description = "Persistent volume size in GB for SQLite storage."
type = number
}

View File

@@ -0,0 +1,218 @@
# =============================================================================
# Azure Platform Module - Shared Infrastructure
# =============================================================================
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.0"
}
}
}
data "azurerm_client_config" "current" {}
locals {
# Sanitize name_prefix for resources that require alphanumeric-only names
name_prefix_clean = replace(var.name_prefix, "-", "")
# Validate: existing VPC and subnets must be provided together
_validate_vpc_subnet = (
(var.existing_vpc_id == null) == (var.existing_subnet_ids == null)
)
# Resolve resource group: use existing or create new
resource_group_name = var.resource_group_name != null ? var.resource_group_name : azurerm_resource_group.this[0].name
resource_group_id = var.resource_group_name != null ? data.azurerm_resource_group.existing[0].id : azurerm_resource_group.this[0].id
# Resolve networking: use existing or create new
vnet_id = var.existing_vpc_id != null ? var.existing_vpc_id : azurerm_virtual_network.this[0].id
subnet_ids = var.existing_subnet_ids != null ? var.existing_subnet_ids : [azurerm_subnet.this[0].id]
}
# =============================================================================
# Resource Group
# =============================================================================
data "azurerm_resource_group" "existing" {
count = var.resource_group_name != null ? 1 : 0
name = var.resource_group_name
}
resource "azurerm_resource_group" "this" {
count = var.resource_group_name == null ? 1 : 0
name = "${var.name_prefix}-rg"
location = var.region
tags = var.tags
}
# =============================================================================
# Virtual Network (optional - skip if existing_vpc_id provided)
# =============================================================================
resource "azurerm_virtual_network" "this" {
count = var.existing_vpc_id == null ? 1 : 0
name = "${var.name_prefix}-vnet"
location = var.region
resource_group_name = local.resource_group_name
address_space = ["10.0.0.0/16"]
tags = var.tags
}
resource "azurerm_subnet" "this" {
count = var.existing_vpc_id == null ? 1 : 0
name = "${var.name_prefix}-subnet"
resource_group_name = local.resource_group_name
virtual_network_name = azurerm_virtual_network.this[0].name
address_prefixes = ["10.0.1.0/24"]
service_endpoints = ["Microsoft.KeyVault"]
}
# =============================================================================
# Network Security Group
# =============================================================================
resource "azurerm_network_security_group" "this" {
name = "${var.name_prefix}-nsg"
location = var.region
resource_group_name = local.resource_group_name
tags = var.tags
security_rule {
name = "allow-bifrost-inbound"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = tostring(var.container_port)
source_address_prefix = var.allowed_cidr
destination_address_prefix = "*"
}
}
resource "azurerm_subnet_network_security_group_association" "this" {
count = var.existing_vpc_id == null ? 1 : 0
subnet_id = azurerm_subnet.this[0].id
network_security_group_id = azurerm_network_security_group.this.id
}
# =============================================================================
# Key Vault for config storage
# =============================================================================
resource "azurerm_key_vault" "this" {
name = "${local.name_prefix_clean}kv"
location = var.region
resource_group_name = local.resource_group_name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
soft_delete_retention_days = 7
purge_protection_enabled = false
tags = var.tags
network_acls {
default_action = "Deny"
bypass = "AzureServices"
virtual_network_subnet_ids = local.subnet_ids
}
# Access policy for the current Terraform principal
access_policy {
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.azurerm_client_config.current.object_id
secret_permissions = [
"Get",
"List",
"Set",
"Delete",
"Purge",
]
}
# Access policy for the managed identity
access_policy {
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_user_assigned_identity.this.principal_id
secret_permissions = [
"Get",
"List",
]
}
}
resource "azurerm_key_vault_secret" "config" {
name = "${var.name_prefix}-config"
value = var.config_json
key_vault_id = azurerm_key_vault.this.id
}
# =============================================================================
# User Assigned Identity
# =============================================================================
resource "azurerm_user_assigned_identity" "this" {
name = "${var.name_prefix}-identity"
location = var.region
resource_group_name = local.resource_group_name
tags = var.tags
}
# =============================================================================
# Service Modules
# =============================================================================
# --- AKS (Azure Kubernetes Service) ---
module "aks" {
source = "./services/aks"
count = var.service == "aks" ? 1 : 0
name_prefix = var.name_prefix
region = var.region
resource_group_name = local.resource_group_name
tags = var.tags
config_json = var.config_json
image = var.image
container_port = var.container_port
health_check_path = var.health_check_path
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
subnet_ids = local.subnet_ids
create_load_balancer = var.create_load_balancer
enable_autoscaling = var.enable_autoscaling
min_capacity = var.min_capacity
max_capacity = var.max_capacity
autoscaling_cpu_threshold = var.autoscaling_cpu_threshold
autoscaling_memory_threshold = var.autoscaling_memory_threshold
domain_name = var.domain_name
create_cluster = var.create_cluster
kubernetes_namespace = var.kubernetes_namespace
node_count = var.node_count
node_machine_type = var.node_machine_type
volume_size_gb = var.volume_size_gb
identity_id = azurerm_user_assigned_identity.this.id
}
# --- ACI (Azure Container Instances) ---
module "aci" {
source = "./services/aci"
count = var.service == "aci" ? 1 : 0
name_prefix = var.name_prefix
region = var.region
resource_group_name = local.resource_group_name
tags = var.tags
config_json = var.config_json
image = var.image
container_port = var.container_port
health_check_path = var.health_check_path
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
subnet_ids = local.subnet_ids
identity_id = azurerm_user_assigned_identity.this.id
}

View File

@@ -0,0 +1,15 @@
output "service_url" {
description = "URL to access the Bifrost service."
value = try(coalesce(
try(module.aks[0].service_url, null),
try(module.aci[0].service_url, null),
), null)
}
output "health_check_url" {
description = "URL to the Bifrost health check endpoint."
value = try(coalesce(
try(module.aks[0].health_check_url, null),
try(module.aci[0].health_check_url, null),
), null)
}

View File

@@ -0,0 +1,63 @@
# =============================================================================
# ACI Service Module - Azure Container Instances for Bifrost
# =============================================================================
locals {
# Convert CPU millicores to ACI decimal cores (e.g. 500 -> 0.5, 1000 -> 1.0)
cpu_cores = var.cpu >= 100 ? var.cpu / 1000 : var.cpu
# Convert memory MB to GB for ACI (e.g. 1024 -> 1.0, 2048 -> 2.0)
memory_gb = var.memory / 1024
}
# =============================================================================
# Container Group
# =============================================================================
resource "azurerm_container_group" "bifrost" {
name = "${var.name_prefix}-aci"
location = var.region
resource_group_name = var.resource_group_name
os_type = "Linux"
ip_address_type = "Public"
dns_name_label = var.name_prefix
tags = var.tags
identity {
type = "UserAssigned"
identity_ids = [var.identity_id]
}
container {
name = "bifrost"
image = var.image
cpu = local.cpu_cores
memory = local.memory_gb
ports {
port = var.container_port
protocol = "TCP"
}
# Config injected as secure env var, written to file via command override
secure_environment_variables = {
BIFROST_CONFIG = var.config_json
}
# Write config from env var to file, then start Bifrost
commands = [
"/bin/sh",
"-c",
"if [ -n \"$BIFROST_CONFIG\" ]; then printf '%s' \"$BIFROST_CONFIG\" > /app/data/config.json; else echo 'ERROR: BIFROST_CONFIG not set' >&2 && exit 1; fi && exec /app/docker-entrypoint.sh /app/main",
]
liveness_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 30
period_seconds = 10
}
}
}

View File

@@ -0,0 +1,24 @@
output "container_group_name" {
description = "Name of the Azure Container Group."
value = azurerm_container_group.bifrost.name
}
output "fqdn" {
description = "FQDN of the container group."
value = azurerm_container_group.bifrost.fqdn
}
output "ip_address" {
description = "Public IP address of the container group."
value = azurerm_container_group.bifrost.ip_address
}
output "service_url" {
description = "URL to access the Bifrost service."
value = "http://${azurerm_container_group.bifrost.fqdn}:${var.container_port}"
}
output "health_check_url" {
description = "URL to the Bifrost health check endpoint."
value = "http://${azurerm_container_group.bifrost.fqdn}:${var.container_port}${var.health_check_path}"
}

View File

@@ -0,0 +1,73 @@
# --- Infrastructure ---
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
}
variable "region" {
description = "Azure region."
type = string
}
variable "resource_group_name" {
description = "Azure resource group name."
type = string
}
variable "tags" {
description = "Tags to apply to all resources."
type = map(string)
}
# --- Bifrost config ---
variable "config_json" {
description = "Complete Bifrost config.json as a string."
type = string
sensitive = true
}
# --- Image ---
variable "image" {
description = "Full Docker image reference (repository:tag)."
type = string
}
# --- Container ---
variable "container_port" {
description = "Port the Bifrost container listens on."
type = number
}
variable "health_check_path" {
description = "HTTP path for health checks."
type = string
}
# --- Compute ---
variable "desired_count" {
description = "Number of container group instances."
type = number
}
variable "cpu" {
description = "CPU allocation (will be converted to ACI decimal cores)."
type = number
}
variable "memory" {
description = "Memory allocation in MB (will be converted to GB for ACI)."
type = number
}
# --- Networking ---
variable "subnet_ids" {
description = "Subnet IDs (optional; used for private networking if needed)."
type = list(string)
default = null
}
# --- Identity ---
variable "identity_id" {
description = "User assigned identity ID for the container group."
type = string
}

View File

@@ -0,0 +1,400 @@
# =============================================================================
# AKS Service Module - Azure Kubernetes Service for Bifrost
# =============================================================================
locals {
service_name = "${var.name_prefix}-service"
vm_size = var.node_machine_type != null ? var.node_machine_type : "Standard_D2s_v3"
cpu_request = "${var.cpu}m"
cpu_limit = "${var.cpu * 2}m"
memory_request = "${var.memory}Mi"
memory_limit = "${var.memory * 2}Mi"
cluster_name = "${var.name_prefix}-aks"
ingress_enabled = var.create_load_balancer && var.domain_name != null
}
# =============================================================================
# AKS Cluster (optional)
# =============================================================================
resource "azurerm_kubernetes_cluster" "this" {
count = var.create_cluster ? 1 : 0
name = local.cluster_name
location = var.region
resource_group_name = var.resource_group_name
dns_prefix = var.name_prefix
tags = var.tags
role_based_access_control_enabled = true
api_server_access_profile {
authorized_ip_ranges = var.api_server_authorized_ip_ranges
}
default_node_pool {
name = "default"
node_count = var.node_count
vm_size = local.vm_size
vnet_subnet_id = var.subnet_ids[0]
}
identity {
type = "UserAssigned"
identity_ids = [var.identity_id]
}
network_profile {
network_plugin = "azure"
service_cidr = "10.1.0.0/16"
dns_service_ip = "10.1.0.10"
}
}
# =============================================================================
# Kubernetes Namespace
# =============================================================================
resource "kubernetes_namespace" "this" {
metadata {
name = var.kubernetes_namespace
labels = {
app = var.name_prefix
}
}
depends_on = [azurerm_kubernetes_cluster.this]
}
# =============================================================================
# Configuration Secret
# =============================================================================
resource "kubernetes_secret" "bifrost_config" {
metadata {
name = "${var.name_prefix}-config"
namespace = kubernetes_namespace.this.metadata[0].name
}
data = {
"config.json" = var.config_json
}
type = "Opaque"
}
# =============================================================================
# Managed Disk + Persistent Volume + PVC for SQLite storage
# =============================================================================
resource "azurerm_managed_disk" "bifrost_disk" {
name = "${var.name_prefix}-disk"
location = var.region
resource_group_name = var.resource_group_name
storage_account_type = "Premium_LRS"
create_option = "Empty"
disk_size_gb = var.volume_size_gb
tags = var.tags
lifecycle {
ignore_changes = [tags]
}
}
resource "kubernetes_persistent_volume" "bifrost_volume" {
metadata {
name = "${var.name_prefix}-volume"
}
spec {
capacity = {
storage = "${var.volume_size_gb}Gi"
}
access_modes = ["ReadWriteOnce"]
persistent_volume_reclaim_policy = "Retain"
storage_class_name = "managed-premium"
persistent_volume_source {
azure_disk {
disk_name = azurerm_managed_disk.bifrost_disk.name
data_disk_uri = azurerm_managed_disk.bifrost_disk.id
kind = "Managed"
caching_mode = "None"
}
}
}
depends_on = [azurerm_managed_disk.bifrost_disk]
lifecycle {
prevent_destroy = false
}
}
resource "kubernetes_persistent_volume_claim" "bifrost_volume_claim" {
metadata {
name = "${var.name_prefix}-volume-claim"
namespace = kubernetes_namespace.this.metadata[0].name
}
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "${var.volume_size_gb}Gi"
}
}
storage_class_name = "managed-premium"
volume_name = kubernetes_persistent_volume.bifrost_volume.metadata[0].name
}
}
# =============================================================================
# Deployment
# =============================================================================
resource "kubernetes_deployment" "bifrost" {
metadata {
name = local.service_name
namespace = kubernetes_namespace.this.metadata[0].name
labels = {
app = local.service_name
}
}
spec {
replicas = var.desired_count
selector {
match_labels = {
app = local.service_name
}
}
template {
metadata {
labels = {
app = local.service_name
}
}
spec {
security_context {
fs_group = 1000
fs_group_change_policy = "OnRootMismatch"
}
# Init container to fix volume permissions
init_container {
name = "fix-permissions"
image = "busybox:latest"
command = ["sh", "-c", "chown -R 1000:1000 /app/data && chmod -R 755 /app/data"]
security_context {
run_as_user = 0
}
volume_mount {
name = "bifrost-volume"
mount_path = "/app/data"
}
}
# Main Bifrost container
container {
name = "bifrost"
image = var.image
port {
container_port = var.container_port
name = "http"
}
security_context {
run_as_user = 1000
run_as_group = 1000
run_as_non_root = true
allow_privilege_escalation = false
}
resources {
requests = {
cpu = local.cpu_request
memory = local.memory_request
}
limits = {
cpu = local.cpu_limit
memory = local.memory_limit
}
}
# Data volume
volume_mount {
name = "bifrost-volume"
mount_path = "/app/data"
}
# Config file mounted via subPath
volume_mount {
name = "config-volume"
mount_path = "/app/data/config.json"
sub_path = "config.json"
}
liveness_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 30
period_seconds = 10
timeout_seconds = 5
failure_threshold = 3
}
readiness_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 10
period_seconds = 5
timeout_seconds = 3
failure_threshold = 3
}
}
volume {
name = "bifrost-volume"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.bifrost_volume_claim.metadata[0].name
}
}
volume {
name = "config-volume"
secret {
secret_name = kubernetes_secret.bifrost_config.metadata[0].name
}
}
}
}
}
depends_on = [
kubernetes_secret.bifrost_config,
kubernetes_persistent_volume_claim.bifrost_volume_claim,
]
}
# =============================================================================
# Service (ClusterIP)
# =============================================================================
resource "kubernetes_service" "bifrost" {
metadata {
name = local.service_name
namespace = kubernetes_namespace.this.metadata[0].name
labels = {
app = local.service_name
}
}
spec {
selector = {
app = local.service_name
}
port {
name = "http"
port = 80
target_port = var.container_port
protocol = "TCP"
}
type = "ClusterIP"
}
}
# =============================================================================
# Horizontal Pod Autoscaler (optional)
# =============================================================================
resource "kubernetes_horizontal_pod_autoscaler_v2" "bifrost" {
count = var.enable_autoscaling ? 1 : 0
metadata {
name = "${local.service_name}-hpa"
namespace = kubernetes_namespace.this.metadata[0].name
}
spec {
min_replicas = var.min_capacity
max_replicas = var.max_capacity
scale_target_ref {
api_version = "apps/v1"
kind = "Deployment"
name = kubernetes_deployment.bifrost.metadata[0].name
}
metric {
type = "Resource"
resource {
name = "cpu"
target {
type = "Utilization"
average_utilization = var.autoscaling_cpu_threshold
}
}
}
metric {
type = "Resource"
resource {
name = "memory"
target {
type = "Utilization"
average_utilization = var.autoscaling_memory_threshold
}
}
}
}
}
# =============================================================================
# Ingress (optional - when create_load_balancer = true)
# =============================================================================
resource "kubernetes_ingress_v1" "bifrost" {
count = local.ingress_enabled ? 1 : 0
metadata {
name = "${local.service_name}-ingress"
namespace = kubernetes_namespace.this.metadata[0].name
annotations = {
"kubernetes.io/ingress.class" = "nginx"
}
}
spec {
rule {
host = var.domain_name
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service.bifrost.metadata[0].name
port {
number = 80
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
output "cluster_name" {
description = "Name of the AKS cluster."
value = var.create_cluster ? azurerm_kubernetes_cluster.this[0].name : null
}
output "cluster_fqdn" {
description = "FQDN of the AKS cluster."
value = var.create_cluster ? azurerm_kubernetes_cluster.this[0].fqdn : null
}
output "namespace" {
description = "Kubernetes namespace where Bifrost is deployed."
value = kubernetes_namespace.this.metadata[0].name
}
output "service_name" {
description = "Name of the Kubernetes service."
value = kubernetes_service.bifrost.metadata[0].name
}
output "ingress_ip" {
description = "IP address of the Ingress (if created)."
value = var.create_load_balancer ? try(kubernetes_ingress_v1.bifrost[0].status[0].load_balancer[0].ingress[0].ip, null) : null
}
output "service_url" {
description = "URL to access the Bifrost service."
value = var.create_load_balancer ? (
var.domain_name != null ? "http://${var.domain_name}" : (
try("http://${kubernetes_ingress_v1.bifrost[0].status[0].load_balancer[0].ingress[0].ip}", null)
)
) : "http://${kubernetes_service.bifrost.metadata[0].name}.${kubernetes_namespace.this.metadata[0].name}.svc.cluster.local"
}
output "health_check_url" {
description = "URL to the Bifrost health check endpoint."
value = var.create_load_balancer ? (
var.domain_name != null ? "http://${var.domain_name}${var.health_check_path}" : (
try("http://${kubernetes_ingress_v1.bifrost[0].status[0].load_balancer[0].ingress[0].ip}${var.health_check_path}", null)
)
) : "http://${kubernetes_service.bifrost.metadata[0].name}.${kubernetes_namespace.this.metadata[0].name}.svc.cluster.local${var.health_check_path}"
}

View File

@@ -0,0 +1,142 @@
# --- Infrastructure ---
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
}
variable "region" {
description = "Azure region."
type = string
}
variable "resource_group_name" {
description = "Azure resource group name."
type = string
}
variable "tags" {
description = "Tags to apply to all resources."
type = map(string)
}
# --- Bifrost config ---
variable "config_json" {
description = "Complete Bifrost config.json as a string."
type = string
sensitive = true
}
# --- Image ---
variable "image" {
description = "Full Docker image reference (repository:tag)."
type = string
}
# --- Container ---
variable "container_port" {
description = "Port the Bifrost container listens on."
type = number
}
variable "health_check_path" {
description = "HTTP path for health checks."
type = string
}
# --- Compute ---
variable "desired_count" {
description = "Number of K8s pod replicas."
type = number
}
variable "cpu" {
description = "CPU allocation in millicores (e.g. 500)."
type = number
}
variable "memory" {
description = "Memory allocation in MB."
type = number
}
# --- Networking ---
variable "subnet_ids" {
description = "Subnet IDs for the AKS cluster."
type = list(string)
}
# --- Optional features ---
variable "create_load_balancer" {
description = "Create a Kubernetes Ingress for external access."
type = bool
}
variable "enable_autoscaling" {
description = "Enable horizontal pod autoscaling."
type = bool
}
variable "min_capacity" {
description = "Minimum number of replicas when autoscaling is enabled."
type = number
}
variable "max_capacity" {
description = "Maximum number of replicas when autoscaling is enabled."
type = number
}
variable "autoscaling_cpu_threshold" {
description = "Target CPU utilization percentage for autoscaling."
type = number
}
variable "autoscaling_memory_threshold" {
description = "Target memory utilization percentage for autoscaling."
type = number
}
variable "domain_name" {
description = "Custom domain name for the Ingress (optional)."
type = string
default = null
}
# --- K8s-specific ---
variable "create_cluster" {
description = "Create a new AKS cluster. Set to false to use an existing cluster."
type = bool
}
variable "kubernetes_namespace" {
description = "Kubernetes namespace to deploy into."
type = string
}
variable "node_count" {
description = "Number of nodes in the AKS default node pool."
type = number
}
variable "node_machine_type" {
description = "VM size for AKS nodes (e.g. Standard_D2s_v3)."
type = string
default = null
}
variable "volume_size_gb" {
description = "Persistent volume size in GB for SQLite storage."
type = number
}
# --- Identity ---
variable "identity_id" {
description = "User assigned identity ID for the AKS cluster."
type = string
}
variable "api_server_authorized_ip_ranges" {
description = "IP ranges authorized to access the AKS API server."
type = list(string)
default = ["0.0.0.0/0"]
}

View File

@@ -0,0 +1,151 @@
# --- Deployment target ---
variable "service" {
description = "Azure service to deploy on (aks or aci)."
type = string
}
# --- Bifrost config ---
variable "config_json" {
description = "Complete Bifrost config.json as a string."
type = string
sensitive = true
}
# --- Image ---
variable "image" {
description = "Full Docker image reference (repository:tag)."
type = string
}
# --- Container ---
variable "container_port" {
description = "Port the Bifrost container listens on."
type = number
}
variable "health_check_path" {
description = "HTTP path for health checks."
type = string
}
# --- Infrastructure ---
variable "region" {
description = "Azure region (e.g. eastus, westeurope)."
type = string
}
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
}
variable "tags" {
description = "Tags to apply to all resources."
type = map(string)
}
# --- Compute ---
variable "desired_count" {
description = "Number of replicas (K8s pods / ACI container groups)."
type = number
}
variable "cpu" {
description = "CPU allocation (AKS: millicores, ACI: cores)."
type = number
}
variable "memory" {
description = "Memory allocation in MB."
type = number
}
# --- Networking ---
variable "allowed_cidr" {
description = "CIDR block or address prefix allowed for ingress traffic."
type = string
default = "*"
}
variable "existing_vpc_id" {
description = "Existing VNet ID. If null, a new VNet will be created."
type = string
default = null
}
variable "existing_subnet_ids" {
description = "Existing subnet IDs. If null, new subnets will be created."
type = list(string)
default = null
}
# --- Optional features ---
variable "create_load_balancer" {
description = "Create a load balancer (Ingress for AKS)."
type = bool
}
variable "enable_autoscaling" {
description = "Enable autoscaling for the service."
type = bool
}
variable "min_capacity" {
description = "Minimum number of replicas when autoscaling is enabled."
type = number
}
variable "max_capacity" {
description = "Maximum number of replicas when autoscaling is enabled."
type = number
}
variable "autoscaling_cpu_threshold" {
description = "Target CPU utilization percentage for autoscaling."
type = number
}
variable "autoscaling_memory_threshold" {
description = "Target memory utilization percentage for autoscaling."
type = number
}
variable "domain_name" {
description = "Custom domain name for the service (optional)."
type = string
default = null
}
# --- K8s-specific (AKS) ---
variable "create_cluster" {
description = "Create a new AKS cluster. Set to false to use an existing cluster."
type = bool
}
variable "kubernetes_namespace" {
description = "Kubernetes namespace to deploy into."
type = string
}
variable "node_count" {
description = "Number of nodes in the AKS node pool."
type = number
}
variable "node_machine_type" {
description = "VM size for AKS nodes (e.g. Standard_D2s_v3)."
type = string
default = null
}
variable "volume_size_gb" {
description = "Persistent volume size in GB for SQLite storage."
type = number
}
# --- Azure-specific ---
variable "resource_group_name" {
description = "Existing Azure resource group name. If null, a new one will be created."
type = string
default = null
}

View File

@@ -0,0 +1,211 @@
# ──────────────────────────────────────────────────────────────────────────────
# GCP platform module shared infrastructure for GKE and Cloud Run
# ──────────────────────────────────────────────────────────────────────────────
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = ">= 5.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.0"
}
}
}
locals {
service_name = "${var.name_prefix}-service"
# Validate: VPC and subnet must be provided together
_validate_vpc_subnet = (
(var.existing_vpc_id == null) == (var.existing_subnet_ids == null)
)
# Resolve networking: use existing or created
vpc_id = var.existing_vpc_id != null ? var.existing_vpc_id : try(google_compute_network.bifrost[0].self_link, null)
subnet_id = var.existing_subnet_ids != null ? var.existing_subnet_ids[0] : try(google_compute_subnetwork.bifrost[0].self_link, null)
create_network = var.existing_vpc_id == null
}
# ──────────────────────────────────────────────────────────────────────────────
# VPC Network (optional skip if existing_vpc_id is provided)
# ──────────────────────────────────────────────────────────────────────────────
resource "google_compute_network" "bifrost" {
count = local.create_network ? 1 : 0
name = "${var.name_prefix}-vpc"
project = var.project_id
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "bifrost" {
count = local.create_network ? 1 : 0
name = "${var.name_prefix}-subnet"
project = var.project_id
region = var.region
network = google_compute_network.bifrost[0].self_link
ip_cidr_range = "10.0.0.0/16"
secondary_ip_range {
range_name = "${var.name_prefix}-pods"
ip_cidr_range = "10.1.0.0/16"
}
secondary_ip_range {
range_name = "${var.name_prefix}-services"
ip_cidr_range = "10.2.0.0/16"
}
}
# ──────────────────────────────────────────────────────────────────────────────
# Firewall allow ingress on the container port
# ──────────────────────────────────────────────────────────────────────────────
resource "google_compute_firewall" "bifrost_allow_ingress" {
count = local.create_network ? 1 : 0
name = "${var.name_prefix}-allow-ingress"
project = var.project_id
network = local.vpc_id
direction = "INGRESS"
allow {
protocol = "tcp"
ports = [tostring(var.container_port)]
}
source_ranges = [var.allowed_cidr]
target_tags = ["${var.name_prefix}-node"]
}
# ──────────────────────────────────────────────────────────────────────────────
# Secret Manager store config.json
# ──────────────────────────────────────────────────────────────────────────────
resource "google_secret_manager_secret" "bifrost_config" {
secret_id = "${var.name_prefix}-config"
project = var.project_id
labels = var.tags
replication {
auto {}
}
}
resource "google_secret_manager_secret_version" "bifrost_config" {
secret = google_secret_manager_secret.bifrost_config.id
secret_data = var.config_json
}
# ──────────────────────────────────────────────────────────────────────────────
# Service Account + IAM bindings
# ──────────────────────────────────────────────────────────────────────────────
resource "google_service_account" "bifrost" {
account_id = "${var.name_prefix}-sa"
display_name = "Bifrost Service Account"
project = var.project_id
}
resource "google_project_iam_member" "bifrost_secret_accessor" {
project = var.project_id
role = "roles/secretmanager.secretAccessor"
member = "serviceAccount:${google_service_account.bifrost.email}"
}
resource "google_project_iam_member" "bifrost_log_writer" {
project = var.project_id
role = "roles/logging.logWriter"
member = "serviceAccount:${google_service_account.bifrost.email}"
}
# ──────────────────────────────────────────────────────────────────────────────
# Route to GKE or Cloud Run
# ──────────────────────────────────────────────────────────────────────────────
module "gke" {
source = "./services/gke"
count = var.service == "gke" ? 1 : 0
# Bifrost
service_name = local.service_name
config_json = var.config_json
image = var.image
container_port = var.container_port
health_check_path = var.health_check_path
# GCP
project_id = var.project_id
region = var.region
name_prefix = var.name_prefix
tags = var.tags
service_account = google_service_account.bifrost.email
# Networking
vpc_id = local.vpc_id
subnet_id = local.subnet_id
# Compute
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
# Cluster
create_cluster = var.create_cluster
kubernetes_namespace = var.kubernetes_namespace
node_count = var.node_count
node_machine_type = var.node_machine_type
volume_size_gb = var.volume_size_gb
# Optional features
create_load_balancer = var.create_load_balancer
enable_autoscaling = var.enable_autoscaling
min_capacity = var.min_capacity
max_capacity = var.max_capacity
autoscaling_cpu_threshold = var.autoscaling_cpu_threshold
autoscaling_memory_threshold = var.autoscaling_memory_threshold
domain_name = var.domain_name
}
module "cloud_run" {
source = "./services/cloud-run"
count = var.service == "cloud-run" ? 1 : 0
# Bifrost
service_name = local.service_name
config_json = var.config_json
image = var.image
container_port = var.container_port
health_check_path = var.health_check_path
# GCP
project_id = var.project_id
region = var.region
name_prefix = var.name_prefix
tags = var.tags
service_account = google_service_account.bifrost.email
secret_id = google_secret_manager_secret.bifrost_config.secret_id
secret_version = google_secret_manager_secret_version.bifrost_config.version
# Networking
vpc_id = local.vpc_id
# Compute
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
# Scaling
max_capacity = var.max_capacity
# Optional features
create_load_balancer = var.create_load_balancer
domain_name = var.domain_name
}

View File

@@ -0,0 +1,15 @@
output "service_url" {
description = "URL to access the Bifrost service."
value = try(coalesce(
try(module.gke[0].service_url, null),
try(module.cloud_run[0].service_url, null),
), null)
}
output "health_check_url" {
description = "URL to the Bifrost health check endpoint."
value = try(coalesce(
try(module.gke[0].health_check_url, null),
try(module.cloud_run[0].health_check_url, null),
), null)
}

View File

@@ -0,0 +1,136 @@
# ──────────────────────────────────────────────────────────────────────────────
# Cloud Run service sub-module Bifrost on Google Cloud Run
# ──────────────────────────────────────────────────────────────────────────────
terraform {
required_providers {
google = {
source = "hashicorp/google"
}
}
}
locals {
# Cloud Run expects CPU as string like "1" or "2" (whole vCPUs) or "1000m"
cpu_string = "${var.cpu}m"
# Cloud Run expects memory as string like "512Mi"
memory_string = "${var.memory}Mi"
}
# ──────────────────────────────────────────────────────────────────────────────
# Cloud Run v2 Service
# ──────────────────────────────────────────────────────────────────────────────
resource "google_cloud_run_v2_service" "bifrost" {
name = var.service_name
project = var.project_id
location = var.region
labels = var.tags
template {
service_account = var.service_account
scaling {
min_instance_count = var.desired_count
max_instance_count = var.max_capacity
}
volumes {
name = "config-volume"
secret {
secret = var.secret_id
items {
version = var.secret_version
path = "config.json"
}
}
}
containers {
image = var.image
ports {
container_port = var.container_port
}
resources {
limits = {
cpu = local.cpu_string
memory = local.memory_string
}
}
# Mount config.json from Secret Manager
volume_mounts {
name = "config-volume"
mount_path = "/app/data"
}
# Startup probe allows time for the container to initialize
startup_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 10
period_seconds = 5
timeout_seconds = 3
failure_threshold = 10
}
# Liveness probe restarts the container if unhealthy
liveness_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 30
period_seconds = 10
timeout_seconds = 5
failure_threshold = 3
}
}
}
lifecycle {
ignore_changes = [
labels["run.googleapis.com/startupProbeType"],
]
}
}
# ──────────────────────────────────────────────────────────────────────────────
# IAM Allow unauthenticated access (optional, when create_load_balancer is true)
# ──────────────────────────────────────────────────────────────────────────────
resource "google_cloud_run_v2_service_iam_member" "public_access" {
count = var.create_load_balancer ? 1 : 0
project = var.project_id
location = var.region
name = google_cloud_run_v2_service.bifrost.name
role = "roles/run.invoker"
member = "allUsers"
}
# ──────────────────────────────────────────────────────────────────────────────
# Domain Mapping (optional when domain_name is set)
# ──────────────────────────────────────────────────────────────────────────────
resource "google_cloud_run_domain_mapping" "bifrost" {
count = var.domain_name != null ? 1 : 0
name = var.domain_name
project = var.project_id
location = var.region
metadata {
namespace = var.project_id
labels = var.tags
}
spec {
route_name = google_cloud_run_v2_service.bifrost.name
}
}

View File

@@ -0,0 +1,22 @@
output "service_name" {
description = "Name of the Cloud Run service."
value = google_cloud_run_v2_service.bifrost.name
}
output "service_url" {
description = "URL to access the Bifrost service."
value = (
var.domain_name != null
? "https://${var.domain_name}"
: google_cloud_run_v2_service.bifrost.uri
)
}
output "health_check_url" {
description = "URL to the Bifrost health check endpoint."
value = (
var.domain_name != null
? "https://${var.domain_name}${var.health_check_path}"
: "${google_cloud_run_v2_service.bifrost.uri}${var.health_check_path}"
)
}

View File

@@ -0,0 +1,102 @@
# --- Bifrost ---
variable "service_name" {
description = "Name for the Cloud Run service."
type = string
}
variable "config_json" {
description = "Complete Bifrost config.json as a string."
type = string
sensitive = true
}
variable "image" {
description = "Full Docker image reference (repository:tag)."
type = string
}
variable "container_port" {
description = "Port the Bifrost container listens on."
type = number
}
variable "health_check_path" {
description = "HTTP path for health checks."
type = string
}
# --- GCP ---
variable "project_id" {
description = "GCP project ID."
type = string
}
variable "region" {
description = "GCP region."
type = string
}
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
}
variable "tags" {
description = "Labels to apply to all resources."
type = map(string)
}
variable "service_account" {
description = "GCP service account email for the Cloud Run service."
type = string
}
variable "secret_id" {
description = "Secret Manager secret ID containing config.json."
type = string
}
variable "secret_version" {
description = "Secret Manager secret version for config.json."
type = string
}
# --- Networking ---
variable "vpc_id" {
description = "VPC network self_link (used for VPC connector if needed)."
type = string
}
# --- Compute ---
variable "desired_count" {
description = "Minimum number of Cloud Run instances."
type = number
}
variable "cpu" {
description = "CPU allocation in millicores (e.g. 1000 = 1 vCPU)."
type = number
}
variable "memory" {
description = "Memory allocation in MB."
type = number
}
# --- Scaling ---
variable "max_capacity" {
description = "Maximum number of Cloud Run instances."
type = number
}
# --- Optional features ---
variable "create_load_balancer" {
description = "Create a load balancer for the Cloud Run service."
type = bool
}
variable "domain_name" {
description = "Custom domain name for the service (optional)."
type = string
default = null
}

View File

@@ -0,0 +1,456 @@
# ──────────────────────────────────────────────────────────────────────────────
# GKE service sub-module Bifrost on Google Kubernetes Engine
# ──────────────────────────────────────────────────────────────────────────────
terraform {
required_providers {
google = {
source = "hashicorp/google"
}
kubernetes = {
source = "hashicorp/kubernetes"
}
}
}
locals {
machine_type = coalesce(var.node_machine_type, "e2-standard-4")
}
# ──────────────────────────────────────────────────────────────────────────────
# GKE Cluster (optional skip if create_cluster is false)
# ──────────────────────────────────────────────────────────────────────────────
resource "google_container_cluster" "bifrost" {
count = var.create_cluster ? 1 : 0
name = "${var.name_prefix}-cluster"
project = var.project_id
location = var.region
network = var.vpc_id
subnetwork = var.subnet_id
# We manage the node pool separately
remove_default_node_pool = true
initial_node_count = 1
ip_allocation_policy {
cluster_secondary_range_name = "${var.name_prefix}-pods"
services_secondary_range_name = "${var.name_prefix}-services"
}
master_authorized_networks_config {
cidr_blocks {
cidr_block = var.master_authorized_cidr
display_name = "authorized-network"
}
}
resource_labels = var.tags
}
resource "google_container_node_pool" "bifrost" {
count = var.create_cluster ? 1 : 0
name = "${var.name_prefix}-node-pool"
project = var.project_id
location = var.region
cluster = google_container_cluster.bifrost[0].name
node_count = var.node_count
node_config {
machine_type = local.machine_type
service_account = var.service_account
oauth_scopes = [
"https://www.googleapis.com/auth/cloud-platform",
]
labels = var.tags
tags = ["${var.name_prefix}-node"]
disk_size_gb = 50
disk_type = "pd-standard"
workload_metadata_config {
mode = "GKE_METADATA"
}
}
}
# ──────────────────────────────────────────────────────────────────────────────
# Kubernetes Namespace
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_namespace" "bifrost" {
metadata {
name = var.kubernetes_namespace
labels = var.tags
}
depends_on = [google_container_node_pool.bifrost]
}
# ──────────────────────────────────────────────────────────────────────────────
# Kubernetes Secret config.json
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_secret" "bifrost_config" {
metadata {
name = "${var.name_prefix}-config"
namespace = kubernetes_namespace.bifrost.metadata[0].name
}
data = {
"config.json" = var.config_json
}
type = "Opaque"
}
# ──────────────────────────────────────────────────────────────────────────────
# Persistent Disk + PV + PVC for SQLite storage
# ──────────────────────────────────────────────────────────────────────────────
resource "google_compute_disk" "bifrost" {
name = "${var.name_prefix}-disk"
project = var.project_id
zone = "${var.region}-a"
size = var.volume_size_gb
type = "pd-ssd"
labels = var.tags
lifecycle {
ignore_changes = [labels]
}
}
resource "kubernetes_persistent_volume" "bifrost" {
metadata {
name = "${var.name_prefix}-volume"
}
spec {
capacity = {
storage = "${var.volume_size_gb}Gi"
}
access_modes = ["ReadWriteOnce"]
persistent_volume_reclaim_policy = "Retain"
storage_class_name = "premium-rwo"
persistent_volume_source {
gce_persistent_disk {
pd_name = google_compute_disk.bifrost.name
}
}
node_affinity {
required {
node_selector_term {
match_expressions {
key = "topology.kubernetes.io/zone"
operator = "In"
values = ["${var.region}-a"]
}
}
}
}
}
depends_on = [google_compute_disk.bifrost]
lifecycle {
prevent_destroy = false
}
}
resource "kubernetes_persistent_volume_claim" "bifrost" {
metadata {
name = "${var.name_prefix}-volume-claim"
namespace = kubernetes_namespace.bifrost.metadata[0].name
}
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "${var.volume_size_gb}Gi"
}
}
storage_class_name = "premium-rwo"
volume_name = kubernetes_persistent_volume.bifrost.metadata[0].name
}
depends_on = [kubernetes_persistent_volume.bifrost]
}
# ──────────────────────────────────────────────────────────────────────────────
# Kubernetes Deployment
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_deployment" "bifrost" {
metadata {
name = var.service_name
namespace = kubernetes_namespace.bifrost.metadata[0].name
labels = merge(var.tags, {
app = var.service_name
})
}
spec {
replicas = var.desired_count
selector {
match_labels = {
app = var.service_name
}
}
template {
metadata {
labels = merge(var.tags, {
app = var.service_name
})
}
spec {
security_context {
fs_group = 1000
fs_group_change_policy = "OnRootMismatch"
}
# Init container: fix permissions on the data volume
init_container {
name = "fix-permissions"
image = "busybox:latest"
command = ["sh", "-c", "chown -R 1000:1000 /app/data && chmod -R 755 /app/data"]
security_context {
run_as_user = 0
}
volume_mount {
name = "bifrost-volume"
mount_path = "/app/data"
}
}
# Main Bifrost container
container {
name = "bifrost"
image = var.image
port {
container_port = var.container_port
name = "http"
}
security_context {
run_as_user = 1000
run_as_group = 1000
run_as_non_root = true
allow_privilege_escalation = false
}
resources {
requests = {
cpu = "${var.cpu}m"
memory = "${var.memory}Mi"
}
limits = {
cpu = "${var.cpu * 2}m"
memory = "${var.memory * 2}Mi"
}
}
# Data volume
volume_mount {
name = "bifrost-volume"
mount_path = "/app/data"
}
# Config file mounted via subPath
volume_mount {
name = "config-volume"
mount_path = "/app/data/config.json"
sub_path = "config.json"
}
liveness_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 30
period_seconds = 10
timeout_seconds = 5
failure_threshold = 3
}
readiness_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 10
period_seconds = 5
timeout_seconds = 3
failure_threshold = 3
}
}
# Volumes
volume {
name = "bifrost-volume"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.bifrost.metadata[0].name
}
}
volume {
name = "config-volume"
secret {
secret_name = kubernetes_secret.bifrost_config.metadata[0].name
}
}
}
}
}
depends_on = [
kubernetes_secret.bifrost_config,
kubernetes_persistent_volume_claim.bifrost,
]
}
# ──────────────────────────────────────────────────────────────────────────────
# Kubernetes Service (ClusterIP, port 80 -> 8080)
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_service" "bifrost" {
metadata {
name = var.service_name
namespace = kubernetes_namespace.bifrost.metadata[0].name
labels = merge(var.tags, {
app = var.service_name
})
}
spec {
selector = {
app = var.service_name
}
port {
name = "http"
port = 80
target_port = var.container_port
protocol = "TCP"
}
type = "ClusterIP"
}
}
# ──────────────────────────────────────────────────────────────────────────────
# Horizontal Pod Autoscaler (optional)
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_horizontal_pod_autoscaler_v2" "bifrost" {
count = var.enable_autoscaling ? 1 : 0
metadata {
name = "${var.service_name}-hpa"
namespace = kubernetes_namespace.bifrost.metadata[0].name
}
spec {
scale_target_ref {
api_version = "apps/v1"
kind = "Deployment"
name = kubernetes_deployment.bifrost.metadata[0].name
}
min_replicas = var.min_capacity
max_replicas = var.max_capacity
metric {
type = "Resource"
resource {
name = "cpu"
target {
type = "Utilization"
average_utilization = var.autoscaling_cpu_threshold
}
}
}
metric {
type = "Resource"
resource {
name = "memory"
target {
type = "Utilization"
average_utilization = var.autoscaling_memory_threshold
}
}
}
}
}
# ──────────────────────────────────────────────────────────────────────────────
# Kubernetes Ingress (optional created when create_load_balancer is true)
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_ingress_v1" "bifrost" {
count = var.create_load_balancer ? 1 : 0
metadata {
name = "${var.service_name}-ingress"
namespace = kubernetes_namespace.bifrost.metadata[0].name
annotations = {
"kubernetes.io/ingress.class" = "gce"
"kubernetes.io/ingress.global-static-ip-name" = "${var.name_prefix}-ip"
}
labels = var.tags
}
spec {
rule {
host = var.domain_name
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service.bifrost.metadata[0].name
port {
number = 80
}
}
}
}
}
}
}
}
# Reserve a global static IP for the Ingress
resource "google_compute_global_address" "bifrost" {
count = var.create_load_balancer ? 1 : 0
name = "${var.name_prefix}-ip"
project = var.project_id
}

View File

@@ -0,0 +1,42 @@
output "cluster_name" {
description = "Name of the GKE cluster."
value = try(google_container_cluster.bifrost[0].name, null)
}
output "cluster_endpoint" {
description = "Endpoint of the GKE cluster."
value = try(google_container_cluster.bifrost[0].endpoint, null)
}
output "namespace" {
description = "Kubernetes namespace where Bifrost is deployed."
value = kubernetes_namespace.bifrost.metadata[0].name
}
output "service_name" {
description = "Name of the Kubernetes service."
value = kubernetes_service.bifrost.metadata[0].name
}
output "ingress_ip" {
description = "Static IP address of the Ingress (if domain_name is set)."
value = try(google_compute_global_address.bifrost[0].address, null)
}
output "service_url" {
description = "URL to access the Bifrost service."
value = (
var.domain_name != null
? "http://${var.domain_name}"
: "http://${kubernetes_service.bifrost.metadata[0].name}.${kubernetes_namespace.bifrost.metadata[0].name}.svc.cluster.local"
)
}
output "health_check_url" {
description = "URL to the Bifrost health check endpoint."
value = (
var.domain_name != null
? "http://${var.domain_name}${var.health_check_path}"
: "http://${kubernetes_service.bifrost.metadata[0].name}.${kubernetes_namespace.bifrost.metadata[0].name}.svc.cluster.local${var.health_check_path}"
)
}

View File

@@ -0,0 +1,149 @@
# --- Bifrost ---
variable "service_name" {
description = "Name for the Bifrost Kubernetes service/deployment."
type = string
}
variable "config_json" {
description = "Complete Bifrost config.json as a string."
type = string
sensitive = true
}
variable "image" {
description = "Full Docker image reference (repository:tag)."
type = string
}
variable "container_port" {
description = "Port the Bifrost container listens on."
type = number
}
variable "health_check_path" {
description = "HTTP path for health checks."
type = string
}
# --- GCP ---
variable "project_id" {
description = "GCP project ID."
type = string
}
variable "region" {
description = "GCP region."
type = string
}
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
}
variable "tags" {
description = "Labels to apply to all resources."
type = map(string)
}
variable "service_account" {
description = "GCP service account email for the GKE nodes."
type = string
}
# --- Networking ---
variable "vpc_id" {
description = "VPC network self_link."
type = string
}
variable "subnet_id" {
description = "Subnet self_link."
type = string
}
# --- Compute ---
variable "desired_count" {
description = "Number of pod replicas."
type = number
}
variable "cpu" {
description = "CPU allocation in millicores (e.g. 500)."
type = number
}
variable "memory" {
description = "Memory allocation in MB."
type = number
}
# --- Cluster ---
variable "create_cluster" {
description = "Create a new GKE cluster. Set to false to use an existing cluster."
type = bool
}
variable "kubernetes_namespace" {
description = "Kubernetes namespace to deploy into."
type = string
}
variable "node_count" {
description = "Number of nodes in the GKE node pool."
type = number
}
variable "node_machine_type" {
description = "Machine type for GKE nodes (e.g. e2-standard-4)."
type = string
default = null
}
variable "volume_size_gb" {
description = "Persistent volume size in GB for SQLite storage."
type = number
}
# --- Optional features ---
variable "create_load_balancer" {
description = "Create a load balancer via Kubernetes Ingress."
type = bool
}
variable "enable_autoscaling" {
description = "Enable Horizontal Pod Autoscaler."
type = bool
}
variable "min_capacity" {
description = "Minimum number of replicas when autoscaling is enabled."
type = number
}
variable "max_capacity" {
description = "Maximum number of replicas when autoscaling is enabled."
type = number
}
variable "autoscaling_cpu_threshold" {
description = "Target CPU utilization percentage for autoscaling."
type = number
}
variable "autoscaling_memory_threshold" {
description = "Target memory utilization percentage for autoscaling."
type = number
}
variable "domain_name" {
description = "Custom domain name for the service (optional)."
type = string
default = null
}
variable "master_authorized_cidr" {
description = "CIDR block authorized to access the GKE master endpoint."
type = string
default = "0.0.0.0/0"
}

View File

@@ -0,0 +1,155 @@
# --- Deployment target ---
variable "service" {
description = "GCP service to deploy on (gke or cloud-run)."
type = string
}
# --- Bifrost config ---
variable "config_json" {
description = "Complete Bifrost config.json as a string."
type = string
sensitive = true
}
# --- Image ---
variable "image" {
description = "Full Docker image reference (repository:tag)."
type = string
}
# --- Container ---
variable "container_port" {
description = "Port the Bifrost container listens on."
type = number
}
variable "health_check_path" {
description = "HTTP path for health checks."
type = string
}
# --- GCP project ---
variable "project_id" {
description = "GCP project ID."
type = string
}
# --- Infrastructure ---
variable "region" {
description = "GCP region."
type = string
}
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
}
variable "tags" {
description = "Labels to apply to all resources."
type = map(string)
}
# --- Compute ---
variable "desired_count" {
description = "Number of replicas (K8s pods / Cloud Run instances)."
type = number
}
variable "cpu" {
description = "CPU allocation (K8s: millicores, Cloud Run: vCPUs as millicores)."
type = number
}
variable "memory" {
description = "Memory allocation in MB."
type = number
}
# --- Networking ---
variable "allowed_cidr" {
description = "CIDR block allowed for ingress traffic."
type = string
default = "0.0.0.0/0"
}
variable "existing_vpc_id" {
description = "Existing VPC network self_link. If null, a new VPC will be created."
type = string
default = null
}
variable "existing_subnet_ids" {
description = "Existing subnet self_links. If null, new subnets will be created. Must be provided together with existing_vpc_id."
type = list(string)
default = null
validation {
condition = var.existing_subnet_ids == null || length(var.existing_subnet_ids) > 0
error_message = "existing_subnet_ids must be null or a non-empty list."
}
}
# --- Optional features ---
variable "create_load_balancer" {
description = "Create a load balancer (GKE Ingress / Cloud Run domain mapping)."
type = bool
}
variable "enable_autoscaling" {
description = "Enable autoscaling for the service."
type = bool
}
variable "min_capacity" {
description = "Minimum number of replicas when autoscaling is enabled."
type = number
}
variable "max_capacity" {
description = "Maximum number of replicas when autoscaling is enabled."
type = number
}
variable "autoscaling_cpu_threshold" {
description = "Target CPU utilization percentage for autoscaling."
type = number
}
variable "autoscaling_memory_threshold" {
description = "Target memory utilization percentage for autoscaling."
type = number
}
variable "domain_name" {
description = "Custom domain name for the service (optional)."
type = string
default = null
}
# --- K8s-specific (GKE) ---
variable "create_cluster" {
description = "Create a new GKE cluster. Set to false to use an existing cluster."
type = bool
}
variable "kubernetes_namespace" {
description = "Kubernetes namespace to deploy into."
type = string
}
variable "node_count" {
description = "Number of nodes in the GKE node pool."
type = number
}
variable "node_machine_type" {
description = "Machine type for GKE nodes (e.g. e2-standard-4)."
type = string
default = null
}
variable "volume_size_gb" {
description = "Persistent volume size in GB for SQLite storage."
type = number
}

View File

@@ -0,0 +1,320 @@
# ──────────────────────────────────────────────────────────────────────────────
# Generic Kubernetes module Bifrost on any K8s cluster
# ──────────────────────────────────────────────────────────────────────────────
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.0"
}
}
}
# ──────────────────────────────────────────────────────────────────────────────
# Kubernetes Namespace
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_namespace_v1" "bifrost" {
metadata {
name = var.kubernetes_namespace
labels = var.tags
}
}
# ──────────────────────────────────────────────────────────────────────────────
# Kubernetes Secret config.json
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_secret_v1" "bifrost_config" {
metadata {
name = "${var.name_prefix}-config"
namespace = kubernetes_namespace_v1.bifrost.metadata[0].name
}
data = {
"config.json" = var.config_json
}
type = "Opaque"
}
# ──────────────────────────────────────────────────────────────────────────────
# Persistent Volume Claim (dynamic provisioning via StorageClass)
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_persistent_volume_claim_v1" "bifrost" {
metadata {
name = "${var.name_prefix}-volume-claim"
namespace = kubernetes_namespace_v1.bifrost.metadata[0].name
}
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "${var.volume_size_gb}Gi"
}
}
storage_class_name = var.storage_class_name
}
}
# ──────────────────────────────────────────────────────────────────────────────
# Kubernetes Deployment
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_deployment_v1" "bifrost" {
metadata {
name = var.service_name
namespace = kubernetes_namespace_v1.bifrost.metadata[0].name
labels = merge(var.tags, {
app = var.service_name
})
}
spec {
replicas = var.desired_count
selector {
match_labels = {
app = var.service_name
}
}
template {
metadata {
labels = merge(var.tags, {
app = var.service_name
})
}
spec {
security_context {
fs_group = 1000
fs_group_change_policy = "OnRootMismatch"
}
# Init container: fix permissions on the data volume
init_container {
name = "fix-permissions"
image = "busybox:latest"
command = ["sh", "-c", "chown -R 1000:1000 /app/data && chmod -R 755 /app/data"]
security_context {
run_as_user = 0
}
volume_mount {
name = "bifrost-volume"
mount_path = "/app/data"
}
}
# Main Bifrost container
container {
name = "bifrost"
image = var.image
port {
container_port = var.container_port
name = "http"
}
security_context {
run_as_user = 1000
run_as_group = 1000
run_as_non_root = true
allow_privilege_escalation = false
}
resources {
requests = {
cpu = "${var.cpu}m"
memory = "${var.memory}Mi"
}
limits = {
cpu = "${var.cpu * 2}m"
memory = "${var.memory * 2}Mi"
}
}
# Data volume
volume_mount {
name = "bifrost-volume"
mount_path = "/app/data"
}
# Config file mounted via subPath
volume_mount {
name = "config-volume"
mount_path = "/app/data/config.json"
sub_path = "config.json"
}
liveness_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 30
period_seconds = 10
timeout_seconds = 5
failure_threshold = 3
}
readiness_probe {
http_get {
path = var.health_check_path
port = var.container_port
}
initial_delay_seconds = 10
period_seconds = 5
timeout_seconds = 3
failure_threshold = 3
}
}
# Volumes
volume {
name = "bifrost-volume"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim_v1.bifrost.metadata[0].name
}
}
volume {
name = "config-volume"
secret {
secret_name = kubernetes_secret_v1.bifrost_config.metadata[0].name
}
}
}
}
}
depends_on = [
kubernetes_secret_v1.bifrost_config,
kubernetes_persistent_volume_claim_v1.bifrost,
]
}
# ──────────────────────────────────────────────────────────────────────────────
# Kubernetes Service (ClusterIP, port 80 -> 8080)
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_service_v1" "bifrost" {
metadata {
name = var.service_name
namespace = kubernetes_namespace_v1.bifrost.metadata[0].name
labels = merge(var.tags, {
app = var.service_name
})
}
spec {
selector = {
app = var.service_name
}
port {
name = "http"
port = 80
target_port = var.container_port
protocol = "TCP"
}
type = "ClusterIP"
}
}
# ──────────────────────────────────────────────────────────────────────────────
# Horizontal Pod Autoscaler (optional)
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_horizontal_pod_autoscaler_v2" "bifrost" {
count = var.enable_autoscaling ? 1 : 0
metadata {
name = "${var.service_name}-hpa"
namespace = kubernetes_namespace_v1.bifrost.metadata[0].name
}
spec {
scale_target_ref {
api_version = "apps/v1"
kind = "Deployment"
name = kubernetes_deployment_v1.bifrost.metadata[0].name
}
min_replicas = var.min_capacity
max_replicas = var.max_capacity
metric {
type = "Resource"
resource {
name = "cpu"
target {
type = "Utilization"
average_utilization = var.autoscaling_cpu_threshold
}
}
}
metric {
type = "Resource"
resource {
name = "memory"
target {
type = "Utilization"
average_utilization = var.autoscaling_memory_threshold
}
}
}
}
}
# ──────────────────────────────────────────────────────────────────────────────
# Kubernetes Ingress (optional when create_load_balancer is true)
# ──────────────────────────────────────────────────────────────────────────────
resource "kubernetes_ingress_v1" "bifrost" {
count = var.create_load_balancer && var.domain_name != null ? 1 : 0
metadata {
name = "${var.service_name}-ingress"
namespace = kubernetes_namespace_v1.bifrost.metadata[0].name
annotations = var.ingress_annotations
labels = var.tags
}
spec {
ingress_class_name = var.ingress_class_name
rule {
host = var.domain_name
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service_v1.bifrost.metadata[0].name
port {
number = 80
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
output "namespace" {
description = "Kubernetes namespace where Bifrost is deployed."
value = kubernetes_namespace_v1.bifrost.metadata[0].name
}
output "service_name" {
description = "Name of the Kubernetes service."
value = kubernetes_service_v1.bifrost.metadata[0].name
}
output "service_url" {
description = "URL to access the Bifrost service."
value = (
var.domain_name != null
? "http://${var.domain_name}"
: "http://${kubernetes_service_v1.bifrost.metadata[0].name}.${kubernetes_namespace_v1.bifrost.metadata[0].name}.svc.cluster.local"
)
}
output "health_check_url" {
description = "URL to the Bifrost health check endpoint."
value = (
var.domain_name != null
? "http://${var.domain_name}/health"
: "http://${kubernetes_service_v1.bifrost.metadata[0].name}.${kubernetes_namespace_v1.bifrost.metadata[0].name}.svc.cluster.local/health"
)
}

View File

@@ -0,0 +1,120 @@
# --- Bifrost ---
variable "service_name" {
description = "Name for the Bifrost Kubernetes service/deployment."
type = string
}
variable "config_json" {
description = "Complete Bifrost config.json as a string."
type = string
sensitive = true
}
variable "image" {
description = "Full Docker image reference (repository:tag)."
type = string
}
variable "container_port" {
description = "Port the Bifrost container listens on."
type = number
}
variable "health_check_path" {
description = "HTTP path for health checks."
type = string
}
# --- Naming ---
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
}
variable "tags" {
description = "Labels to apply to all resources."
type = map(string)
}
# --- Compute ---
variable "desired_count" {
description = "Number of pod replicas."
type = number
}
variable "cpu" {
description = "CPU allocation in millicores (e.g. 500)."
type = number
}
variable "memory" {
description = "Memory allocation in MB."
type = number
}
# --- Kubernetes ---
variable "kubernetes_namespace" {
description = "Kubernetes namespace to deploy into."
type = string
}
variable "volume_size_gb" {
description = "Persistent volume claim size in GB."
type = number
}
variable "storage_class_name" {
description = "Kubernetes StorageClass name for dynamic PVC provisioning (e.g. standard, gp2, premium-rwo)."
type = string
default = "standard"
}
# --- Optional features ---
variable "create_load_balancer" {
description = "Create a Kubernetes Ingress resource."
type = bool
}
variable "enable_autoscaling" {
description = "Enable Horizontal Pod Autoscaler."
type = bool
}
variable "min_capacity" {
description = "Minimum number of replicas when autoscaling is enabled."
type = number
}
variable "max_capacity" {
description = "Maximum number of replicas when autoscaling is enabled."
type = number
}
variable "autoscaling_cpu_threshold" {
description = "Target CPU utilization percentage for autoscaling."
type = number
}
variable "autoscaling_memory_threshold" {
description = "Target memory utilization percentage for autoscaling."
type = number
}
variable "domain_name" {
description = "Custom domain name for the Ingress host rule (optional)."
type = string
default = null
}
# --- Ingress ---
variable "ingress_class_name" {
description = "Ingress class name (e.g. nginx, traefik, haproxy)."
type = string
default = "nginx"
}
variable "ingress_annotations" {
description = "Annotations to add to the Ingress resource."
type = map(string)
default = {}
}

View File

@@ -0,0 +1,204 @@
terraform {
required_version = ">= 1.7"
}
# Default provider configuration for azurerm.
# The azurerm provider requires a `features {}` block even when no Azure
# resources are being created (count=0). This default satisfies that
# requirement so AWS/GCP/K8s users don't need to configure azurerm.
# Azure users: configure azurerm in your root module — it will override this.
provider "azurerm" {
features {}
skip_provider_registration = true
}
locals {
# Load base config from file or inline string (decoded to map)
base_config = (
var.config_json_file != null ? jsondecode(file(var.config_json_file)) :
var.config_json != null ? jsondecode(var.config_json) :
{}
)
# Terraform variable overrides (non-null values only)
overrides = {
for k, v in {
"$schema" = "https://www.getbifrost.ai/schema"
encryption_key = var.encryption_key
auth_config = var.auth_config
client = var.client
framework = var.framework
providers = var.providers_config
governance = var.governance
mcp = var.mcp
vector_store = var.vector_store
config_store = var.config_store
logs_store = var.logs_store
cluster_config = var.cluster_config
scim_config = var.scim_config
load_balancer_config = var.load_balancer_config
guardrails_config = var.guardrails_config
plugins = var.plugins
audit_logs = var.audit_logs
websocket = var.websocket
} : k => v if v != null
}
# Merge: base config + overrides (overrides win at top-level key)
config_json = jsonencode(merge(local.base_config, local.overrides))
# Valid cloud_provider → service combinations
valid_services = {
aws = ["ecs", "eks"]
gcp = ["gke", "cloud-run"]
azure = ["aks", "aci"]
kubernetes = ["deployment"]
}
image = "${var.image_repository}:${var.image_tag}"
container_port = 8080
health_check_path = "/health"
}
# --- Validate cloud_provider + service combination ---
resource "terraform_data" "validate_service_combination" {
lifecycle {
precondition {
condition = contains(local.valid_services[var.cloud_provider], var.service)
error_message = "Invalid service '${var.service}' for cloud_provider '${var.cloud_provider}'. Valid services: ${join(", ", local.valid_services[var.cloud_provider])}."
}
}
}
# --- AWS ---
module "aws" {
source = "./aws"
count = var.cloud_provider == "aws" ? 1 : 0
service = var.service
config_json = local.config_json
image = local.image
container_port = local.container_port
health_check_path = local.health_check_path
region = var.region
name_prefix = var.name_prefix
tags = var.tags
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
existing_vpc_id = var.existing_vpc_id
existing_subnet_ids = var.existing_subnet_ids
allowed_cidr = var.allowed_cidr
existing_security_group_ids = var.existing_security_group_ids
create_load_balancer = var.create_load_balancer
assign_public_ip = var.assign_public_ip
enable_autoscaling = var.enable_autoscaling
min_capacity = var.min_capacity
max_capacity = var.max_capacity
autoscaling_cpu_threshold = var.autoscaling_cpu_threshold
autoscaling_memory_threshold = var.autoscaling_memory_threshold
domain_name = var.domain_name
certificate_arn = var.certificate_arn
create_cluster = var.create_cluster
kubernetes_namespace = var.kubernetes_namespace
node_count = var.node_count
node_machine_type = var.node_machine_type
volume_size_gb = var.volume_size_gb
}
# --- GCP ---
module "gcp" {
source = "./gcp"
count = var.cloud_provider == "gcp" ? 1 : 0
service = var.service
config_json = local.config_json
image = local.image
container_port = local.container_port
health_check_path = local.health_check_path
project_id = var.gcp_project_id
region = var.region
name_prefix = var.name_prefix
tags = var.tags
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
allowed_cidr = var.allowed_cidr
existing_vpc_id = var.existing_vpc_id
existing_subnet_ids = var.existing_subnet_ids
create_load_balancer = var.create_load_balancer
enable_autoscaling = var.enable_autoscaling
min_capacity = var.min_capacity
max_capacity = var.max_capacity
autoscaling_cpu_threshold = var.autoscaling_cpu_threshold
autoscaling_memory_threshold = var.autoscaling_memory_threshold
domain_name = var.domain_name
create_cluster = var.create_cluster
kubernetes_namespace = var.kubernetes_namespace
node_count = var.node_count
node_machine_type = var.node_machine_type
volume_size_gb = var.volume_size_gb
}
# --- Azure ---
module "azure" {
source = "./azure"
count = var.cloud_provider == "azure" ? 1 : 0
service = var.service
config_json = local.config_json
image = local.image
container_port = local.container_port
health_check_path = local.health_check_path
region = var.region
name_prefix = var.name_prefix
tags = var.tags
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
allowed_cidr = var.allowed_cidr
existing_vpc_id = var.existing_vpc_id
existing_subnet_ids = var.existing_subnet_ids
create_load_balancer = var.create_load_balancer
enable_autoscaling = var.enable_autoscaling
min_capacity = var.min_capacity
max_capacity = var.max_capacity
autoscaling_cpu_threshold = var.autoscaling_cpu_threshold
autoscaling_memory_threshold = var.autoscaling_memory_threshold
domain_name = var.domain_name
create_cluster = var.create_cluster
kubernetes_namespace = var.kubernetes_namespace
node_count = var.node_count
node_machine_type = var.node_machine_type
volume_size_gb = var.volume_size_gb
resource_group_name = var.azure_resource_group_name
}
# --- Generic Kubernetes ---
module "kubernetes" {
source = "./kubernetes"
count = var.cloud_provider == "kubernetes" ? 1 : 0
service_name = var.name_prefix
config_json = local.config_json
image = local.image
container_port = local.container_port
health_check_path = local.health_check_path
name_prefix = var.name_prefix
tags = var.tags
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
kubernetes_namespace = var.kubernetes_namespace
volume_size_gb = var.volume_size_gb
create_load_balancer = var.create_load_balancer
enable_autoscaling = var.enable_autoscaling
min_capacity = var.min_capacity
max_capacity = var.max_capacity
autoscaling_cpu_threshold = var.autoscaling_cpu_threshold
autoscaling_memory_threshold = var.autoscaling_memory_threshold
domain_name = var.domain_name
storage_class_name = var.storage_class_name
ingress_class_name = var.ingress_class_name
ingress_annotations = var.ingress_annotations
}

View File

@@ -0,0 +1,25 @@
output "service_url" {
description = "URL to access the Bifrost service."
value = try(coalesce(
try(module.aws[0].service_url, null),
try(module.gcp[0].service_url, null),
try(module.azure[0].service_url, null),
try(module.kubernetes[0].service_url, null),
), null)
}
output "health_check_url" {
description = "URL to the Bifrost health check endpoint."
value = try(coalesce(
try(module.aws[0].health_check_url, null),
try(module.gcp[0].health_check_url, null),
try(module.azure[0].health_check_url, null),
try(module.kubernetes[0].health_check_url, null),
), null)
}
output "config_json" {
description = "The resolved Bifrost configuration JSON (for debugging)."
value = local.config_json
sensitive = true
}

View File

@@ -0,0 +1,128 @@
# =============================================================================
# AWS ECS Module Tests
# =============================================================================
mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
}
mock_data "aws_caller_identity" {
defaults = { account_id = "123456789012" }
}
mock_data "aws_region" {
defaults = { name = "us-east-1" }
}
mock_data "aws_iam_policy_document" {
defaults = { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" }
}
}
mock_provider "google" {}
mock_provider "azurerm" {
mock_data "azurerm_client_config" {
defaults = {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "00000000-0000-0000-0000-000000000000"
object_id = "00000000-0000-0000-0000-000000000000"
}
}
}
mock_provider "kubernetes" {}
run "ecs_basic" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
}
}
run "ecs_no_alb_by_default" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
create_load_balancer = false
}
}
run "ecs_with_alb" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
create_load_balancer = true
}
}
run "ecs_no_autoscaling_by_default" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
}
}
run "ecs_with_autoscaling" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
enable_autoscaling = true
min_capacity = 2
max_capacity = 8
}
}
run "ecs_custom_compute" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
cpu = 1024
memory = 2048
desired_count = 3
}
}
run "ecs_existing_vpc" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
existing_vpc_id = "vpc-12345"
existing_subnet_ids = ["subnet-aaa", "subnet-bbb"]
}
}
run "ecs_private_subnet" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
assign_public_ip = false
}
}

View File

@@ -0,0 +1,142 @@
# =============================================================================
# AWS EKS Module Tests
# =============================================================================
mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
}
mock_data "aws_caller_identity" {
defaults = { account_id = "123456789012" }
}
mock_data "aws_region" {
defaults = { name = "us-east-1" }
}
mock_data "aws_iam_policy_document" {
defaults = { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" }
}
}
mock_provider "google" {}
mock_provider "azurerm" {
mock_data "azurerm_client_config" {
defaults = {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "00000000-0000-0000-0000-000000000000"
object_id = "00000000-0000-0000-0000-000000000000"
}
}
}
mock_provider "kubernetes" {}
run "eks_basic" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
}
}
run "eks_create_cluster" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
create_cluster = true
}
}
run "eks_skip_cluster" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
create_cluster = false
}
}
run "eks_custom_namespace" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
kubernetes_namespace = "custom-ns"
}
}
run "eks_with_hpa" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
enable_autoscaling = true
min_capacity = 2
max_capacity = 10
}
}
run "eks_with_ingress" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
create_load_balancer = true
domain_name = "bifrost.example.com"
}
}
run "eks_with_https" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
create_load_balancer = true
domain_name = "bifrost.example.com"
certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/abc-123"
}
}
run "eks_custom_nodes" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
node_count = 5
node_machine_type = "t3.large"
}
}
run "eks_custom_volume" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
volume_size_gb = 50
}
}

View File

@@ -0,0 +1,112 @@
# =============================================================================
# AWS Shared Infrastructure Tests — VPC, SG, Secrets Manager conditionality
# =============================================================================
mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
}
mock_data "aws_caller_identity" {
defaults = { account_id = "123456789012" }
}
mock_data "aws_region" {
defaults = { name = "us-east-1" }
}
mock_data "aws_iam_policy_document" {
defaults = { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" }
}
}
mock_provider "google" {}
mock_provider "azurerm" {
mock_data "azurerm_client_config" {
defaults = {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "00000000-0000-0000-0000-000000000000"
object_id = "00000000-0000-0000-0000-000000000000"
}
}
}
mock_provider "kubernetes" {}
run "creates_vpc" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
}
}
run "skips_vpc_when_existing" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
existing_vpc_id = "vpc-12345"
existing_subnet_ids = ["subnet-aaa", "subnet-bbb"]
}
}
run "skips_sg_when_existing" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
existing_security_group_ids = ["sg-12345"]
}
}
run "custom_cidr" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
allowed_cidr = "10.0.0.0/8"
}
}
run "custom_prefix" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
name_prefix = "my-gateway"
}
}
run "tags_applied" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
tags = { Environment = "test", Team = "platform" }
}
}
run "eks_no_ecs_resources" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
}
}

View File

@@ -0,0 +1,96 @@
# =============================================================================
# Azure ACI Module Tests
# =============================================================================
mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
}
mock_data "aws_caller_identity" {
defaults = { account_id = "123456789012" }
}
mock_data "aws_region" {
defaults = { name = "us-east-1" }
}
mock_data "aws_iam_policy_document" {
defaults = { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" }
}
}
mock_provider "google" {}
mock_provider "azurerm" {
mock_data "azurerm_client_config" {
defaults = {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "00000000-0000-0000-0000-000000000000"
object_id = "00000000-0000-0000-0000-000000000000"
}
}
mock_data "azurerm_resource_group" {
defaults = {
name = "existing-rg"
location = "eastus"
}
}
}
mock_provider "kubernetes" {}
run "aci_basic" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aci"
region = "eastus"
config_json = jsonencode({})
}
}
run "aci_custom_compute" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aci"
region = "eastus"
config_json = jsonencode({})
cpu = 1000
memory = 2048
}
}
run "aci_existing_resource_group" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aci"
region = "eastus"
config_json = jsonencode({})
azure_resource_group_name = "existing-rg"
}
}
run "aci_existing_vnet" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aci"
region = "eastus"
config_json = jsonencode({})
existing_vpc_id = "/subscriptions/00000000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet"
existing_subnet_ids = ["/subscriptions/00000000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet1"]
}
}
run "aci_custom_prefix" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aci"
region = "eastus"
config_json = jsonencode({})
name_prefix = "my-bifrost"
}
}

View File

@@ -0,0 +1,159 @@
# =============================================================================
# Azure AKS Module Tests
# =============================================================================
mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
}
mock_data "aws_caller_identity" {
defaults = { account_id = "123456789012" }
}
mock_data "aws_region" {
defaults = { name = "us-east-1" }
}
mock_data "aws_iam_policy_document" {
defaults = { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" }
}
}
mock_provider "google" {}
mock_provider "azurerm" {
mock_data "azurerm_client_config" {
defaults = {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "00000000-0000-0000-0000-000000000000"
object_id = "00000000-0000-0000-0000-000000000000"
}
}
mock_data "azurerm_resource_group" {
defaults = {
name = "existing-rg"
location = "eastus"
}
}
}
mock_provider "kubernetes" {}
run "aks_basic" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
}
}
run "aks_create_cluster" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
create_cluster = true
}
}
run "aks_skip_cluster" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
create_cluster = false
}
}
run "aks_custom_namespace" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
kubernetes_namespace = "custom-ns"
}
}
run "aks_with_hpa" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
enable_autoscaling = true
min_capacity = 2
max_capacity = 10
}
}
run "aks_with_ingress" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
create_load_balancer = true
domain_name = "bifrost.example.com"
}
}
run "aks_custom_nodes" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
node_count = 5
node_machine_type = "Standard_D4s_v3"
}
}
run "aks_custom_volume" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
volume_size_gb = 50
}
}
run "aks_existing_resource_group" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
azure_resource_group_name = "existing-rg"
}
}
run "aks_existing_vnet" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
existing_vpc_id = "/subscriptions/00000000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet"
existing_subnet_ids = ["/subscriptions/00000000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet1"]
}
}

View File

@@ -0,0 +1,115 @@
# =============================================================================
# Config Merging Tests — precedence: base JSON → variable overrides → $schema
# =============================================================================
mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
}
mock_data "aws_caller_identity" {
defaults = { account_id = "123456789012" }
}
mock_data "aws_region" {
defaults = { name = "us-east-1" }
}
mock_data "aws_iam_policy_document" {
defaults = { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" }
}
}
mock_provider "google" {}
mock_provider "azurerm" {
mock_data "azurerm_client_config" {
defaults = {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "00000000-0000-0000-0000-000000000000"
object_id = "00000000-0000-0000-0000-000000000000"
}
}
}
mock_provider "kubernetes" {}
run "base_config_preserved" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({ encryption_key = "base-key" })
}
assert {
condition = jsondecode(output.config_json)["encryption_key"] == "base-key"
error_message = "Base config key should be preserved"
}
}
run "override_wins" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({ encryption_key = "base-key" })
encryption_key = "override-key"
}
assert {
condition = jsondecode(output.config_json)["encryption_key"] == "override-key"
error_message = "Variable override should win over base config"
}
}
run "schema_url_injected" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
}
assert {
condition = jsondecode(output.config_json)["$schema"] == "https://www.getbifrost.ai/schema"
error_message = "Schema URL should always be injected"
}
}
run "no_base_config" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
encryption_key = "standalone-key"
}
assert {
condition = jsondecode(output.config_json)["encryption_key"] == "standalone-key"
error_message = "Config should work with no base, only overrides"
}
}
run "multiple_overrides" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({ encryption_key = "base-key", framework = "test" })
encryption_key = "new-key"
governance = { budgets = [] }
}
assert {
condition = jsondecode(output.config_json)["encryption_key"] == "new-key"
error_message = "encryption_key override should apply"
}
assert {
condition = jsondecode(output.config_json)["governance"] != null
error_message = "governance override should be present"
}
assert {
condition = jsondecode(output.config_json)["framework"] == "test"
error_message = "Non-overridden base keys should be preserved"
}
}

View File

@@ -0,0 +1,95 @@
# =============================================================================
# GCP Cloud Run Module Tests
# =============================================================================
mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
}
mock_data "aws_caller_identity" {
defaults = { account_id = "123456789012" }
}
mock_data "aws_region" {
defaults = { name = "us-east-1" }
}
mock_data "aws_iam_policy_document" {
defaults = { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" }
}
}
mock_provider "google" {}
mock_provider "azurerm" {
mock_data "azurerm_client_config" {
defaults = {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "00000000-0000-0000-0000-000000000000"
object_id = "00000000-0000-0000-0000-000000000000"
}
}
}
mock_provider "kubernetes" {}
run "cloudrun_basic" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "cloud-run"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
}
}
run "cloudrun_with_public_access" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "cloud-run"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
create_load_balancer = true
}
}
run "cloudrun_with_domain" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "cloud-run"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
domain_name = "bifrost.example.com"
}
}
run "cloudrun_scaling" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "cloud-run"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
desired_count = 2
max_capacity = 20
}
}
run "cloudrun_custom_compute" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "cloud-run"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
cpu = 1000
memory = 2048
}
}

View File

@@ -0,0 +1,150 @@
# =============================================================================
# GCP GKE Module Tests
# =============================================================================
mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
}
mock_data "aws_caller_identity" {
defaults = { account_id = "123456789012" }
}
mock_data "aws_region" {
defaults = { name = "us-east-1" }
}
mock_data "aws_iam_policy_document" {
defaults = { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" }
}
}
mock_provider "google" {}
mock_provider "azurerm" {
mock_data "azurerm_client_config" {
defaults = {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "00000000-0000-0000-0000-000000000000"
object_id = "00000000-0000-0000-0000-000000000000"
}
}
}
mock_provider "kubernetes" {}
run "gke_basic" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "gke"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
}
}
run "gke_create_cluster" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "gke"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
create_cluster = true
}
}
run "gke_skip_cluster" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "gke"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
create_cluster = false
}
}
run "gke_custom_namespace" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "gke"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
kubernetes_namespace = "custom-ns"
}
}
run "gke_with_hpa" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "gke"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
enable_autoscaling = true
min_capacity = 2
max_capacity = 10
}
}
run "gke_with_ingress" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "gke"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
create_load_balancer = true
domain_name = "bifrost.example.com"
}
}
run "gke_custom_nodes" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "gke"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
node_count = 5
node_machine_type = "e2-standard-8"
}
}
run "gke_custom_volume" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "gke"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
volume_size_gb = 50
}
}
run "gke_existing_vpc" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "gke"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
existing_vpc_id = "projects/test/global/networks/existing-vpc"
existing_subnet_ids = ["projects/test/regions/us-central1/subnetworks/existing-subnet"]
}
}

View File

@@ -0,0 +1,157 @@
# =============================================================================
# Generic Kubernetes Module Tests
# =============================================================================
mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
}
mock_data "aws_caller_identity" {
defaults = { account_id = "123456789012" }
}
mock_data "aws_region" {
defaults = { name = "us-east-1" }
}
mock_data "aws_iam_policy_document" {
defaults = { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" }
}
}
mock_provider "google" {}
mock_provider "azurerm" {
mock_data "azurerm_client_config" {
defaults = {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "00000000-0000-0000-0000-000000000000"
object_id = "00000000-0000-0000-0000-000000000000"
}
}
}
mock_provider "kubernetes" {}
run "k8s_basic" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
}
}
run "k8s_custom_namespace" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
kubernetes_namespace = "custom-ns"
}
}
run "k8s_custom_storage_class" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
storage_class_name = "gp2"
}
}
run "k8s_with_hpa" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
enable_autoscaling = true
min_capacity = 2
max_capacity = 10
}
}
run "k8s_with_ingress" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
create_load_balancer = true
domain_name = "bifrost.example.com"
}
}
run "k8s_custom_ingress_class" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
create_load_balancer = true
domain_name = "bifrost.example.com"
ingress_class_name = "traefik"
}
}
run "k8s_ingress_annotations" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
create_load_balancer = true
domain_name = "bifrost.example.com"
ingress_annotations = { "cert-manager.io/cluster-issuer" = "letsencrypt" }
}
}
run "k8s_custom_compute" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
cpu = 1000
memory = 2048
desired_count = 3
}
}
run "k8s_custom_volume" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
volume_size_gb = 50
}
}
run "k8s_tags" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
tags = { Environment = "staging", Team = "platform" }
}
}

View File

@@ -0,0 +1,145 @@
# =============================================================================
# Root Module Validation Tests
#
# Tests variable validation rules and cross-validation of cloud_provider + service.
# Validation failures happen before provider calls, so no mock_provider needed.
# Valid-combination tests use the wrapper module with mock_provider.
# =============================================================================
mock_provider "aws" {
mock_data "aws_availability_zones" {
defaults = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
}
mock_data "aws_caller_identity" {
defaults = { account_id = "123456789012" }
}
mock_data "aws_region" {
defaults = { name = "us-east-1" }
}
mock_data "aws_iam_policy_document" {
defaults = { json = "{\"Version\":\"2012-10-17\",\"Statement\":[]}" }
}
}
mock_provider "google" {}
mock_provider "azurerm" {
mock_data "azurerm_client_config" {
defaults = {
tenant_id = "00000000-0000-0000-0000-000000000000"
subscription_id = "00000000-0000-0000-0000-000000000000"
object_id = "00000000-0000-0000-0000-000000000000"
}
}
}
mock_provider "kubernetes" {}
# --- Valid combinations (all 7 services) ---
run "valid_aws_ecs" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
}
}
run "valid_aws_eks" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "eks"
region = "us-east-1"
config_json = jsonencode({})
}
}
run "valid_gcp_gke" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "gke"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
}
}
run "valid_gcp_cloud_run" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "gcp"
service = "cloud-run"
region = "us-central1"
gcp_project_id = "test-project"
config_json = jsonencode({})
}
}
run "valid_azure_aks" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aks"
region = "eastus"
config_json = jsonencode({})
}
}
run "valid_azure_aci" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "azure"
service = "aci"
region = "eastus"
config_json = jsonencode({})
}
}
run "valid_kubernetes_deployment" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "kubernetes"
service = "deployment"
region = "local"
config_json = jsonencode({})
}
}
# --- Invalid inputs (use wrapper — expect_failures references wrapper vars) ---
run "invalid_cloud_provider_rejected" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "digitalocean"
service = "ecs"
region = "us-east-1"
config_json = jsonencode({})
}
expect_failures = [var.cloud_provider]
}
run "invalid_service_rejected" {
command = plan
module { source = "./tests/setup" }
variables {
cloud_provider = "aws"
service = "lambda"
region = "us-east-1"
config_json = jsonencode({})
}
expect_failures = [var.service]
}
# NOTE: Cross-validation tests (aws+gke, gcp+ecs, etc.) cannot use
# expect_failures with nested module resources — a Terraform limitation.
# The cross-validation precondition is exercised implicitly: all 7 valid
# combos above pass, and any mismatch would fail at the precondition.

View File

@@ -0,0 +1,107 @@
# =============================================================================
# Test Wrapper Module
#
# This module exists solely to declare all required_providers in one place
# for the Terraform test framework. The root bifrost module intentionally
# does NOT declare required_providers so users only need to configure the
# provider for their chosen cloud.
#
# Test files use: module { source = "./tests/setup" } in run blocks.
# =============================================================================
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
google = {
source = "hashicorp/google"
version = ">= 5.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.0"
}
}
}
module "bifrost" {
source = "../../"
# Deployment target
cloud_provider = var.cloud_provider
service = var.service
# Config
config_json = var.config_json
config_json_file = var.config_json_file
encryption_key = var.encryption_key
auth_config = var.auth_config
client = var.client
framework = var.framework
providers_config = var.providers_config
governance = var.governance
mcp = var.mcp
vector_store = var.vector_store
config_store = var.config_store
logs_store = var.logs_store
cluster_config = var.cluster_config
scim_config = var.scim_config
load_balancer_config = var.load_balancer_config
guardrails_config = var.guardrails_config
plugins = var.plugins
audit_logs = var.audit_logs
websocket = var.websocket
# Image
image_tag = var.image_tag
image_repository = var.image_repository
# Infrastructure
region = var.region
name_prefix = var.name_prefix
tags = var.tags
# Compute
desired_count = var.desired_count
cpu = var.cpu
memory = var.memory
# Networking
existing_vpc_id = var.existing_vpc_id
existing_subnet_ids = var.existing_subnet_ids
allowed_cidr = var.allowed_cidr
existing_security_group_ids = var.existing_security_group_ids
# Features
create_load_balancer = var.create_load_balancer
assign_public_ip = var.assign_public_ip
enable_autoscaling = var.enable_autoscaling
min_capacity = var.min_capacity
max_capacity = var.max_capacity
autoscaling_cpu_threshold = var.autoscaling_cpu_threshold
autoscaling_memory_threshold = var.autoscaling_memory_threshold
domain_name = var.domain_name
certificate_arn = var.certificate_arn
# K8s-specific
create_cluster = var.create_cluster
kubernetes_namespace = var.kubernetes_namespace
node_count = var.node_count
node_machine_type = var.node_machine_type
volume_size_gb = var.volume_size_gb
# Cloud-specific
gcp_project_id = var.gcp_project_id
azure_resource_group_name = var.azure_resource_group_name
# Generic K8s
storage_class_name = var.storage_class_name
ingress_class_name = var.ingress_class_name
ingress_annotations = var.ingress_annotations
}

View File

@@ -0,0 +1,12 @@
output "service_url" {
value = module.bifrost.service_url
}
output "health_check_url" {
value = module.bifrost.health_check_url
}
output "config_json" {
value = module.bifrost.config_json
sensitive = true
}

View File

@@ -0,0 +1,97 @@
# All variables pass through to the bifrost module.
# Defaults match the parent module's defaults.
variable "cloud_provider" { type = string }
variable "service" { type = string }
variable "region" { type = string }
variable "config_json" {
type = string
default = null
sensitive = true
}
variable "config_json_file" { default = null }
variable "encryption_key" {
type = string
default = null
sensitive = true
}
variable "auth_config" {
type = any
default = null
sensitive = true
}
variable "providers_config" {
type = any
default = null
sensitive = true
}
variable "client" { default = null }
variable "framework" { default = null }
variable "governance" { default = null }
variable "mcp" { default = null }
variable "vector_store" { default = null }
variable "config_store" { default = null }
variable "logs_store" { default = null }
variable "cluster_config" { default = null }
variable "scim_config" { default = null }
variable "load_balancer_config" { default = null }
variable "guardrails_config" { default = null }
variable "plugins" { default = null }
variable "audit_logs" { default = null }
variable "websocket" { default = null }
variable "image_tag" { default = "latest" }
variable "image_repository" { default = "maximhq/bifrost" }
variable "name_prefix" { default = "bifrost" }
variable "tags" {
type = map(string)
default = {}
}
variable "desired_count" { default = 1 }
variable "cpu" { default = 512 }
variable "memory" { default = 1024 }
variable "existing_vpc_id" { default = null }
variable "existing_subnet_ids" {
type = list(string)
default = null
}
variable "allowed_cidr" { default = "0.0.0.0/0" }
variable "existing_security_group_ids" {
type = list(string)
default = null
}
variable "create_load_balancer" { default = false }
variable "assign_public_ip" { default = true }
variable "enable_autoscaling" { default = false }
variable "min_capacity" { default = 1 }
variable "max_capacity" { default = 10 }
variable "autoscaling_cpu_threshold" { default = 80 }
variable "autoscaling_memory_threshold" { default = 80 }
variable "domain_name" { default = null }
variable "certificate_arn" { default = null }
variable "create_cluster" { default = true }
variable "kubernetes_namespace" { default = "bifrost" }
variable "node_count" { default = 3 }
variable "node_machine_type" { default = null }
variable "volume_size_gb" { default = 10 }
variable "gcp_project_id" { default = null }
variable "azure_resource_group_name" { default = null }
variable "storage_class_name" { default = "standard" }
variable "ingress_class_name" { default = "nginx" }
variable "ingress_annotations" {
type = map(string)
default = {}
}

View File

@@ -0,0 +1,339 @@
# --- Deployment target ---
variable "cloud_provider" {
description = "Cloud provider to deploy on"
type = string
validation {
condition = contains(["aws", "gcp", "azure", "kubernetes"], var.cloud_provider)
error_message = "cloud_provider must be one of: aws, gcp, azure, kubernetes"
}
}
variable "service" {
description = "Cloud service to deploy on. AWS: ecs, eks. GCP: gke, cloud-run. Azure: aks, aci. Kubernetes: deployment."
type = string
validation {
condition = contains(["ecs", "eks", "gke", "cloud-run", "aks", "aci", "deployment"], var.service)
error_message = "service must be one of: ecs, eks, gke, cloud-run, aks, aci, deployment"
}
}
# --- Config: bring your own ---
variable "config_json" {
description = "Complete Bifrost config.json as a string. Can be combined with individual config variables (variables override matching keys)."
type = string
default = null
sensitive = true
}
variable "config_json_file" {
description = "Path to a Bifrost config.json file. Can be combined with individual config variables (variables override matching keys)."
type = string
default = null
}
# --- Config: individual sections (each mirrors a top-level property from config.schema.json) ---
variable "encryption_key" {
description = "Encryption key for sensitive data. Accepts any string; a secure 32-byte AES-256 key will be derived using Argon2id KDF."
type = string
default = null
sensitive = true
}
variable "auth_config" {
description = "Authentication configuration (admin_username, admin_password, is_enabled, disable_auth_on_inference)."
type = any
default = null
sensitive = true
}
variable "client" {
description = "Client configuration (initial_pool_size, allowed_origins, enable_logging, max_request_body_size_mb, etc.)."
type = any
default = null
}
variable "framework" {
description = "Framework configuration (pricing)."
type = any
default = null
}
variable "providers_config" {
description = "LLM provider configurations (openai, anthropic, bedrock, azure, vertex, mistral, ollama, groq, gemini, openrouter, sgl, parasail, perplexity, elevenlabs, cerebras, huggingface)."
type = any
default = null
sensitive = true
}
variable "governance" {
description = "Governance configuration (budgets, rate_limits, customers, teams, virtual_keys, routing_rules)."
type = any
default = null
}
variable "mcp" {
description = "MCP configuration (client_configs, tool_manager_config)."
type = any
default = null
}
variable "vector_store" {
description = "Vector store configuration (enabled, type: weaviate/redis/qdrant/pinecone, config)."
type = any
default = null
}
variable "config_store" {
description = "Config store configuration (enabled, type: sqlite/postgres, config)."
type = any
default = null
}
variable "logs_store" {
description = "Logs store configuration (enabled, type: sqlite/postgres, config)."
type = any
default = null
}
variable "cluster_config" {
description = "Cluster mode configuration (enabled, peers, gossip, discovery)."
type = any
default = null
}
variable "scim_config" {
description = "SCIM/SSO configuration (enabled, provider: okta/entra, config)."
type = any
default = null
sensitive = true
}
variable "load_balancer_config" {
description = "Intelligent load balancer configuration (enabled, tracker_config, bootstrap)."
type = any
default = null
}
variable "guardrails_config" {
description = "Guardrails configuration (guardrail_rules, guardrail_providers)."
type = any
default = null
}
variable "plugins" {
description = "Plugins configuration. Array of plugin objects (telemetry, logging, governance, maxim, semantic_cache, otel, datadog)."
type = any
default = null
}
variable "audit_logs" {
description = "Audit logs configuration (disabled, hmac_key)."
type = any
default = null
}
variable "websocket" {
description = "WebSocket gateway configuration (max_connections_per_user, transcript_buffer_size, pool)."
type = any
default = null
}
# --- Image ---
variable "image_tag" {
description = "Bifrost Docker image tag."
type = string
default = "latest"
}
variable "image_repository" {
description = "Bifrost Docker image repository."
type = string
default = "maximhq/bifrost"
}
# --- Infrastructure ---
variable "region" {
description = "Cloud provider region (e.g. us-east-1, us-central1, eastus)."
type = string
}
variable "name_prefix" {
description = "Prefix for all resource names."
type = string
default = "bifrost"
}
variable "tags" {
description = "Tags to apply to all resources."
type = map(string)
default = {}
}
# --- Compute ---
# -----------------------------------------------------------------------------------------
# NOTE: If you are using OSS version - running multiple nodes has an effect on functionality
# of the system. Please read https://docs.getbifrost.ai/deployment-guides/how-to/multinode
#-----------------------------------------------------------------------------------------
variable "desired_count" {
description = "Number of replicas (ECS tasks / K8s pods). Capped at 1 for SQLite storage on K8s services."
type = number
default = 1
}
variable "cpu" {
description = "CPU allocation. ECS: CPU units (256-4096). K8s: millicores (e.g. 500)."
type = number
default = 512
}
variable "memory" {
description = "Memory allocation in MB."
type = number
default = 1024
}
# --- Networking (optional — creates new if not provided) ---
variable "existing_vpc_id" {
description = "Existing VPC/VNet/Network ID. If not provided, a new one will be created."
type = string
default = null
}
variable "existing_subnet_ids" {
description = "Existing subnet IDs. If not provided, new subnets will be created."
type = list(string)
default = null
}
variable "allowed_cidr" {
description = "CIDR block allowed for ingress traffic. Set to a specific range for production (e.g. your VPN or office IP)."
type = string
default = "0.0.0.0/0"
}
variable "existing_security_group_ids" {
description = "Existing security group IDs. If not provided, a new one will be created."
type = list(string)
default = null
}
# --- Optional features ---
variable "create_load_balancer" {
description = "Create a load balancer. ECS: creates an ALB. EKS: creates a Kubernetes Ingress with ALB annotations (requires AWS Load Balancer Controller). GKE: creates a GCE Ingress. AKS: creates a Kubernetes Ingress."
type = bool
default = false
}
variable "assign_public_ip" {
description = "Assign a public IP to the container (ECS Fargate). Set to false for private subnet deployments."
type = bool
default = true
}
variable "enable_autoscaling" {
description = "Enable autoscaling. Disabled automatically for SQLite storage on K8s services."
type = bool
default = false
}
variable "min_capacity" {
description = "Minimum number of replicas when autoscaling is enabled."
type = number
default = 1
}
variable "max_capacity" {
description = "Maximum number of replicas when autoscaling is enabled."
type = number
default = 10
}
variable "autoscaling_cpu_threshold" {
description = "Target CPU utilization percentage for autoscaling."
type = number
default = 80
}
variable "autoscaling_memory_threshold" {
description = "Target memory utilization percentage for autoscaling."
type = number
default = 80
}
variable "domain_name" {
description = "Custom domain name for the service (optional)."
type = string
default = null
}
variable "certificate_arn" {
description = "ACM/SSL certificate ARN for HTTPS. Used by EKS (ALB Ingress) and can be extended for other services."
type = string
default = null
}
# --- K8s-specific (EKS, GKE, AKS) ---
variable "create_cluster" {
description = "Create a new K8s cluster. Set to false to use an existing cluster."
type = bool
default = true
}
variable "kubernetes_namespace" {
description = "Kubernetes namespace to deploy into."
type = string
default = "bifrost"
}
variable "node_count" {
description = "Number of nodes in the K8s node pool (when creating a new cluster)."
type = number
default = 3
}
variable "node_machine_type" {
description = "Machine type for K8s nodes (e.g. t3.medium, e2-standard-4, Standard_D2s_v3)."
type = string
default = null
}
variable "volume_size_gb" {
description = "Persistent volume size in GB for SQLite storage."
type = number
default = 10
}
# --- GCP-specific ---
variable "gcp_project_id" {
description = "GCP project ID (required when cloud_provider = gcp)."
type = string
default = null
}
# --- Azure-specific ---
variable "azure_resource_group_name" {
description = "Azure resource group name. If not provided, a new one will be created."
type = string
default = null
}
# --- Generic Kubernetes ---
variable "storage_class_name" {
description = "Kubernetes StorageClass name for dynamic PVC provisioning (e.g. standard, gp2, premium-rwo). Used when cloud_provider = kubernetes."
type = string
default = "standard"
}
variable "ingress_class_name" {
description = "Ingress class name (e.g. nginx, traefik, haproxy). Used when cloud_provider = kubernetes."
type = string
default = "nginx"
}
variable "ingress_annotations" {
description = "Annotations to add to the Kubernetes Ingress resource. Used when cloud_provider = kubernetes."
type = map(string)
default = {}
}