Building an Elegant Terraform Cloud VM Manager with Elysia.js and React

Learn how to build a modern, full-stack application for managing Terraform Cloud virtual machines using Elysia.js, React, and Tailwind CSS with inline editing and elegant UI.

Mert Enercan
Full-stack Developer

In this blog post, we'll explore how we built a beautiful, full-stack application for managing Terraform Cloud virtual machines. The application combines the power of Elysia.js (a fast, TypeScript-first web framework) with React to create an elegant, card-based UI that makes infrastructure management a breeze.

What We Built

We created a Terraform Cloud VM Manager - a web application that allows you to:

  • View all VMs in a beautiful card-based interface
  • Create new VMs with intuitive forms
  • Update VM configurations inline without page navigation
  • Delete VMs with confirmation
  • Trigger Terraform runs (plan/apply) directly from the UI

All operations interact with Terraform Cloud's workspace variables API, meaning you can manage your infrastructure remotely without needing local Terraform files.

Tech Stack

Backend

  • Elysia.js - Lightning-fast web framework built for Bun
  • TypeScript - Type-safe development
  • Bun - Fast JavaScript runtime and package manager

Frontend

  • React 18 - Modern UI library
  • Tailwind CSS - Utility-first CSS framework
  • Shadcn-style components - Beautiful, accessible UI components

Architecture

  • Fullstack Dev Server - Bun's built-in HMR support for seamless development
  • RESTful API - Clean API design following REST principles
  • Workspace Variables - Using Terraform Cloud's API to manage VM configurations

Key Features

1. Elegant Card-Based UI

We designed a minimalist, card-based interface where each VM is displayed in its own card. The design philosophy focuses on:

  • Visual clarity - All information is visible at a glance
  • Elegant aesthetics - Gradient backgrounds, smooth transitions, and thoughtful spacing
  • Responsive design - Works beautifully on all screen sizes

2. Inline Editing

One of the standout features is inline editing. Instead of navigating to a separate form, users can:

  • Click "Edit" on any VM card
  • The card expands to show all editable fields
  • Make changes directly on the card
  • Save or cancel without leaving the view

This creates a seamless, modern editing experience that feels natural and intuitive.

3. Button-Based Selection (No Dropdowns!)

We replaced traditional dropdowns with elegant button-based selection:

  • VM Sizes: A grid of clickable buttons showing all available sizes
  • VM States: Large toggle buttons with visual status indicators
  • Visual feedback: Selected options scale up, show rings, and change colors

This approach makes all options visible and easily accessible, improving the user experience significantly.

4. Smart Card Sizing

The cards intelligently adapt based on their state:

  • View mode: Compact cards that don't grow unnecessarily
  • Edit mode: Expanded cards with full editing capabilities
  • Responsive grid: 1 column on mobile, 2 columns on desktop

5. Real-Time Status Indicators

Each VM card shows:

  • Animated status dots - Pulsing green for running VMs
  • Color-coded badges - Green for running, red for stopped
  • Last updated timestamp - Shows when the VM was last modified

Terraform Configuration

The application manages VMs defined in Terraform code that runs on Terraform Cloud. Here's how the Terraform configuration works:

Backend Configuration

The Terraform backend is configured to use Terraform Cloud:

terraform {
  cloud {
    organization = "hardal"
    workspaces {
      name = "signal"
    }
  }
}

This means all Terraform operations run remotely on Terraform Cloud, and the application updates workspace variables that Terraform reads.

Dynamic VM Management with for_each

The Terraform code uses the for_each meta-argument to dynamically create VMs based on the var.vms variable:

resource "azurerm_linux_virtual_machine" "main" {
  for_each = var.vms
  
  name = each.key
  size = each.value.vm_size
  # ... other configuration
}

The var.vms variable is defined as:

variable "vms" {
  description = "Map of VMs to create"
  type = map(object({
    vm_size  = string
    vm_state = string
  }))
  default = {}
}

This allows us to:

  • Add VMs by adding entries to the map
  • Remove VMs by removing entries from the map
  • Update VMs by modifying the map values

