# Phase 2: Docker Compose Deployment - 5 US Commercial Azure Regions # DeFi Oracle Meta Mainnet (ChainID 138) # # Architecture: # - Deploys multi-service docker-compose stacks to Phase 1 VMs # - Each region gets a region-specific docker-compose file # - Services: Besu + FireFly + Cacti + Chainlink + Databases + Monitoring # # NOTE: Phase 1 must be deployed first. Phase 2 deploys docker-compose files to existing VMs. terraform { required_version = ">= 1.0" required_providers { azurerm = { source = "hashicorp/azurerm" version = "~> 3.0" } null = { source = "hashicorp/null" version = "~> 3.0" } local = { source = "hashicorp/local" version = "~> 2.0" } } } provider "azurerm" { features { resource_group { prevent_deletion_if_contains_resources = var.environment == "prod" ? true : false } } } # Local values for configuration locals { cloud_provider = "az" env_codes = { prod = "p" dev = "d" test = "t" staging = "s" } env_code = local.env_codes[var.environment] # Region to docker-compose file mapping region_compose_map = { centralus = "docker-compose.cus.yml" eastus = "docker-compose.eus.yml" eastus2 = "docker-compose.eus2.yml" westus = "docker-compose.wus.yml" westus2 = "docker-compose.wus2.yml" } # Extract region from phase1_vm_info keys regions = keys(var.phase1_vm_info) # Common tags common_tags = merge(var.tags, { Environment = var.environment CostCenter = "Blockchain" Owner = "DevOps Team" NamingConvention = "az-env-region-resource-instance" DeploymentPhase = "phase2" Project = "DeFi Oracle Meta Mainnet" ChainID = "138" }) } # Create deployment directory structure on VMs and copy docker-compose files resource "null_resource" "deploy_docker_compose" { for_each = var.phase1_vm_info triggers = { # Trigger when compose file changes or VM info changes compose_file_hash = filemd5("${var.docker_compose_source_path}/${local.region_compose_map[each.value.region]}") # Use VM name, resource group, and IPs to create unique trigger vm_identifier = "${each.value.resource_group}/${each.value.vm_names[0]}/${join(",", each.value.private_ips)}/${join(",", each.value.public_ips)}" } # Determine connection IP (prefer private IP, fallback to public IP) # NOTE: This Terraform should be run from the Nginx proxy host (20.160.58.99) # where direct access to private IPs is available connection { type = "ssh" user = var.vm_admin_username private_key = file(var.ssh_private_key_path) host = length(each.value.private_ips) > 0 ? each.value.private_ips[0] : (length(each.value.public_ips) > 0 ? each.value.public_ips[0] : null) timeout = "5m" } # Step 1: Create directories for docker-compose and volume mounts provisioner "remote-exec" { inline = [ "sudo mkdir -p /opt/docker-compose", "sudo mkdir -p /opt/besu/{data,config,keys,logs}", "sudo mkdir -p /opt/firefly/{postgres,postgres-primary,postgres-replica}", "sudo mkdir -p /opt/cacti/{postgres,postgres-primary}", "sudo mkdir -p /opt/prometheus", "sudo mkdir -p /opt/grafana", "sudo mkdir -p /opt/grafana-logs", "sudo mkdir -p /opt/alertmanager", "sudo mkdir -p /opt/loki/{data,config}", "sudo mkdir -p /opt/ipfs/data", "sudo mkdir -p /opt/promtail", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/docker-compose", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/besu", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/firefly", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/cacti", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/prometheus", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/grafana", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/grafana-logs", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/alertmanager", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/loki", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/ipfs", "sudo chown -R ${var.vm_admin_username}:${var.vm_admin_username} /opt/promtail", ] } # Step 2: Copy docker-compose file to VM provisioner "file" { source = "${var.docker_compose_source_path}/${local.region_compose_map[each.value.region]}" destination = "/tmp/docker-compose.yml" } # Step 3: Move file to final location and set permissions provisioner "remote-exec" { inline = [ "mv /tmp/docker-compose.yml /opt/docker-compose/docker-compose.yml", "chmod 644 /opt/docker-compose/docker-compose.yml", "chown ${var.vm_admin_username}:${var.vm_admin_username} /opt/docker-compose/docker-compose.yml", ] } } # Create systemd service file for docker-compose stack resource "null_resource" "create_systemd_service" { for_each = var.phase1_vm_info triggers = { compose_deployed = null_resource.deploy_docker_compose[each.key].id } connection { type = "ssh" user = var.vm_admin_username private_key = file(var.ssh_private_key_path) host = length(each.value.private_ips) > 0 ? each.value.private_ips[0] : (length(each.value.public_ips) > 0 ? each.value.public_ips[0] : null) timeout = "5m" } # Create systemd service file provisioner "file" { content = templatefile("${path.module}/templates/phase2-stack.service.tpl", { admin_username = var.vm_admin_username }) destination = "/tmp/phase2-stack.service" } # Install systemd service provisioner "remote-exec" { inline = [ "sudo mv /tmp/phase2-stack.service /etc/systemd/system/phase2-stack.service", "sudo chmod 644 /etc/systemd/system/phase2-stack.service", "sudo systemctl daemon-reload", "sudo systemctl enable phase2-stack.service", "# Service will be started manually or via separate apply" ] } depends_on = [ null_resource.deploy_docker_compose ] }