Skip to main content

Architecture Overview

Multi-Cloud Manager is built as a modern web application with a Python Flask backend and React frontend, designed to manage resources across Azure, GCP, and AWS.

System Architecture

Flask Backend Structure

The backend is organized using Flask Blueprints for modular architecture.

Application Factory

The main application is created using the factory pattern:
project/backend/app.py
from flask import Flask
from flask_cors import CORS

def create_app():
    app = Flask(__name__)
    app.secret_key = "super-secret-key"  
    
    CORS(app, supports_credentials=True, origins=["http://localhost:3000"])
    
    # Register blueprints
    from auth.routes import auth_bp
    from azure_modules.routes import azure_bp_module
    from gcp.routes import gcp_api
    
    app.register_blueprint(gcp_api)
    app.register_blueprint(auth_bp)
    app.register_blueprint(azure_bp_module)
    
    return app

if __name__ == "__main__":
    app = create_app()
    app.run(debug=True, host="0.0.0.0", port=5000)
The application runs on 0.0.0.0:5000 to accept connections from Docker containers and external clients.

Blueprint Architecture

The backend uses three main blueprints:

Auth Blueprint

Handles OAuth2 flows for Azure, GCP, and AWS IAM role assumption

Azure Blueprint

Manages Azure resources via Azure SDK

GCP Blueprint

Manages GCP resources via Google Cloud SDK

Auth Blueprint Structure

The auth blueprint aggregates authentication for all providers:
project/backend/auth/routes.py
from flask import Blueprint

from .azure_auth import azure_auth, get_user
from .gcp_auth import gcp_auth
from .aws_auth import aws_auth

auth_bp = Blueprint("auth_bp", __name__)
auth_bp.register_blueprint(azure_auth)
auth_bp.register_blueprint(gcp_auth)
auth_bp.register_blueprint(aws_auth)

Azure Blueprint Routes

The Azure module exposes 30+ routes for resource management:
project/backend/azure_modules/routes.py
azure_bp_module = Blueprint("azure_module", __name__)

# Resource Groups
azure_bp_module.route("/api/resource_groups")(list_resource_groups)
azure_bp_module.route("/api/create_rg", methods=["POST"])(create_resource_group)
azure_bp_module.route("/api/resource_group_delete", methods=["DELETE"])(rg_delete)

# Virtual Networks
azure_bp_module.route("/api/vnets")(list_vnets)
azure_bp_module.route("/api/vnetsCreate", methods=["POST"])(vnet_create)

# Virtual Machines
azure_bp_module.route("/api/virtual_machines")(list_virtual_machines)
azure_bp_module.route("/api/vmsCreate", methods=["POST"])(create_vm)
azure_bp_module.route("/api/vmsDelete", methods=["DELETE"])(delete_vm)

# VM Monitoring
azure_bp_module.route("/api/vm/<vm_id>/metrics", methods=["POST"])(vm_az_monitor_metrics)
azure_bp_module.route("/api/vm/<vm_id>/agent-status")(agent_status)

# Storage Accounts
azure_bp_module.route("/api/list_storage_accounts")(list_storage_accounts)
azure_bp_module.route("/api/create_storage_account", methods=["POST"])(create_storage_account)

# Blob Storage
azure_bp_module.route("/api/<storage_account_id>/list_blobs", methods=["POST"])(list_blobs)
azure_bp_module.route("/api/<storage_account_id>/upload_blob", methods=["POST"])(upload_blob)

# Containers
azure_bp_module.route("/api/list_containers")(list_containers)
azure_bp_module.route("/api/create_container", methods=["POST"])(create_container)

# Alerts
azure_bp_module.route("/api/vm/<vm_id>/create-alert", methods=["POST"])(create_metric_alert)
azure_bp_module.route("/api/vm/<vm_id>/list_alerts_for_vm")(list_alerts_for_vm)

GCP Blueprint Routes

GCP routes follow a similar pattern:
project/backend/gcp/routes.py
gcp_api = Blueprint("gcp_api", __name__)