All without code changes - just update the workspace variable!

Infrastructure Components

The Terraform configuration creates:

Shared Network Infrastructure (created once):

  • Virtual Network (10.0.0.0/16)
  • Subnet (10.0.1.0/24)
  • Network Security Group (allows SSH on port 22)

Per-VM Resources (created for each VM):

  • Public IP address
  • Network interface
  • Linux Virtual Machine (Debian 12)
  • OS disk

VM State Management

One of the most interesting features is the VM start/stop functionality using a null_resource:

resource "null_resource" "vm_state" {
  for_each = var.vms

  triggers = {
    vm_state = each.value.vm_state
    # ... other triggers
  }

  provisioner "local-exec" {
    # Uses Azure REST API to start/stop VMs
    # Works on both Windows (PowerShell) and Linux (bash)
  }
}

How it works:

  1. When vm_state changes in the workspace variable, Terraform detects the change
  2. The null_resource trigger fires
  3. A local-exec provisioner calls the Azure REST API
  4. The VM is started (if vm_state = "running") or stopped (if vm_state = "stopped")

Cross-platform support:

  • Windows: Uses PowerShell to call Azure REST API
  • Linux/Mac: Uses curl to call Azure REST API
  • No Azure CLI required - works directly with REST API

Integration Flow

Here's how the application integrates with Terraform:

  1. User creates/updates VM in UI
  2. Application updates Terraform Cloud workspace variable 'vms'
  3. Application triggers Terraform run (plan or apply)
  4. Terraform Cloud reads the updated 'vms' variable
  5. Terraform plan/apply runs remotely
  6. Azure resources are created/updated/deleted
  7. VM state is managed via Azure REST API

Key Benefits

  • No Local Files: Everything is managed via Terraform Cloud workspace variables
  • Remote Execution: Terraform runs in the cloud, not on your machine
  • Version Control: Terraform Cloud tracks all changes
  • Team Collaboration: Multiple team members can manage the same infrastructure
  • State Management: Terraform Cloud manages state securely
  • Dynamic Scaling: Add/remove VMs without code changes

Implementation Highlights: Backend Architecture

Service Layer Pattern

Following Elysia.js best practices, we separated concerns:

// Service layer - abstract class with static methods
export abstract class TerraformService {
  static async getVMsConfig(config: Config, workspaceId: string): Promise
  static async updateVMsConfig(...): Promise
  static async createVMAndRun(...): Promise
  // ... more methods
}

Why static methods? They prevent memory leaks by avoiding class instance allocation, which is crucial for long-running server applications.

Workspace Variables Management

Instead of managing local .tfvars files, we use Terraform Cloud's workspace variables API:

// Store VM configuration as HCL in workspace variable
const vmsValue = `{
  "signal-terraform-5" = {
    vm_size  = "Standard_B2s"
    vm_state = "stopped"
  }
}`;

await setWorkspaceVariable(config, workspaceId, "vms", vmsValue, true, false);

Why HCL format? Terraform Cloud workspace variables can store HCL (HashiCorp Configuration Language) when marked as such. This allows us to store complex data structures that Terraform can directly use.

The variable structure:

  • Key: VM name (e.g., "signal-terraform-5")
  • Value: Object with vm_size and vm_state
  • Type: HCL (not JSON) so Terraform can parse it directly

This approach allows:

  • Remote management - No need for local files
  • Version control - Terraform Cloud handles versioning
  • Team collaboration - Multiple team members can manage the same workspace
  • Direct integration - Terraform reads the variable directly, no file parsing needed

Frontend Architecture

Component Design

We created reusable, shadcn-style components:

  • Button - Multiple variants (default, destructive, outline, ghost, secondary)
  • Card - Container with header, title, description, and content sections
  • Badge - Color-coded status indicators
  • Input, Select, Label - Form components
  • Alert - Notification system

State Management

The application uses React hooks for state management:

const [vms, setVms] = useState<Record>({});
const [editingVM, setEditingVM] = useState(null);
const [alert, setAlert] = useState(null);

