first commit
This commit is contained in:
132
terraform/README.md
Normal file
132
terraform/README.md
Normal file
@@ -0,0 +1,132 @@
|
||||
# Bifrost Terraform Modules
|
||||
|
||||
Deploy Bifrost on AWS, GCP, Azure, or any Kubernetes cluster using a single Terraform module.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Reference the module directly from GitHub. Pin to a specific release tag using `?ref=`:
|
||||
|
||||
```hcl
|
||||
module "bifrost" {
|
||||
source = "github.com/maximhq/bifrost//terraform/modules/bifrost?ref=terraform/v0.1.0"
|
||||
cloud_provider = "aws" # "aws" | "gcp" | "azure" | "kubernetes"
|
||||
service = "ecs" # AWS: "ecs" | "eks", GCP: "gke" | "cloud-run", Azure: "aks" | "aci", K8s: "deployment"
|
||||
region = "us-east-1"
|
||||
image_tag = "v1.4.6"
|
||||
|
||||
# Option A: Provide a config.json file
|
||||
config_json_file = "./config.json"
|
||||
|
||||
# Option B: Build config from Terraform variables (overrides matching keys from file)
|
||||
providers_config = {
|
||||
openai = { keys = [{ value = var.openai_key, weight = 1 }] }
|
||||
}
|
||||
config_store = {
|
||||
enabled = true
|
||||
type = "postgres"
|
||||
config = { host = var.db_host, port = "5432", user = "bifrost", password = var.db_password, db_name = "bifrost" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Supported Deployments
|
||||
|
||||
| Cloud | Service | Description |
|
||||
|-------|---------|-------------|
|
||||
| AWS | `ecs` | ECS Fargate with ALB, Secrets Manager, auto-scaling |
|
||||
| AWS | `eks` | EKS with K8s Deployment, PVC for SQLite, HPA |
|
||||
| GCP | `gke` | GKE with K8s Deployment, persistent disk, HPA |
|
||||
| GCP | `cloud-run` | Cloud Run v2 with Secret Manager, auto-scaling |
|
||||
| Azure | `aks` | AKS with K8s Deployment, managed disk, HPA |
|
||||
| Azure | `aci` | Azure Container Instances (single instance, dev/test) |
|
||||
| Kubernetes | `deployment` | Any K8s cluster with Deployment, PVC, HPA, Ingress |
|
||||
|
||||
## Configuration
|
||||
|
||||
Bifrost config can come from two sources simultaneously. Terraform variables override matching keys from the base file.
|
||||
|
||||
1. **File-based**: Set `config_json_file` to a path or `config_json` to a raw JSON string.
|
||||
2. **Variable-based**: Set individual variables (`config_store`, `logs_store`, `providers_config`, `auth_config`, etc.) corresponding to top-level keys in [config.schema.json](../transports/config.schema.json).
|
||||
|
||||
All 17 top-level config properties from the schema are supported as variables:
|
||||
`encryption_key`, `auth_config`, `client`, `framework`, `providers_config`, `governance`, `mcp`, `vector_store`, `config_store`, `logs_store`, `cluster_config`, `scim_config`, `load_balancer_config`, `guardrails_config`, `plugins`, `audit_logs`, `websocket`.
|
||||
|
||||
For `scim_config` with `provider = "okta"`, include `config.issuerUrl`, `config.clientId`, `config.clientSecret`, and `config.apiToken`.
|
||||
|
||||
## Provider Configuration
|
||||
|
||||
You only need to configure the Terraform providers for the cloud you are deploying to. For example, deploying to AWS ECS only requires the `aws` provider -- you do not need to configure `google`, `azurerm`, or `kubernetes`.
|
||||
|
||||
See the [module README](modules/bifrost/README.md#provider-configuration) for provider configuration examples per cloud.
|
||||
|
||||
## Testing
|
||||
|
||||
The module includes native Terraform tests (requires Terraform >= 1.7) that run with mocked providers -- no cloud credentials needed:
|
||||
|
||||
```bash
|
||||
cd modules/bifrost
|
||||
terraform init
|
||||
terraform test
|
||||
```
|
||||
|
||||
Tests cover all 7 deployment targets across 10 test files. See the [module README](modules/bifrost/README.md#testing) for details.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```text
|
||||
terraform/
|
||||
modules/bifrost/ # Top-level module (the only thing you call)
|
||||
aws/ # AWS platform (VPC, SG, IAM, Secrets Manager)
|
||||
services/ecs/ # ECS Fargate
|
||||
services/eks/ # EKS + K8s resources
|
||||
gcp/ # GCP platform (VPC, firewall, Secret Manager, SA)
|
||||
services/gke/ # GKE + K8s resources
|
||||
services/cloud-run/ # Cloud Run v2
|
||||
azure/ # Azure platform (VNet, NSG, Key Vault, identity)
|
||||
services/aks/ # AKS + K8s resources
|
||||
services/aci/ # Azure Container Instances
|
||||
kubernetes/ # Generic K8s (any cluster, no cloud APIs)
|
||||
examples/
|
||||
aws-ecs/ # Deploy on ECS Fargate
|
||||
gcp-gke/ # Deploy on GKE
|
||||
azure-aks/ # Deploy on AKS
|
||||
kubernetes/ # Deploy on any K8s cluster
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Each example directory contains `main.tf`, `variables.tf`, `outputs.tf`, `terraform.tfvars.example`, and a `README.md`.
|
||||
|
||||
```bash
|
||||
cd examples/aws-ecs
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
# Edit terraform.tfvars with your values
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
## Key Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `cloud_provider` | (required) | `"aws"`, `"gcp"`, `"azure"`, or `"kubernetes"` |
|
||||
| `service` | (required) | Service type (see table above) |
|
||||
| `region` | (required) | Cloud region |
|
||||
| `image_tag` | `"latest"` | Bifrost Docker image tag |
|
||||
| `desired_count` | `1` | Number of replicas |
|
||||
| `cpu` | `512` | CPU units (ECS) or millicores (K8s) |
|
||||
| `memory` | `1024` | Memory in MB |
|
||||
| `create_load_balancer` | `false` | Create a load balancer |
|
||||
| `enable_autoscaling` | `false` | Enable auto-scaling |
|
||||
| `create_cluster` | `true` | Create new cluster (set `false` to use existing) |
|
||||
| `storage_class_name` | `"standard"` | K8s StorageClass for PVC (generic K8s only) |
|
||||
| `ingress_class_name` | `"nginx"` | Ingress controller class (generic K8s only) |
|
||||
| `ingress_annotations` | `{}` | Ingress annotations (generic K8s only) |
|
||||
|
||||
## Outputs
|
||||
|
||||
| Output | Description |
|
||||
|--------|-------------|
|
||||
| `service_url` | URL to access Bifrost |
|
||||
| `health_check_url` | Health endpoint URL |
|
||||
36
terraform/examples/aws-ecs/README.md
Normal file
36
terraform/examples/aws-ecs/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Bifrost on AWS ECS
|
||||
|
||||
Deploys Bifrost as an ECS Fargate service with optional ALB and autoscaling.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- AWS account with appropriate permissions
|
||||
- AWS CLI configured (`aws configure`)
|
||||
- Terraform >= 1.0
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Copy and edit the example variables file
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
|
||||
# Deploy
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Two approaches can be combined:
|
||||
|
||||
1. **File-based** -- Set `config_json_file` to point to an existing `config.json`.
|
||||
2. **Variable-based** -- Set individual variables (`config_store`, `logs_store`, `providers_config`). These override matching keys from the file.
|
||||
|
||||
See `terraform.tfvars.example` for examples of both.
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
terraform destroy
|
||||
```
|
||||
41
terraform/examples/aws-ecs/main.tf
Normal file
41
terraform/examples/aws-ecs/main.tf
Normal file
@@ -0,0 +1,41 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
}
|
||||
|
||||
module "bifrost" {
|
||||
source = "../../modules/bifrost"
|
||||
cloud_provider = "aws"
|
||||
service = "ecs"
|
||||
region = var.region
|
||||
image_tag = var.image_tag
|
||||
name_prefix = var.name_prefix
|
||||
|
||||
# Config: use a file as base, override with variables
|
||||
config_json_file = var.config_json_file
|
||||
|
||||
# Override specific config sections
|
||||
config_store = var.config_store
|
||||
logs_store = var.logs_store
|
||||
providers_config = var.providers_config
|
||||
|
||||
# Compute
|
||||
desired_count = var.desired_count
|
||||
cpu = var.cpu
|
||||
memory = var.memory
|
||||
create_load_balancer = var.create_load_balancer
|
||||
|
||||
# Autoscaling
|
||||
enable_autoscaling = var.enable_autoscaling
|
||||
min_capacity = var.min_capacity
|
||||
max_capacity = var.max_capacity
|
||||
}
|
||||
9
terraform/examples/aws-ecs/outputs.tf
Normal file
9
terraform/examples/aws-ecs/outputs.tf
Normal file
@@ -0,0 +1,9 @@
|
||||
output "service_url" {
|
||||
description = "URL to access the Bifrost service."
|
||||
value = module.bifrost.service_url
|
||||
}
|
||||
|
||||
output "health_check_url" {
|
||||
description = "URL to the Bifrost health check endpoint."
|
||||
value = module.bifrost.health_check_url
|
||||
}
|
||||
62
terraform/examples/aws-ecs/terraform.tfvars.example
Normal file
62
terraform/examples/aws-ecs/terraform.tfvars.example
Normal file
@@ -0,0 +1,62 @@
|
||||
# =============================================================================
|
||||
# AWS ECS Example — terraform.tfvars
|
||||
# WARNING: Do NOT commit this file with real secrets (API keys, passwords).
|
||||
# Use environment variables, a secrets manager, or .gitignore this file.
|
||||
# =============================================================================
|
||||
|
||||
region = "us-east-1"
|
||||
image_tag = "latest"
|
||||
name_prefix = "bifrost"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Config approach 1: File-based
|
||||
# Point to an existing config.json. Variable overrides below will merge on top.
|
||||
# -----------------------------------------------------------------------------
|
||||
# config_json_file = "./config.json"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Config approach 2: Variable-based
|
||||
# Define config sections directly. These override matching keys from the file.
|
||||
# -----------------------------------------------------------------------------
|
||||
config_store = {
|
||||
enabled = true
|
||||
type = "sqlite"
|
||||
config = {
|
||||
path = "/app/data/bifrost.db"
|
||||
}
|
||||
}
|
||||
|
||||
logs_store = {
|
||||
enabled = true
|
||||
type = "sqlite"
|
||||
config = {
|
||||
path = "/app/data/bifrost-logs.db"
|
||||
}
|
||||
}
|
||||
|
||||
providers_config = {
|
||||
openai = {
|
||||
api_key = "sk-..."
|
||||
}
|
||||
anthropic = {
|
||||
api_key = "sk-ant-..."
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Compute
|
||||
# -----------------------------------------------------------------------------
|
||||
desired_count = 2
|
||||
cpu = 512
|
||||
memory = 1024
|
||||
create_load_balancer = true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Autoscaling
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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
|
||||
enable_autoscaling = true
|
||||
min_capacity = 1
|
||||
max_capacity = 5
|
||||
91
terraform/examples/aws-ecs/variables.tf
Normal file
91
terraform/examples/aws-ecs/variables.tf
Normal file
@@ -0,0 +1,91 @@
|
||||
variable "region" {
|
||||
description = "AWS region to deploy into."
|
||||
type = string
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Bifrost Docker image tag."
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "name_prefix" {
|
||||
description = "Prefix for all resource names."
|
||||
type = string
|
||||
default = "bifrost"
|
||||
}
|
||||
|
||||
# --- Config: file-based ---
|
||||
|
||||
variable "config_json_file" {
|
||||
description = "Path to a Bifrost config.json file. Variables below override matching keys."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Config: variable-based overrides ---
|
||||
|
||||
variable "config_store" {
|
||||
description = "Config store configuration (type: sqlite/postgres)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "logs_store" {
|
||||
description = "Logs store configuration (type: sqlite/postgres)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "providers_config" {
|
||||
description = "LLM provider configurations (openai, anthropic, bedrock, etc.)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Compute ---
|
||||
|
||||
variable "desired_count" {
|
||||
description = "Number of ECS tasks."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "cpu" {
|
||||
description = "CPU units for the ECS task (256-4096)."
|
||||
type = number
|
||||
default = 512
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
description = "Memory in MB for the ECS task."
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
variable "create_load_balancer" {
|
||||
description = "Create an Application Load Balancer in front of the service."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
# --- Autoscaling ---
|
||||
|
||||
variable "enable_autoscaling" {
|
||||
description = "Enable ECS service autoscaling."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "min_capacity" {
|
||||
description = "Minimum number of tasks when autoscaling is enabled."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "max_capacity" {
|
||||
description = "Maximum number of tasks when autoscaling is enabled."
|
||||
type = number
|
||||
default = 10
|
||||
}
|
||||
36
terraform/examples/azure-aks/README.md
Normal file
36
terraform/examples/azure-aks/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# Bifrost on Azure AKS
|
||||
|
||||
Deploys Bifrost as a Kubernetes workload on Azure Kubernetes Service.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Azure subscription
|
||||
- Azure CLI authenticated (`az login`)
|
||||
- Terraform >= 1.0
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Copy and edit the example variables file
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
|
||||
# Deploy
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Two approaches can be combined:
|
||||
|
||||
1. **File-based** -- Set `config_json_file` to point to an existing `config.json`.
|
||||
2. **Variable-based** -- Set individual variables (`config_store`, `logs_store`, `providers_config`). These override matching keys from the file.
|
||||
|
||||
See `terraform.tfvars.example` for examples of both.
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
terraform destroy
|
||||
```
|
||||
50
terraform/examples/azure-aks/main.tf
Normal file
50
terraform/examples/azure-aks/main.tf
Normal file
@@ -0,0 +1,50 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
required_providers {
|
||||
azurerm = {
|
||||
source = "hashicorp/azurerm"
|
||||
version = "~> 3.0"
|
||||
}
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "~> 2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "azurerm" {
|
||||
features {}
|
||||
}
|
||||
|
||||
module "bifrost" {
|
||||
source = "../../modules/bifrost"
|
||||
cloud_provider = "azure"
|
||||
service = "aks"
|
||||
region = var.region
|
||||
image_tag = var.image_tag
|
||||
name_prefix = var.name_prefix
|
||||
|
||||
# Config: use a file as base, override with variables
|
||||
config_json_file = var.config_json_file
|
||||
|
||||
# Override specific config sections
|
||||
config_store = var.config_store
|
||||
logs_store = var.logs_store
|
||||
providers_config = var.providers_config
|
||||
|
||||
# Compute
|
||||
desired_count = var.desired_count
|
||||
cpu = var.cpu
|
||||
memory = var.memory
|
||||
create_cluster = var.create_cluster
|
||||
node_count = var.node_count
|
||||
create_load_balancer = var.create_load_balancer
|
||||
|
||||
# Autoscaling
|
||||
enable_autoscaling = var.enable_autoscaling
|
||||
min_capacity = var.min_capacity
|
||||
max_capacity = var.max_capacity
|
||||
|
||||
# Azure-specific
|
||||
azure_resource_group_name = var.resource_group_name
|
||||
}
|
||||
9
terraform/examples/azure-aks/outputs.tf
Normal file
9
terraform/examples/azure-aks/outputs.tf
Normal file
@@ -0,0 +1,9 @@
|
||||
output "service_url" {
|
||||
description = "URL to access the Bifrost service."
|
||||
value = module.bifrost.service_url
|
||||
}
|
||||
|
||||
output "health_check_url" {
|
||||
description = "URL to the Bifrost health check endpoint."
|
||||
value = module.bifrost.health_check_url
|
||||
}
|
||||
66
terraform/examples/azure-aks/terraform.tfvars.example
Normal file
66
terraform/examples/azure-aks/terraform.tfvars.example
Normal file
@@ -0,0 +1,66 @@
|
||||
# =============================================================================
|
||||
# Azure AKS Example — terraform.tfvars
|
||||
# WARNING: Do NOT commit this file with real secrets (API keys, passwords).
|
||||
# Use environment variables, a secrets manager, or .gitignore this file.
|
||||
# =============================================================================
|
||||
|
||||
region = "eastus"
|
||||
image_tag = "latest"
|
||||
name_prefix = "bifrost"
|
||||
resource_group_name = "bifrost-rg"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Config approach 1: File-based
|
||||
# Point to an existing config.json. Variable overrides below will merge on top.
|
||||
# -----------------------------------------------------------------------------
|
||||
# config_json_file = "./config.json"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Config approach 2: Variable-based
|
||||
# Define config sections directly. These override matching keys from the file.
|
||||
# -----------------------------------------------------------------------------
|
||||
config_store = {
|
||||
enabled = true
|
||||
type = "sqlite"
|
||||
config = {
|
||||
path = "/app/data/bifrost.db"
|
||||
}
|
||||
}
|
||||
|
||||
logs_store = {
|
||||
enabled = true
|
||||
type = "sqlite"
|
||||
config = {
|
||||
path = "/app/data/bifrost-logs.db"
|
||||
}
|
||||
}
|
||||
|
||||
providers_config = {
|
||||
openai = {
|
||||
api_key = "sk-..."
|
||||
}
|
||||
azure = {
|
||||
api_key = "..."
|
||||
resource_id = "my-azure-openai-resource"
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Compute
|
||||
# -----------------------------------------------------------------------------
|
||||
desired_count = 2
|
||||
cpu = 500
|
||||
memory = 1024
|
||||
create_cluster = true
|
||||
node_count = 3
|
||||
create_load_balancer = true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Autoscaling
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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
|
||||
enable_autoscaling = false
|
||||
# min_capacity = 1
|
||||
# max_capacity = 5
|
||||
111
terraform/examples/azure-aks/variables.tf
Normal file
111
terraform/examples/azure-aks/variables.tf
Normal file
@@ -0,0 +1,111 @@
|
||||
variable "region" {
|
||||
description = "Azure region to deploy into."
|
||||
type = string
|
||||
default = "eastus"
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Bifrost Docker image tag."
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "name_prefix" {
|
||||
description = "Prefix for all resource names."
|
||||
type = string
|
||||
default = "bifrost"
|
||||
}
|
||||
|
||||
variable "resource_group_name" {
|
||||
description = "Azure resource group name. If null, a new one will be created."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Config: file-based ---
|
||||
|
||||
variable "config_json_file" {
|
||||
description = "Path to a Bifrost config.json file. Variables below override matching keys."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Config: variable-based overrides ---
|
||||
|
||||
variable "config_store" {
|
||||
description = "Config store configuration (type: sqlite/postgres)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "logs_store" {
|
||||
description = "Logs store configuration (type: sqlite/postgres)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "providers_config" {
|
||||
description = "LLM provider configurations (openai, anthropic, azure, etc.)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Compute ---
|
||||
|
||||
variable "desired_count" {
|
||||
description = "Number of Kubernetes pods."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "cpu" {
|
||||
description = "CPU in millicores for each pod."
|
||||
type = number
|
||||
default = 500
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
description = "Memory in MB for each pod."
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
# --- Cluster ---
|
||||
|
||||
variable "create_cluster" {
|
||||
description = "Create a new AKS cluster. Set to false to use an existing cluster."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "node_count" {
|
||||
description = "Number of nodes in the AKS node pool."
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
|
||||
variable "create_load_balancer" {
|
||||
description = "Create a load balancer via Kubernetes Ingress."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
# --- Autoscaling ---
|
||||
|
||||
variable "enable_autoscaling" {
|
||||
description = "Enable Horizontal Pod Autoscaler."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "min_capacity" {
|
||||
description = "Minimum number of pods when autoscaling is enabled."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "max_capacity" {
|
||||
description = "Maximum number of pods when autoscaling is enabled."
|
||||
type = number
|
||||
default = 10
|
||||
}
|
||||
37
terraform/examples/gcp-gke/README.md
Normal file
37
terraform/examples/gcp-gke/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Bifrost on GCP GKE
|
||||
|
||||
Deploys Bifrost as a Kubernetes workload on Google Kubernetes Engine.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- GCP project with billing enabled
|
||||
- `gcloud` CLI authenticated (`gcloud auth application-default login`)
|
||||
- Terraform >= 1.0
|
||||
- GKE API enabled (`gcloud services enable container.googleapis.com`)
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Copy and edit the example variables file
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
|
||||
# Deploy
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Two approaches can be combined:
|
||||
|
||||
1. **File-based** -- Set `config_json_file` to point to an existing `config.json`.
|
||||
2. **Variable-based** -- Set individual variables (`config_store`, `logs_store`, `providers_config`). These override matching keys from the file.
|
||||
|
||||
See `terraform.tfvars.example` for examples of both.
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
terraform destroy
|
||||
```
|
||||
49
terraform/examples/gcp-gke/main.tf
Normal file
49
terraform/examples/gcp-gke/main.tf
Normal file
@@ -0,0 +1,49 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
required_providers {
|
||||
google = {
|
||||
source = "hashicorp/google"
|
||||
version = "~> 5.0"
|
||||
}
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "~> 2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "google" {
|
||||
project = var.project_id
|
||||
region = var.region
|
||||
}
|
||||
|
||||
module "bifrost" {
|
||||
source = "../../modules/bifrost"
|
||||
cloud_provider = "gcp"
|
||||
service = "gke"
|
||||
region = var.region
|
||||
gcp_project_id = var.project_id
|
||||
image_tag = var.image_tag
|
||||
name_prefix = var.name_prefix
|
||||
|
||||
# Config: use a file as base, override with variables
|
||||
config_json_file = var.config_json_file
|
||||
|
||||
# Override specific config sections
|
||||
config_store = var.config_store
|
||||
logs_store = var.logs_store
|
||||
providers_config = var.providers_config
|
||||
|
||||
# Compute
|
||||
desired_count = var.desired_count
|
||||
cpu = var.cpu
|
||||
memory = var.memory
|
||||
create_cluster = var.create_cluster
|
||||
node_count = var.node_count
|
||||
create_load_balancer = var.create_load_balancer
|
||||
|
||||
# Autoscaling
|
||||
enable_autoscaling = var.enable_autoscaling
|
||||
min_capacity = var.min_capacity
|
||||
max_capacity = var.max_capacity
|
||||
}
|
||||
9
terraform/examples/gcp-gke/outputs.tf
Normal file
9
terraform/examples/gcp-gke/outputs.tf
Normal file
@@ -0,0 +1,9 @@
|
||||
output "service_url" {
|
||||
description = "URL to access the Bifrost service."
|
||||
value = module.bifrost.service_url
|
||||
}
|
||||
|
||||
output "health_check_url" {
|
||||
description = "URL to the Bifrost health check endpoint."
|
||||
value = module.bifrost.health_check_url
|
||||
}
|
||||
66
terraform/examples/gcp-gke/terraform.tfvars.example
Normal file
66
terraform/examples/gcp-gke/terraform.tfvars.example
Normal file
@@ -0,0 +1,66 @@
|
||||
# =============================================================================
|
||||
# GCP GKE Example — terraform.tfvars
|
||||
# WARNING: Do NOT commit this file with real secrets (API keys, passwords).
|
||||
# Use environment variables, a secrets manager, or .gitignore this file.
|
||||
# =============================================================================
|
||||
|
||||
project_id = "my-gcp-project"
|
||||
region = "us-central1"
|
||||
image_tag = "latest"
|
||||
name_prefix = "bifrost"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Config approach 1: File-based
|
||||
# Point to an existing config.json. Variable overrides below will merge on top.
|
||||
# -----------------------------------------------------------------------------
|
||||
# config_json_file = "./config.json"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Config approach 2: Variable-based
|
||||
# Define config sections directly. These override matching keys from the file.
|
||||
# -----------------------------------------------------------------------------
|
||||
config_store = {
|
||||
enabled = true
|
||||
type = "sqlite"
|
||||
config = {
|
||||
path = "/app/data/bifrost.db"
|
||||
}
|
||||
}
|
||||
|
||||
logs_store = {
|
||||
enabled = true
|
||||
type = "sqlite"
|
||||
config = {
|
||||
path = "/app/data/bifrost-logs.db"
|
||||
}
|
||||
}
|
||||
|
||||
providers_config = {
|
||||
openai = {
|
||||
api_key = "sk-..."
|
||||
}
|
||||
vertex = {
|
||||
project_id = "my-gcp-project"
|
||||
region = "us-central1"
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Compute
|
||||
# -----------------------------------------------------------------------------
|
||||
desired_count = 2
|
||||
cpu = 500
|
||||
memory = 1024
|
||||
create_cluster = true
|
||||
node_count = 3
|
||||
create_load_balancer = true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Autoscaling
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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
|
||||
enable_autoscaling = false
|
||||
# min_capacity = 1
|
||||
# max_capacity = 5
|
||||
110
terraform/examples/gcp-gke/variables.tf
Normal file
110
terraform/examples/gcp-gke/variables.tf
Normal file
@@ -0,0 +1,110 @@
|
||||
variable "project_id" {
|
||||
description = "GCP project ID."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "GCP region to deploy into."
|
||||
type = string
|
||||
default = "us-central1"
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Bifrost Docker image tag."
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "name_prefix" {
|
||||
description = "Prefix for all resource names."
|
||||
type = string
|
||||
default = "bifrost"
|
||||
}
|
||||
|
||||
# --- Config: file-based ---
|
||||
|
||||
variable "config_json_file" {
|
||||
description = "Path to a Bifrost config.json file. Variables below override matching keys."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Config: variable-based overrides ---
|
||||
|
||||
variable "config_store" {
|
||||
description = "Config store configuration (type: sqlite/postgres)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "logs_store" {
|
||||
description = "Logs store configuration (type: sqlite/postgres)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "providers_config" {
|
||||
description = "LLM provider configurations (openai, anthropic, vertex, etc.)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Compute ---
|
||||
|
||||
variable "desired_count" {
|
||||
description = "Number of Kubernetes pods."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "cpu" {
|
||||
description = "CPU in millicores for each pod."
|
||||
type = number
|
||||
default = 500
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
description = "Memory in MB for each pod."
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
# --- Cluster ---
|
||||
|
||||
variable "create_cluster" {
|
||||
description = "Create a new GKE cluster. Set to false to use an existing cluster."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "node_count" {
|
||||
description = "Number of nodes in the GKE node pool."
|
||||
type = number
|
||||
default = 3
|
||||
}
|
||||
|
||||
variable "create_load_balancer" {
|
||||
description = "Create a load balancer via Kubernetes Ingress."
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
# --- Autoscaling ---
|
||||
|
||||
variable "enable_autoscaling" {
|
||||
description = "Enable Horizontal Pod Autoscaler."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "min_capacity" {
|
||||
description = "Minimum number of pods when autoscaling is enabled."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "max_capacity" {
|
||||
description = "Maximum number of pods when autoscaling is enabled."
|
||||
type = number
|
||||
default = 10
|
||||
}
|
||||
45
terraform/examples/kubernetes/README.md
Normal file
45
terraform/examples/kubernetes/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Bifrost on Kubernetes
|
||||
|
||||
Deploys Bifrost on any existing Kubernetes cluster using a Deployment, PVC, and optional Ingress + HPA.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running Kubernetes cluster with `kubectl` access
|
||||
- A kubeconfig file (default: `~/.kube/config`)
|
||||
- A StorageClass that supports dynamic provisioning (e.g. `standard`, `gp2`, `premium-rwo`)
|
||||
- Terraform >= 1.0
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Copy and edit the example variables file
|
||||
cp terraform.tfvars.example terraform.tfvars
|
||||
|
||||
# Deploy
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Two approaches can be combined:
|
||||
|
||||
1. **File-based** -- Set `config_json_file` to point to an existing `config.json`.
|
||||
2. **Variable-based** -- Set individual variables (`config_store`, `logs_store`, `providers_config`). These override matching keys from the file.
|
||||
|
||||
See `terraform.tfvars.example` for examples of both.
|
||||
|
||||
## Ingress
|
||||
|
||||
To expose Bifrost externally, set `create_load_balancer = true` and configure:
|
||||
|
||||
- `ingress_class_name` -- Your ingress controller class (e.g. `nginx`, `traefik`, `haproxy`)
|
||||
- `domain_name` -- The hostname for the Ingress rule
|
||||
- `ingress_annotations` -- Any annotations your ingress controller needs (e.g. TLS, rate limiting)
|
||||
|
||||
## Cleanup
|
||||
|
||||
```bash
|
||||
terraform destroy
|
||||
```
|
||||
50
terraform/examples/kubernetes/main.tf
Normal file
50
terraform/examples/kubernetes/main.tf
Normal file
@@ -0,0 +1,50 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
required_providers {
|
||||
kubernetes = {
|
||||
source = "hashicorp/kubernetes"
|
||||
version = "~> 2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "kubernetes" {
|
||||
config_path = pathexpand(var.kubeconfig_path)
|
||||
config_context = var.kubeconfig_context
|
||||
}
|
||||
|
||||
module "bifrost" {
|
||||
source = "../../modules/bifrost"
|
||||
cloud_provider = "kubernetes"
|
||||
service = "deployment"
|
||||
region = "local"
|
||||
image_tag = var.image_tag
|
||||
name_prefix = var.name_prefix
|
||||
|
||||
# Config: use a file as base, override with variables
|
||||
config_json_file = var.config_json_file
|
||||
|
||||
# Override specific config sections
|
||||
config_store = var.config_store
|
||||
logs_store = var.logs_store
|
||||
providers_config = var.providers_config
|
||||
|
||||
# Compute
|
||||
desired_count = var.desired_count
|
||||
cpu = var.cpu
|
||||
memory = var.memory
|
||||
kubernetes_namespace = var.kubernetes_namespace
|
||||
volume_size_gb = var.volume_size_gb
|
||||
storage_class_name = var.storage_class_name
|
||||
|
||||
# Ingress
|
||||
create_load_balancer = var.create_load_balancer
|
||||
ingress_class_name = var.ingress_class_name
|
||||
ingress_annotations = var.ingress_annotations
|
||||
domain_name = var.domain_name
|
||||
|
||||
# Autoscaling
|
||||
enable_autoscaling = var.enable_autoscaling
|
||||
min_capacity = var.min_capacity
|
||||
max_capacity = var.max_capacity
|
||||
}
|
||||
9
terraform/examples/kubernetes/outputs.tf
Normal file
9
terraform/examples/kubernetes/outputs.tf
Normal file
@@ -0,0 +1,9 @@
|
||||
output "service_url" {
|
||||
description = "URL to access the Bifrost service."
|
||||
value = module.bifrost.service_url
|
||||
}
|
||||
|
||||
output "health_check_url" {
|
||||
description = "URL to the Bifrost health check endpoint."
|
||||
value = module.bifrost.health_check_url
|
||||
}
|
||||
77
terraform/examples/kubernetes/terraform.tfvars.example
Normal file
77
terraform/examples/kubernetes/terraform.tfvars.example
Normal file
@@ -0,0 +1,77 @@
|
||||
# =============================================================================
|
||||
# Generic Kubernetes Example — terraform.tfvars
|
||||
# WARNING: Do NOT commit this file with real secrets (API keys, passwords).
|
||||
# Use environment variables, a secrets manager, or .gitignore this file.
|
||||
# =============================================================================
|
||||
|
||||
image_tag = "latest"
|
||||
name_prefix = "bifrost"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Kubernetes connection
|
||||
# -----------------------------------------------------------------------------
|
||||
# kubeconfig_path = "~/.kube/config"
|
||||
# kubeconfig_context = "my-cluster"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Config approach 1: File-based
|
||||
# Point to an existing config.json. Variable overrides below will merge on top.
|
||||
# -----------------------------------------------------------------------------
|
||||
# config_json_file = "./config.json"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Config approach 2: Variable-based
|
||||
# Define config sections directly. These override matching keys from the file.
|
||||
# -----------------------------------------------------------------------------
|
||||
config_store = {
|
||||
enabled = true
|
||||
type = "sqlite"
|
||||
config = {
|
||||
path = "/app/data/bifrost.db"
|
||||
}
|
||||
}
|
||||
|
||||
logs_store = {
|
||||
enabled = true
|
||||
type = "sqlite"
|
||||
config = {
|
||||
path = "/app/data/bifrost-logs.db"
|
||||
}
|
||||
}
|
||||
|
||||
providers_config = {
|
||||
openai = {
|
||||
api_key = "sk-..."
|
||||
}
|
||||
anthropic = {
|
||||
api_key = "sk-ant-..."
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Compute
|
||||
# -----------------------------------------------------------------------------
|
||||
desired_count = 1
|
||||
cpu = 512
|
||||
memory = 1024
|
||||
kubernetes_namespace = "bifrost"
|
||||
volume_size_gb = 10
|
||||
storage_class_name = "standard"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Ingress (optional)
|
||||
# -----------------------------------------------------------------------------
|
||||
create_load_balancer = false
|
||||
# ingress_class_name = "nginx"
|
||||
# ingress_annotations = { "cert-manager.io/cluster-issuer" = "letsencrypt-prod" }
|
||||
# domain_name = "bifrost.example.com"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Autoscaling (optional)
|
||||
# -----------------------------------------------------------------------------
|
||||
# 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
|
||||
enable_autoscaling = false
|
||||
# min_capacity = 1
|
||||
# max_capacity = 5
|
||||
139
terraform/examples/kubernetes/variables.tf
Normal file
139
terraform/examples/kubernetes/variables.tf
Normal file
@@ -0,0 +1,139 @@
|
||||
# --- Kubernetes connection ---
|
||||
|
||||
variable "kubeconfig_path" {
|
||||
description = "Path to the kubeconfig file."
|
||||
type = string
|
||||
default = "~/.kube/config"
|
||||
}
|
||||
|
||||
variable "kubeconfig_context" {
|
||||
description = "Kubeconfig context to use. Defaults to current context."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Bifrost ---
|
||||
|
||||
variable "image_tag" {
|
||||
description = "Bifrost Docker image tag."
|
||||
type = string
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "name_prefix" {
|
||||
description = "Prefix for all resource names."
|
||||
type = string
|
||||
default = "bifrost"
|
||||
}
|
||||
|
||||
# --- Config: file-based ---
|
||||
|
||||
variable "config_json_file" {
|
||||
description = "Path to a Bifrost config.json file. Variables below override matching keys."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Config: variable-based overrides ---
|
||||
|
||||
variable "config_store" {
|
||||
description = "Config store configuration (type: sqlite/postgres)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "logs_store" {
|
||||
description = "Logs store configuration (type: sqlite/postgres)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "providers_config" {
|
||||
description = "LLM provider configurations (openai, anthropic, etc.)."
|
||||
type = any
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Compute ---
|
||||
|
||||
variable "desired_count" {
|
||||
description = "Number of pod replicas."
|
||||
type = number
|
||||
default = 1
|
||||
}
|
||||
|
||||
variable "cpu" {
|
||||
description = "CPU allocation in millicores (e.g. 500)."
|
||||
type = number
|
||||
default = 512
|
||||
}
|
||||
|
||||
variable "memory" {
|
||||
description = "Memory allocation in MB."
|
||||
type = number
|
||||
default = 1024
|
||||
}
|
||||
|
||||
variable "kubernetes_namespace" {
|
||||
description = "Kubernetes namespace to deploy into."
|
||||
type = string
|
||||
default = "bifrost"
|
||||
}
|
||||
|
||||
variable "volume_size_gb" {
|
||||
description = "Persistent volume claim size in GB."
|
||||
type = number
|
||||
default = 10
|
||||
}
|
||||
|
||||
variable "storage_class_name" {
|
||||
description = "Kubernetes StorageClass name for dynamic PVC provisioning."
|
||||
type = string
|
||||
default = "standard"
|
||||
}
|
||||
|
||||
# --- Ingress ---
|
||||
|
||||
variable "create_load_balancer" {
|
||||
description = "Create a Kubernetes Ingress resource."
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
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 = {}
|
||||
}
|
||||
|
||||
variable "domain_name" {
|
||||
description = "Custom domain name for the Ingress host rule."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
# --- Autoscaling ---
|
||||
|
||||
variable "enable_autoscaling" {
|
||||
description = "Enable Horizontal Pod Autoscaler."
|
||||
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
|
||||
}
|
||||
172
terraform/modules/bifrost/README.md
Normal file
172
terraform/modules/bifrost/README.md
Normal 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.
|
||||
320
terraform/modules/bifrost/aws/main.tf
Normal file
320
terraform/modules/bifrost/aws/main.tf
Normal 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
|
||||
}
|
||||
17
terraform/modules/bifrost/aws/outputs.tf
Normal file
17
terraform/modules/bifrost/aws/outputs.tf
Normal 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,
|
||||
)
|
||||
}
|
||||
236
terraform/modules/bifrost/aws/services/ecs/main.tf
Normal file
236
terraform/modules/bifrost/aws/services/ecs/main.tf
Normal 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
|
||||
}
|
||||
}
|
||||
29
terraform/modules/bifrost/aws/services/ecs/outputs.tf
Normal file
29
terraform/modules/bifrost/aws/services/ecs/outputs.tf
Normal 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
|
||||
}
|
||||
122
terraform/modules/bifrost/aws/services/ecs/variables.tf
Normal file
122
terraform/modules/bifrost/aws/services/ecs/variables.tf
Normal 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
|
||||
}
|
||||
567
terraform/modules/bifrost/aws/services/eks/main.tf
Normal file
567
terraform/modules/bifrost/aws/services/eks/main.tf
Normal 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]
|
||||
}
|
||||
33
terraform/modules/bifrost/aws/services/eks/outputs.tf
Normal file
33
terraform/modules/bifrost/aws/services/eks/outputs.tf
Normal 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}"
|
||||
}
|
||||
150
terraform/modules/bifrost/aws/services/eks/variables.tf
Normal file
150
terraform/modules/bifrost/aws/services/eks/variables.tf
Normal 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
|
||||
}
|
||||
168
terraform/modules/bifrost/aws/variables.tf
Normal file
168
terraform/modules/bifrost/aws/variables.tf
Normal 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
|
||||
}
|
||||
218
terraform/modules/bifrost/azure/main.tf
Normal file
218
terraform/modules/bifrost/azure/main.tf
Normal 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
|
||||
}
|
||||
15
terraform/modules/bifrost/azure/outputs.tf
Normal file
15
terraform/modules/bifrost/azure/outputs.tf
Normal 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)
|
||||
}
|
||||
63
terraform/modules/bifrost/azure/services/aci/main.tf
Normal file
63
terraform/modules/bifrost/azure/services/aci/main.tf
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
24
terraform/modules/bifrost/azure/services/aci/outputs.tf
Normal file
24
terraform/modules/bifrost/azure/services/aci/outputs.tf
Normal 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}"
|
||||
}
|
||||
73
terraform/modules/bifrost/azure/services/aci/variables.tf
Normal file
73
terraform/modules/bifrost/azure/services/aci/variables.tf
Normal 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
|
||||
}
|
||||
400
terraform/modules/bifrost/azure/services/aks/main.tf
Normal file
400
terraform/modules/bifrost/azure/services/aks/main.tf
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
terraform/modules/bifrost/azure/services/aks/outputs.tf
Normal file
42
terraform/modules/bifrost/azure/services/aks/outputs.tf
Normal 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}"
|
||||
}
|
||||
142
terraform/modules/bifrost/azure/services/aks/variables.tf
Normal file
142
terraform/modules/bifrost/azure/services/aks/variables.tf
Normal 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"]
|
||||
}
|
||||
151
terraform/modules/bifrost/azure/variables.tf
Normal file
151
terraform/modules/bifrost/azure/variables.tf
Normal 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
|
||||
}
|
||||
211
terraform/modules/bifrost/gcp/main.tf
Normal file
211
terraform/modules/bifrost/gcp/main.tf
Normal 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
|
||||
}
|
||||
15
terraform/modules/bifrost/gcp/outputs.tf
Normal file
15
terraform/modules/bifrost/gcp/outputs.tf
Normal 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)
|
||||
}
|
||||
136
terraform/modules/bifrost/gcp/services/cloud-run/main.tf
Normal file
136
terraform/modules/bifrost/gcp/services/cloud-run/main.tf
Normal 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
|
||||
}
|
||||
}
|
||||
22
terraform/modules/bifrost/gcp/services/cloud-run/outputs.tf
Normal file
22
terraform/modules/bifrost/gcp/services/cloud-run/outputs.tf
Normal 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}"
|
||||
)
|
||||
}
|
||||
102
terraform/modules/bifrost/gcp/services/cloud-run/variables.tf
Normal file
102
terraform/modules/bifrost/gcp/services/cloud-run/variables.tf
Normal 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
|
||||
}
|
||||
456
terraform/modules/bifrost/gcp/services/gke/main.tf
Normal file
456
terraform/modules/bifrost/gcp/services/gke/main.tf
Normal 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
|
||||
}
|
||||
42
terraform/modules/bifrost/gcp/services/gke/outputs.tf
Normal file
42
terraform/modules/bifrost/gcp/services/gke/outputs.tf
Normal 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}"
|
||||
)
|
||||
}
|
||||
149
terraform/modules/bifrost/gcp/services/gke/variables.tf
Normal file
149
terraform/modules/bifrost/gcp/services/gke/variables.tf
Normal 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"
|
||||
}
|
||||
155
terraform/modules/bifrost/gcp/variables.tf
Normal file
155
terraform/modules/bifrost/gcp/variables.tf
Normal 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
|
||||
}
|
||||
320
terraform/modules/bifrost/kubernetes/main.tf
Normal file
320
terraform/modules/bifrost/kubernetes/main.tf
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
terraform/modules/bifrost/kubernetes/outputs.tf
Normal file
27
terraform/modules/bifrost/kubernetes/outputs.tf
Normal 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"
|
||||
)
|
||||
}
|
||||
120
terraform/modules/bifrost/kubernetes/variables.tf
Normal file
120
terraform/modules/bifrost/kubernetes/variables.tf
Normal 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 = {}
|
||||
}
|
||||
204
terraform/modules/bifrost/main.tf
Normal file
204
terraform/modules/bifrost/main.tf
Normal 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
|
||||
}
|
||||
25
terraform/modules/bifrost/outputs.tf
Normal file
25
terraform/modules/bifrost/outputs.tf
Normal 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
|
||||
}
|
||||
128
terraform/modules/bifrost/tests/aws_ecs.tftest.hcl
Normal file
128
terraform/modules/bifrost/tests/aws_ecs.tftest.hcl
Normal 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
|
||||
}
|
||||
}
|
||||
142
terraform/modules/bifrost/tests/aws_eks.tftest.hcl
Normal file
142
terraform/modules/bifrost/tests/aws_eks.tftest.hcl
Normal 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
|
||||
}
|
||||
}
|
||||
112
terraform/modules/bifrost/tests/aws_shared.tftest.hcl
Normal file
112
terraform/modules/bifrost/tests/aws_shared.tftest.hcl
Normal 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({})
|
||||
}
|
||||
}
|
||||
96
terraform/modules/bifrost/tests/azure_aci.tftest.hcl
Normal file
96
terraform/modules/bifrost/tests/azure_aci.tftest.hcl
Normal 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"
|
||||
}
|
||||
}
|
||||
159
terraform/modules/bifrost/tests/azure_aks.tftest.hcl
Normal file
159
terraform/modules/bifrost/tests/azure_aks.tftest.hcl
Normal 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"]
|
||||
}
|
||||
}
|
||||
115
terraform/modules/bifrost/tests/config_merging.tftest.hcl
Normal file
115
terraform/modules/bifrost/tests/config_merging.tftest.hcl
Normal 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"
|
||||
}
|
||||
}
|
||||
95
terraform/modules/bifrost/tests/gcp_cloudrun.tftest.hcl
Normal file
95
terraform/modules/bifrost/tests/gcp_cloudrun.tftest.hcl
Normal 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
|
||||
}
|
||||
}
|
||||
150
terraform/modules/bifrost/tests/gcp_gke.tftest.hcl
Normal file
150
terraform/modules/bifrost/tests/gcp_gke.tftest.hcl
Normal 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"]
|
||||
}
|
||||
}
|
||||
157
terraform/modules/bifrost/tests/kubernetes.tftest.hcl
Normal file
157
terraform/modules/bifrost/tests/kubernetes.tftest.hcl
Normal 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" }
|
||||
}
|
||||
}
|
||||
145
terraform/modules/bifrost/tests/root_validation.tftest.hcl
Normal file
145
terraform/modules/bifrost/tests/root_validation.tftest.hcl
Normal 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.
|
||||
107
terraform/modules/bifrost/tests/setup/main.tf
Normal file
107
terraform/modules/bifrost/tests/setup/main.tf
Normal 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
|
||||
}
|
||||
12
terraform/modules/bifrost/tests/setup/outputs.tf
Normal file
12
terraform/modules/bifrost/tests/setup/outputs.tf
Normal 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
|
||||
}
|
||||
97
terraform/modules/bifrost/tests/setup/variables.tf
Normal file
97
terraform/modules/bifrost/tests/setup/variables.tf
Normal 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 = {}
|
||||
}
|
||||
339
terraform/modules/bifrost/variables.tf
Normal file
339
terraform/modules/bifrost/variables.tf
Normal 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 = {}
|
||||
}
|
||||
Reference in New Issue
Block a user