# Projects and Accounts
gcp_api.route("/api/account/google/projects")(api_gcp_projects)
gcp_api.route("/api/account/gcp")(api_gcp_accounts)

# Storage (Buckets)
gcp_api.route("/api/projects/list_buckets")(list_gcp_buckets)
gcp_api.route("/api/projects/create_bucket", methods=["POST"])(create_gcp_bucket)
gcp_api.route("/api/gcp/buckets/blobs")(list_bucket_blobs)
gcp_api.route("/api/gcp/buckets/blobs", methods=["POST"])(upload_blob_to_bucket)

# Virtual Machines
gcp_api.route("/api/gcp/list_vms")(list_gcp_vms)
gcp_api.route("/api/gcp/create_gcp_vms", methods=["POST"])(create_gcp_vm)
gcp_api.route("/api/gcp/delete_gcp_vm", methods=["DELETE"])(delete_gcp_vm)

# VM Monitoring
gcp_api.route("/api/gcp/vm/<project_id>/<instance_id>/metrics", methods=["POST"])(get_metric_timeseries)
gcp_api.route("/api/gcp/vm/<project_id>/<instance_id>/agent-status")(get_vm_agent_status)
gcp_api.route("/api/gcp/vm/<project_id>/<instance_id>/logs/query", methods=["POST"])(query_lql_logs)

# Containers (Cloud Run)
gcp_api.route("/api/gcp/list_containers")(list_gcp_containers)
gcp_api.route("/api/gcp/create_container", methods=["POST"])(create_gcp_container)

# VPCs
gcp_api.route("/api/gcp/list_gcp_vpcs")(list_gcp_vpcs)
gcp_api.route("/api/gcp/create_gcp_vpc", methods=["POST"])(create_gcp_vpc)

Module Organization

Backend modules are organized by provider and functionality:
project/backend/
├── app.py                    # Application factory
├── auth/
│   ├── __init__.py
│   ├── routes.py            # Auth blueprint aggregator
│   ├── azure_auth.py        # Azure OAuth2 flow
│   ├── gcp_auth.py          # GCP OAuth2 flow
│   └── aws_auth.py          # AWS IAM role assumption
├── azure_modules/
│   ├── routes.py            # Azure blueprint
│   ├── utils.py             # Session helpers
│   ├── rg.py                # Resource groups
│   ├── vm.py                # Virtual machines
│   ├── vnet.py              # Virtual networks
│   ├── storage.py           # Storage accounts
│   ├── containers.py        # Azure Container Instances
│   ├── vmmonitor.py         # VM monitoring
│   ├── containermonitor.py  # Container monitoring
│   └── alerts.py            # Alert management
└── gcp/
    ├── routes.py            # GCP blueprint
    ├── utils.py             # GCP helpers
    ├── vm.py                # Compute Engine VMs
    ├── storage.py           # Cloud Storage buckets
    ├── containers.py        # Cloud Run services
    ├── vpcs.py              # VPC networks
    ├── vmmonitor.py         # VM monitoring
    └── containermonitor.py  # Container monitoring

React Frontend Structure

The frontend is built with React and React Router for single-page application functionality.

Application Router

project/frontend/src/App.js
import React, { useEffect, useState } from "react";
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
import Sidebar from "./components/Sidebar";
import Dashboard from "./pages/Dashboard";
import VirtualMachines from "./pages/VirtualMachines";
import Containers from "./pages/Containers";

function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch("/api/user")
      .then((res) => res.json())
      .then((data) => setUser(data))
      .catch((err) => console.error(err));
  }, []);

  if (!user) return <p>Loading...</p>;

  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home user={user} />} />
        {user.logged_in ? (
          <Route path="/*" element={
            <div style={{ display: "flex", minHeight: "100vh" }}>
              <Sidebar onLogout={handleLogout} />
              <main className="app-main">
                <Routes>
                  <Route path="/dashboard" element={<Dashboard />} />
                  <Route path="/virtual-machines" element={<VirtualMachines />} />
                  <Route path="/containers" element={<Containers />} />
                  <Route path="/vm/:vmId/monitoring" element={<VMMonitor />} />
                  <Route path="/storage" element={<Storage />} />
                </Routes>
              </main>
            </div>
          } />
        ) : (
          <Route path="/*" element={<Navigate to="/" />} />
        )}
      </Routes>
    </Router>
  );
}
The app checks user authentication on mount and conditionally renders routes based on user.logged_in status.

