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:
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):
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)
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" )
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:
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 %20e mail%20profile%20https://www.googleapis.com/auth/cloud-platform"
"&access_type=offline"
"&prompt=consent %20s elect_account"
)
return redirect(url)
access_type=offline requests a refresh token for long-lived access.
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" )
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
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:
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
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' ]
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
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
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