Inline Editing Pattern

The inline editing is implemented with a toggle pattern:

const VMCard = ({ isEditing, onEditToggle, ... }) => {
  // Card renders differently based on isEditing state
  return (
    
      {isEditing ?  : }
    
  );
};

UI/UX Design Decisions

1. Color Palette

We chose a sophisticated color scheme:

  • Primary: Slate/Blue tones for a professional look
  • Status colors: Green for running, red for stopped
  • Gradients: Subtle gradients for depth without distraction

2. Typography

  • Headings: Bold, clear hierarchy
  • Body text: Readable sizes with proper line heights
  • Labels: Uppercase tracking for form labels

3. Spacing & Layout

  • Generous whitespace - Makes the interface breathe
  • Consistent padding - 4px, 8px, 16px, 24px scale
  • Grid system - Responsive grid for cards and buttons

4. Interactions

  • Hover effects - Cards lift with shadow on hover
  • Transitions - Smooth 200-300ms transitions
  • Visual feedback - Selected buttons scale and show rings
  • Loading states - Spinner animations during API calls

API Endpoints

The application exposes a clean REST API:

  • GET /vms - List all VMs
  • POST /vms/:name - Create a new VM
  • PUT /vms/:name - Update an existing VM
  • DELETE /vms/:name - Delete a VM
  • POST /runs/plan - Trigger a Terraform plan
  • POST /runs/apply - Trigger a Terraform apply
  • GET /workspace - Get workspace information

All endpoints support optional query parameters for workspace configuration:

  • orgName - Terraform Cloud organization
  • workspaceName - Workspace name
  • apiToken - API token (or use environment variable)

Getting Started

Prerequisites

  • Bun runtime installed
  • Terraform Cloud account
  • Terraform Cloud API token

Installation

# Clone the repository
git clone 
cd terraform-app

# Install dependencies
bun install

# Set up environment variables
echo "TERRAFORM_API_TOKEN=your-token-here" > .env
echo "TERRAFORM_ORG=your-org" >> .env
echo "TERRAFORM_WORKSPACE=your-workspace" >> .env

# Start the development server
bun run dev

Usage

  • View VMs: Navigate to http://localhost:3000/ to see all configured VMs
  • Create VM: Click the "Create" tab and fill in the form
  • Edit VM: Click "Edit" on any VM card to edit inline
  • Delete VM: Click "Delete" on any VM card (with confirmation)
  • Trigger Runs: Use the "Runs" tab to manually trigger plan/apply operations

Best Practices Implemented

1. Memory Leak Prevention

  • Used static methods instead of class instances
  • Proper cleanup of event listeners
  • No unnecessary object allocation

2. Type Safety

  • Full TypeScript coverage
  • Elysia's built-in validation
  • Type-safe API responses

3. Error Handling

  • Comprehensive error messages
  • User-friendly alerts
  • Graceful degradation

4. Code Organization

  • Separation of concerns (service/controller/model)
  • Reusable components
  • Clean file structure

Terraform Code Structure

The Terraform code follows best practices:

Variable-Driven Design

All VM configurations come from the var.vms workspace variable, making the infrastructure:

  • Declarative - Infrastructure is defined by data, not code
  • Dynamic - Easy to add/remove VMs without code changes
  • Maintainable - Single source of truth for VM configurations

Resource Lifecycle

The Terraform resources use lifecycle blocks to ensure smooth updates:

lifecycle {
  create_before_destroy = true
}

This prevents downtime when updating resources.

Azure Provider Configuration

The Azure provider is configured with service principal credentials:

provider "azurerm" {
  client_id       = var.azure_client_id
  client_secret   = var.azure_client_secret
  subscription_id = var.azure_subscription_id
  tenant_id       = var.azure_tenant_id
}

These credentials are stored as sensitive workspace variables in Terraform Cloud.

Network Architecture

The infrastructure uses a hub-and-spoke model:

  • One shared network for all VMs (cost-effective)
  • Individual public IPs for each VM (direct access)
  • Shared security group (consistent rules)