Frontend Pages

Key pages include:

Home

Landing page with provider login options

Dashboard

Overview of all connected accounts and resources

Virtual Machines

List and manage VMs across Azure and GCP

Containers

Manage ACI and Cloud Run containers

Storage

Browse storage accounts, buckets, and blobs

Networks

View and create VNets, VPCs, and subnets

VM Monitor

Detailed metrics, logs, and alerts for VMs

Accounts

Manage connected cloud accounts

OAuth2 Authentication Flow

Azure OAuth2 Implementation

Azure authentication uses the MSAL (Microsoft Authentication Library):
1

Authorization Request

project/backend/auth/azure_auth.py
AUTHORITY = f"https://login.microsoftonline.com/{os.getenv('AZURE_TENANT_ID')}"
SCOPE = ["https://management.azure.com/.default"]
CLIENT_ID = os.getenv("AZURE_CLIENT_ID")
CLIENT_SECRET = os.getenv("AZURE_CLIENT_SECRET")

def build_msal_app():
    return msal.ConfidentialClientApplication(
        CLIENT_ID, 
        authority=AUTHORITY, 
        client_credential=CLIENT_SECRET
    )

@azure_auth.route("/api/login/azure")
def login():
    url = build_msal_app().get_authorization_request_url(
        scopes=SCOPE, 
        redirect_uri=f"{APP_BASE_URL}{REDIRECT_PATH}",
    )
    return redirect(url)
2

Token Exchange

project/backend/auth/azure_auth.py
@azure_auth.route("/getAToken")
def authorized():
    code = request.args.get("code")
    if not code:
        return jsonify({"error": "No code"}), 401

    result = build_msal_app().acquire_token_by_authorization_code(
        code, scopes=SCOPE, redirect_uri=f"{APP_BASE_URL}{REDIRECT_PATH}"
    )
    
    if "id_token_claims" in result:
        session["user"] = result["id_token_claims"]
        session["access_token"] = result.get("access_token")
3

Fetch Subscriptions

project/backend/auth/azure_auth.py
cred = ClientSecretCredential(
    tenant_id=tenant_id,
    client_id=client_id,
    client_secret=client_secret
)

sub_client = SubscriptionClient(cred)
subs = [s.subscription_id for s in sub_client.subscriptions.list()]

azure_account = {
    "provider": "azure",
    "tenantId": tenant_id,
    "displayName": display_name,
    "subscriptions": subs
}
session["accounts"] = filtered_accounts

GCP OAuth2 Implementation

GCP uses standard OAuth2 with Google’s identity platform:
1

Authorization URL

project/backend/auth/gcp_auth.py
@gcp_auth.route("/api/login/google")
def login_google():
    url = (
        "https://accounts.google.com/o/oauth2/v2/auth"
        f"?client_id={GOOGLE_CLIENT_ID}"
        f"&redirect_uri={GOOGLE_REDIRECT_URI}"
        "&response_type=code"
        "&scope=openid%20email%20profile%20https://www.googleapis.com/auth/cloud-platform"
        "&access_type=offline"
        "&prompt=consent%20select_account"
    )
    return redirect(url)
access_type=offline requests a refresh token for long-lived access.
2

Token Exchange

project/backend/auth/gcp_auth.py
@gcp_auth.route("/google/callback")
def google_callback():
    code = request.args.get("code")
    
    token_res = http_requests.post(TOKEN_URI, data={
        "code": code,
        "client_id": GOOGLE_CLIENT_ID,
        "client_secret": GOOGLE_CLIENT_SECRET,
        "redirect_uri": GOOGLE_REDIRECT_URI,
        "grant_type": "authorization_code"
    })
    
    token_data = token_res.json()
    id_token_str = token_data.get("id_token")
    access_token = token_data.get("access_token")
    refresh_token = token_data.get("refresh_token")
3

Verify ID Token

project/backend/auth/gcp_auth.py
from google.oauth2 import id_token
from google.auth.transport import requests as google_requests

idinfo = id_token.verify_oauth2_token(
    id_token_str, 
    google_requests.Request(), 
    GOOGLE_CLIENT_ID, 
    clock_skew_in_seconds=10
)

session["user"] = idinfo
session["access_token"] = access_token
4

Store Account

project/backend/auth/gcp_auth.py
new_gcp_account = {
    "provider": "gcp",
    "email": idinfo.get("email"),
    "displayName": idinfo.get("name"),
    "access_token": access_token,
    "refresh_token": refresh_token
}

accounts = session.setdefault("accounts", [])
# Update or append account
for i, acc in enumerate(accounts):
    if acc.get("email") == new_gcp_account["email"] and acc.get("provider") == "gcp":
        accounts[i] = new_gcp_account
        break
else:
    accounts.append(new_gcp_account)

AWS IAM Role Assumption

AWS uses a different approach - STS AssumeRole with external ID:
1

Provide Configuration

project/backend/auth/aws_auth.py
AWS_SERVER_ACCOUNT_ID = os.getenv("AWS_ACCOUNT_ID") 
APP_EXTERNAL_ID = "multi-cloud-manager-app-v1-secret"

@aws_auth.route("/api/account/aws/config")
def get_aws_config_info():
    return jsonify({
        "awsAccountId": AWS_SERVER_ACCOUNT_ID,
        "externalId": APP_EXTERNAL_ID
    }), 200
2

Assume Role

project/backend/auth/aws_auth.py
@aws_auth.route("/api/account/aws/add", methods=["POST"])
def add_aws_account():
    role_arn_from_user = request.get_json().get("roleArn")
    
    sts_client = boto3.client(
        'sts',
        aws_access_key_id=AWS_SERVER_ACCESS_KEY_ID,
        aws_secret_access_key=AWS_SERVER_SECRET_KEY,
        region_name='us-east-1'
    )
    
    assumed_role_object = sts_client.assume_role(
        RoleArn=role_arn_from_user,
        RoleSessionName="MultiCloudManagerVerification",
        ExternalId=APP_EXTERNAL_ID
    )
    
    temp_credentials = assumed_role_object['Credentials']
3

Verify Access

project/backend/auth/aws_auth.py
ec2_client = boto3.client(
    'ec2',
    aws_access_key_id=temp_credentials['AccessKeyId'],
    aws_secret_access_key=temp_credentials['SecretAccessKey'],
    aws_session_token=temp_credentials['SessionToken'],
    region_name='us-east-1' 
)

ec2_client.describe_regions()  # Verification call
4

Store in Session

project/backend/auth/aws_auth.py
user_account_id = role_arn_from_user.split(':')[4]

new_aws_account = {
    "provider": "aws",
    "displayName": f"AWS Account ({user_account_id})",
    "roleArn": role_arn_from_user,
    "externalId": APP_EXTERNAL_ID,
    "accountId": user_account_id
}

accounts = session.get("accounts", [])
accounts.append(new_aws_account)
session["accounts"] = accounts
External ID is critical for security. It prevents the “confused deputy” problem where an attacker tricks the service into accessing the wrong account.

Session Management

Multi-Cloud Manager uses Flask’s built-in server-side sessions.

Session Structure