This design balances:

  • Cost - Shared network resources
  • Flexibility - Individual VM access
  • Security - Centralized security rules

Future Enhancements

Potential improvements for the future:

  • Real-time updates - WebSocket support for live status updates
  • VM history - Track changes over time
  • Bulk operations - Select and update multiple VMs
  • Advanced filtering - Filter VMs by size, state, etc.
  • Export/Import - Export configurations as JSON/YAML
  • Run monitoring - Real-time run status and logs
  • Notifications - Toast notifications for operations
  • Dark mode - Theme switching support
  • VM metrics - CPU, memory, disk usage from Azure Monitor
  • Cost tracking - Show estimated costs per VM
  • Backup management - Manage VM snapshots and backups
  • Tag management - Add/remove Azure resource tags

Technical Deep Dive

How VM State Management Works

The VM start/stop functionality is particularly clever. Here's the flow:

  1. User changes VM state in the UI (e.g., from "running" to "stopped")
  2. Application updates the vms workspace variable in Terraform Cloud
  3. Terraform run is triggered (plan or apply)
  4. Terraform detects change in vm_state for the specific VM
  5. null_resource trigger fires because vm_state changed
  6. local-exec provisioner runs the Azure REST API call
  7. VM is started or stopped in Azure

The provisioner uses Azure's REST API directly:

  • OAuth 2.0 authentication with client credentials
  • Start endpoint: POST /virtualMachines/{vmName}/start
  • Deallocate endpoint: POST /virtualMachines/{vmName}/deallocate

This approach is superior to using Azure CLI because:

  • No dependencies - Works without installing Azure CLI
  • Cross-platform - PowerShell on Windows, curl on Linux/Mac
  • Direct API calls - Faster and more reliable
  • Works in CI/CD - No need to install CLI in pipelines

Variable Parsing

The application parses HCL format from workspace variables:

// Parse HCL: { "vm-name" = { vm_size = "...", vm_state = "..." } }
const vmBlockPattern = /"([^"]+)"\s*=\s*\{([^}]+)\}/g;

This regex pattern:

  • Finds each VM block in the HCL string
  • Extracts the VM name (key)
  • Extracts the configuration (value)
  • Parses vm_size and vm_state from the configuration

Then converts it back to HCL when updating:

const vmsHcl = vmsEntries
  .map(([name, cfg], index) => {
    const comma = index < vmsEntries.length - 1 ? ',' : '';
    return `  "${name}" = {\n    vm_size  = "${cfg.vm_size}"\n    vm_state = "${cfg.vm_state}"\n  }${comma}`;
  })
  .join('\n');

Error Handling

The application handles various error scenarios:

  • Missing API token - Clear error message with setup instructions
  • Invalid workspace - Detailed error from Terraform Cloud API
  • Network errors - Graceful degradation with user-friendly messages
  • Validation errors - Elysia's built-in validation provides clear feedback

Security Considerations

  • API tokens - Stored as environment variables, never in code
  • Sensitive variables - Azure credentials stored as sensitive in Terraform Cloud
  • HTTPS only - All API calls use HTTPS
  • No local state - State is managed by Terraform Cloud (secure and backed up)

Conclusion

We've built a modern, elegant infrastructure management tool that combines:

  • Powerful backend - Fast, type-safe API with Elysia.js
  • Beautiful frontend - React with Tailwind CSS and shadcn-style components
  • Great UX - Inline editing, button-based selection, intuitive design
  • Best practices - Memory-safe, type-safe, well-organized code

The application demonstrates how modern web technologies can make infrastructure management more accessible and enjoyable. The card-based UI, inline editing, and button-based selection create an experience that feels more like a modern SaaS application than a traditional infrastructure tool.

Whether you're managing a few VMs or hundreds, this tool provides an elegant interface for Terraform Cloud operations, making infrastructure management a more pleasant experience.

Built with ❤️ using Elysia.js, React, Tailwind CSS, and Terraform

Ready to switch first-party and server-side measurement?

Join hundreds of companies using Hardal for better and faster data quality.