session = {
    "user": {                    # User identity (Azure/GCP)
        "name": "John Doe",
        "email": "[email protected]",
        "oid": "user-object-id"  # Azure
    },
    "access_token": "ya29...",  # OAuth2 access token
    "accounts": [                # Multi-account support
        {
            "provider": "azure",
            "tenantId": "...",
            "displayName": "Azure Subscription",
            "subscriptions": ["sub-1", "sub-2"]
        },
        {
            "provider": "gcp",
            "email": "[email protected]",
            "displayName": "GCP Account",
            "access_token": "...",
            "refresh_token": "..."
        },
        {
            "provider": "aws",
            "roleArn": "arn:aws:iam::123456789012:role/MCMRole",
            "accountId": "123456789012",
            "externalId": "multi-cloud-manager-app-v1-secret"
        }
    ]
}

Session Security

Secret Key Signing

Sessions are signed with app.secret_key to prevent tampering

HTTP-Only Cookies

Session cookies are HTTP-only to prevent XSS attacks

CORS with Credentials

CORS is configured with supports_credentials=True for cookie-based auth

Server-Side Storage

Session data is stored server-side, only session ID is in cookie

Custom Credential Wrapper

Azure SDK requires a credential object. The app uses a custom wrapper:
project/backend/azure_modules/utils.py
from flask import session
from azure.core.credentials import AccessToken

class FlaskCredential:
    """Wraps Flask session token for Azure SDK"""
    
    def get_token(self, *scopes, **kwargs):
        token = session.get("access_token")
        if not token:
            raise ValueError("No access token in session")
        return AccessToken(token, 9999999999)
Usage:
project/backend/azure_modules/vm.py
from .utils import FlaskCredential
from azure.mgmt.compute import ComputeManagementClient

def list_virtual_machines():
    if "access_token" not in session:
        return jsonify({"error": "Unauthorized"}), 401
    
    credential = FlaskCredential()
    compute_client = ComputeManagementClient(credential, subscription_id)
    
    vms = compute_client.virtual_machines.list_all()
    return jsonify([{"name": vm.name, "location": vm.location} for vm in vms])

Docker Deployment

Docker Compose Configuration

docker-compose.yml
services:
  backend:
    build: ./project/backend
    container_name: flask-backend
    ports:
      - "5000:5000"
    env_file:
      - .env
    volumes:
      - ./project/backend:/app
    networks:
      - app-network

  frontend:
    build: ./project/frontend
    container_name: react-frontend
    ports:
      - "3000:3000"
    stdin_open: true
    tty: true
    volumes:
      - ./project/frontend:/app
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

Backend Dockerfile

project/backend/Dockerfile
FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

Key Dependencies

project/backend/requirements.txt
# Core Framework
flask
flask_cors

# Azure SDK
msal
azure-identity
azure-mgmt-subscription
azure-mgmt-resource
azure-mgmt-compute
azure-mgmt-network
azure-mgmt-storage
azure-mgmt-containerinstance
azure-monitor-query==1.2.0
azure-mgmt-loganalytics
azure-mgmt-monitor
azure-storage-blob

# GCP SDK
google-auth
google-auth-oauthlib
google-auth-httplib2
google-api-python-client
google-cloud-storage
google-cloud-resource-manager
google-cloud-compute
google-cloud-run
google-cloud-monitoring
google-cloud-logging

# AWS SDK
boto3

# Utilities
python-dotenv
requests
Azure Monitor Query is pinned to version 1.2.0 for stability with the monitoring features.

API Request Flow

Typical request flow for resource operations:

Security Considerations

Token Storage

Access tokens stored server-side in Flask session, never exposed to client

CORS Configuration

Strict origin whitelist: only http://localhost:3000 allowed

External ID

AWS uses external ID to prevent confused deputy attacks

HTTPS in Production

Configure reverse proxy (nginx) with TLS for production deployments
The app.secret_key in the source code is for development only. In production, use a secure random key from environment variables:
app.secret_key = os.getenv("FLASK_SECRET_KEY", secrets.token_hex(32))

Next Steps

API Reference

Complete API endpoint documentation

Resource Management

Learn about VM, storage, and network operations

Monitoring & Alerts

Configure metrics collection and alerting

Production Deployment

Deploy to production with HTTPS and scaling

Build docs developers (and LLMs) love