How to build an automated VM provisioning workflow

React Web App with Azure Container Apps, Azure SQL and Logic Apps approval flow.

Εισαγωγή

Today we are building an automatic Azure VM provisioning Web APP with approval workflow ! It is quite a responsibility to be an Administrator, especially on Azure. So when a VM provisioning request comes, the Administrator must take care a lot of things, from compliance and security to approval and of course provisioning! So here it is: A Web App that automates the request, send the approval email , and if approved proceeds to create the new VM!

The Web App offers Entra ID Authentication and the user can select the VM Size along with the Operating System, set the VM name and select the region. So we have absolute control over specifics, The request is send to an Azure SQL DB Table, where we have enabled Change Tracking. Azure Logic Apps is triggering a workflow to initiate process. Send the approval request and depending the response it executes the relevant tasks, being either a set of API calls for the VM provisioning and inform with Email upon status or informing the User that the request was not approved. Finally it writes the request status to the SQL Database for compliance and logging.

Εργαλεία

Η εφαρμογή Ιστού μας είναι ένα React Frontend με ένα Express JS Backend που φιλοξενείται ως κοντέινερ Docker σε Εφαρμογές κοντέινερ Azure. Χρησιμοποιούμε Azure SQL, επομένως αποθηκεύουμε κάθε αίτημα και το Key Vault, καθώς και τις Logic Apps με Managed Identity >.

Ένα μεγάλο ευχαριστώ στον Kristof Ivancza για τον runtime-env-cra , όπου κατάφερα να αναπτύξω το Container Εφαρμογές με μεταβλητές περιβάλλοντος χρόνου εκτέλεσης, κυριολεκτικά μια πρωτοποριακή προσθήκη για το React Web Apps! Δεν θα εξηγήσω τη μέθοδο, είναι πολύ εύκολο να κάνετε τις αλλαγές και να απολαύσετε τις Περιβαλλοντικές Αξίες που προστέθηκαν στο Runtime!

Κτίζοντας

Κάθε λεπτομέρεια σχετικά με τον Κώδικα και το Terraform είναι διαθέσιμη στο GitHub, αναζητήστε τον λογαριασμό μου και θα βρείτε τις περισσότερες αν όχι όλες τις αναρτήσεις μαζί με αυτό! Τώρα χρησιμοποιούμε το Terraform για να δημιουργήσουμε γρήγορα την κύρια υποδομή μας και μερικά βήματα συντονισμού και διαμορφώσεων για να την ολοκληρώσουμε!

Ας δούμε την εφαρμογή React και το NodeJS/ExpressJS backend. Δεν δημοσιεύω το CSS, και πάλι όλα είναι διαθέσιμα στο GitHub. Η εφαρμογή React μας διαθέτει Έλεγχο ταυτότητας Entra ID, ο λόγος είναι το γεγονός ότι θέλουμε να καταγράψουμε το όνομα χρήστη και να το αποθηκεύσουμε στο SQL DB.

import React, { useState, useEffect } from 'react';
import './VMProvisioningForm.css';

function VMProvisioningForm({ account }) {
    const [vmConfig, setVmConfig] = useState({
        name: '',
        os: '',
        size: '',
        region: ''
    });
    const [message, setMessage] = useState('');
    const [isError, setIsError] = useState(false);

    // Extract the username (email) from the account object
    useEffect(() => {
        if (account) {
            setVmConfig(prevState => ({
                ...prevState,
                username: account.username
            }));
        }
    }, [account]);

    const handleInputChange = (e) => {
        const { name, value } = e.target;
        setVmConfig({ ...vmConfig, [name]: value });
    };

    const backendUrl = window.__RUNTIME_CONFIG__.API_URL || 'http://backend10:5000';

    const handleSubmit = async (e) => {
        e.preventDefault();
        setMessage('');
        setIsError(false);


        if (!account || !account.username) {
            setIsError(true);
            setMessage('User is not logged in.');
            return;
        }

        const provisioningData = {
            ...vmConfig,
            username: account.username
        };
        try {
            const response = await fetch(`${backendUrl}/provision-vm`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                body: JSON.stringify(provisioningData),
            });

            if (response.ok) {
                const result = await response.json();
                setMessage('VM provisioning data saved successfully.');
            } else {
                setIsError(true);
                setMessage('Server responded with an error.');
            }
        } catch (error) {
            setIsError(true);
            setMessage('Failed to send data to the server.');
        }
    };
    

    return (
        <div classname="form-container">
            <form onsubmit="{handleSubmit}" classname="vm-form">
                <div>
                    <label>VM Name:</label>
                    <input
                        type="text"
                        name="name"
                        value="{vmConfig.name}"                        onchange="{handleInputChange}"                        placeholder="Enter VM Name"
                    />
                </div>
                <div>
                   <label>Operating System:</label>
                   <select name="os" value="{vmConfig.os}" onchange="{handleInputChange}">
                   <option value="">Select OS</option>
                   <option value="Windows 11">Windows 11</option>
                   <option value="Windows 10">Windows 10</option>
                   {/* Add other OS options as needed */}
                 </select>
                </div>

                <div>
                    <label>VM Size:</label>
                    <select name="size" value="{vmConfig.size}" onchange="{handleInputChange}">
                        <option value="">Select Size</option>
                        <option value="Standard_D4s_v3">Standard_D4s_v3</option>
                        <option value="Standard_DS2_v2">Standard_DS2_v2</option>
                        <option value="Standard_DS3_v2">Standard_DS3_v2</option>
                        {/* Add other size options */}
                    </select>
                </div>
                <div>
                    <label>Region:</label>
                    <select name="region" value="{vmConfig.region}" onchange="{handleInputChange}">
                        <option value="">Select Region</option>
                        <option value="westeurope">West Europe</option>
                        <option value="northeurope">North Europe</option>
                        {/* Add other region options */}
                    </select>
                </div>
                <button type="submit">Submit</button>
            <input type="hidden" name="trp-form-language" value="el"/></form>
            {message && (
                <div classname="{isError" ? &#39;error-message&#39; : &#39;success-message&#39;}>
                    {message}
                </div>
            )}
        </div>
    );
}

export default VMProvisioningForm;

Ακολουθούν επίσης τα απαραίτητα αρχεία για την ενσωμάτωση του Ελέγχου ταυτότητας Entra στην εφαρμογή Ιστού μας με το MSAL:

// msalConfig.js
import { LogLevel } from "@azure/msal-browser";
export const msalConfig = {
    auth: {
        clientId: "xxxxxxx", // This is your application's ID in Azure AD
        authority: "https://login.microsoftonline.com/xxx", // Replace with your tenant ID
        redirectUri: "https://xxxxx", // This should be the URI where your app is running
    },
    cache: {
        cacheLocation: "sessionStorage", // This configures where your cache will be stored
        storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
    },
    system: {	
        loggerOptions: {	
            loggerCallback: (level, message, containsPii) => {	
                if (containsPii) {		
                    return;		
                }		
                switch (level) {
                    case LogLevel.Error:
                        console.error(message);
                        return;
                    case LogLevel.Info:
                        console.info(message);
                        return;
                    case LogLevel.Verbose:
                        console.debug(message);
                        return;
                    case LogLevel.Warning:
                        console.warn(message);
                        return;
                    default:
                        return;
                }	
            }	
        }	
    }
};

/**
 * Scopes you add here will be prompted for user consent during sign-in.
 * By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
 * For more information about OIDC scopes, visit: 
 * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
 */
export const loginRequest = {
    scopes: ["User.Read"]
};

/**
 * Add here the scopes to request when obtaining an access token for MS Graph API. For more information, see:
 * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/resources-and-scopes.md
 */
export const graphConfig = {
    graphMeEndpoint: "https://graph.microsoft.com/v1.0/me",
};
// authConfig.js
import { PublicClientApplication } from '@azure/msal-browser';
import { msalConfig } from './msalConfig'; // Ensure this path is correct

export const msalInstance = new PublicClientApplication(msalConfig);

console.log('MSAL Instance:', msalInstance);

Όπως μπορούμε να δούμε, δημιουργήσαμε μια φόρμα Web όπου ο Χρήστης μπορεί να επιλέξει το μέγεθος VM από το διαθέσιμο και το λειτουργικό σύστημα, να ορίσει το όνομα και να επιλέξει από ένα συγκεκριμένο σύνολο Περιοχών. Το Backend κάνει μια "χαρτογράφηση" του λειτουργικού συστήματος σε δύο παραμέτρους που είναι το ImageOffer και το ImageSku. Αυτά τα χρειαζόμαστε αν θέλουμε να αναπτύξουμε συγκεκριμένες Εικόνες και φυσικά θέλουμε να τις προσθέσουμε στο Πρότυπο ARM μας. Ας ρίξουμε λοιπόν μια ματιά στο Backend:

const express = require('express');
const fetch = require('node-fetch');
const { DefaultAzureCredential } = require('@azure/identity');
const { SecretClient } = require('@azure/keyvault-secrets');
const sql = require('mssql');

const osMapping = {
    "Windows 11": {
        imageOffer: "windows-11",
        imageSku: "win11-22h2-pro"
    },
    "Windows 10": {
        imageOffer: "windows-10",
        imageSku: "win10-22h2-pro-g2"
    },
    // Add other OS mappings here
};

function processVmConfig(requestBody) {
    const osDetails = osMapping[requestBody.os];
    if (!osDetails) {
        throw new Error('Unsupported OS selected');
    }
    return {
        vmName: requestBody.name,
        imageOffer: osDetails.imageOffer,
        imageSku: osDetails.imageSku,
        vmSize: requestBody.size,
        region: requestBody.region,
        username: requestBody.username // Include the username in the processed config
    };
}

const app = express();
const port = 3001;
app.use(express.json());

// Azure Key Vault details
const credential = new DefaultAzureCredential();
const vaultName = process.env["KEY_VAULT_NAME"];
const url = `https://${vaultName}.vault.azure.net`;
const client = new SecretClient(url, credential);

// Function to get secret from Azure Key Vault
async function getSecret(secretName) {
    const secret = await client.getSecret(secretName);
    return secret.value;
}

// Function to connect to Azure SQL Database
async function getSqlConfig() {
    const username = await getSecret("sql-admin");
    const password = await getSecret("sql-pass");
    const server = await getSecret("sql-server");
    const database = await getSecret("sql-db");

    return {
        user: username,
        password: password,
        server: server,
        database: database,
        options: {
            encrypt: true,
            trustServerCertificate: false
        }
    };
}

app.post('/provision-vm', async (req, res) => {
    try {
        const vmConfig = processVmConfig(req.body);

        // Connect to the SQL database
        let pool = await sql.connect(await getSqlConfig());
        
        // Perform the SQL INSERT operation
        let result = await pool.request()
            .input('username', sql.NVarChar, vmConfig.username) // Add username input
            .input('vmName', sql.NVarChar, vmConfig.vmName)
            .input('imageOffer', sql.NVarChar, vmConfig.imageOffer)
            .input('imageSku', sql.NVarChar, vmConfig.imageSku)
            .input('vmSize', sql.NVarChar, vmConfig.vmSize)
            .input('region', sql.NVarChar, vmConfig.region)
            .query(`
                INSERT INTO vmprovs 
                (Username, VmName, ImageOffer, ImageSku, VmSize, Location) 
                VALUES 
                (@username, @vmName, @imageOffer, @imageSku, @vmSize, @region)
            `);

        res.status(201).send({ message: 'VM provisioning data saved successfully' });
    } catch (error) {
        console.error("Error details:", error);
        res.status(500).send({ message: 'Error saving VM provisioning data', error: error.message });
    }
});

app.listen(port, () => {
    console.log(`Server running on http://localhost:${port}`);
});

Κατασκευάζουμε επίσης το Key Vault Client με το SDK, ώστε να μπορούμε να λάβουμε τις λεπτομέρειες ελέγχου ταυτότητας και να γράψουμε τα δεδομένα φόρμας στον πίνακα DB μας. Ο πίνακας έχει επίσης στήλες ID , Timestamp , Username και ApprovalStatus που κάνουν την εφαρμογή Web ένα καλά κατασκευασμένο έργο που καταγράφει κάθε αίτημα. Αυτές είναι οι εντολές SQL που εκτελούμε μέσω ενός σεναρίου .sql με Terraform:

CREATE TABLE vmprov (
    RequestId INT IDENTITY(1,1) PRIMARY KEY,
    VmName NVARCHAR(50) NOT NULL,
    VmSize NVARCHAR(50) NOT NULL,
    ImageOffer NVARCHAR(50) NOT NULL,
    ImageSku NVARCHAR(100) NOT NULL,
    Location NVARCHAR(50) NOT NULL,
    ApprovalStatus NVARCHAR(50) NOT NULL,
	Username NVARCHAR(50) NOT NULL,
    Timestamp datetime2 NOT NULL DEFAULT GETDATE()
);
ALTER DATABASE provdb01  
SET CHANGE_TRACKING = ON  
(CHANGE_RETENTION = 2 DAYS, AUTO_CLEANUP = ON)

Για να μπορούν οι Λογικές Εφαρμογές να «παρακολουθούν» τις αλλαγές στο SQL, πρέπει να ενεργοποιήσουμε την Παρακολούθηση Αλλαγών σε επίπεδο βάσης δεδομένων, επομένως τις επισημασμένες εντολές στο τέλος.

Χρειαζόμαστε λοιπόν 2 DockerFiles για το Frontend και το Backend, όπου το Frontend είναι ευθυγραμμισμένο με το runtime-env-cra πακέτο και τις οδηγίες για να λειτουργήσει και με το Docker.

# Frontend
# Build stage
FROM node:18 AS build

# Set the working directory in the build stage
WORKDIR /usr/src/app

# Copy the frontend directory contents into the container at /usr/src/app
COPY . .

# Install dependencies and build the app
RUN npm install
RUN npm run build

# Serve stage
FROM nginx:alpine

COPY --from=build /usr/src/app/build /usr/share/nginx/html
# copy .env.example as .env to the relase build
COPY --from=build /usr/src/app/.env.example /usr/share/nginx/html/.env
COPY --from=build /usr/src/app/nginx/default.conf /etc/nginx/conf.d/default.conf

# (Optional) Copy the custom Nginx config into the image
# COPY custom_nginx.conf /etc/nginx/conf.d/default.conf
RUN apk add --update nodejs
RUN apk add --update npm
# Install runtime-env-cra
RUN npm i -g runtime-env-cra@0.2.0

# Expose port 80 for the app
EXPOSE 80

# Start Nginx with runtime-env-cra
CMD ["/bin/sh", "-c", "cd /usr/share/nginx/html && runtime-env-cra && nginx -g \"daemon off;\""]
# Backend
# Use the official Node.js 18 image as the base image
FROM node:18

# Set the working directory in the container
WORKDIR /usr/src/app

# Copy the package.json and package-lock.json (if available) files
COPY package*.json ./

# Install dependencies in the container
RUN npm install

# Copy the rest of your application's code
COPY . .

# Your app binds to port 3001, expose it
EXPOSE 3001

# Define the command to run the app
CMD ["node", "server.js"]

Επιτέλους δημιουργία εφαρμογών Azure Container ! Μπορούμε να μεταφέρουμε δυναμικά στην εφαρμογή Web React μας τις Περιβαλλοντικές Αξίες μας κατά την εκτέλεση, κάτι που κάνει τη ζωή μας πολύ πιο εύκολη και κάνει τον αυτοματισμό αρκετά σταθερό! Το μόνο που έχουμε να κάνουμε είναι να δημιουργήσουμε το Περιβάλλον και να εκτελέσουμε τις σχετικές εντολές με τις παραμέτρους -env για να δημιουργήσουμε τις Εφαρμογές μας!

Logic Apps

Η ροή εργασιών έγκρισης είναι μια αρκετά απλή διαδικασία. Υπάρχει Τεκμηρίωση επισης. Αλλά για την περίπτωσή μας, ο καλύτερος και ταχύτερος τρόπος είναι να χρησιμοποιήσουμε αιτήματα HTTP API στο Azure Resource Manager. Χρειαζόμαστε 3 αιτήματα καθώς το NIC και η Δημόσια IP (αν αναπτύξουμε ένα) θα πρέπει να υπάρχουν για την 3η κλήση API για τη δημιουργία της Εικονικής Μηχανής. Η ροή διαβάζει τον SQL Server και μετά τη δημιουργία ενός νέου στοιχείου εκτελείται. Το ενδιαφέρον μέρος φαίνεται παρακάτω και ο κώδικας θα βρίσκεται στο GitHub (Αποθηκεύω όλες τις μεταβλητές, επιπλέον ως πίνακα ξεχωριστά ως συμβολοσειρές, επομένως είναι μεγάλος!)

Logic Apps – SQL Trigger\Approval Email

Και για την κατανόηση, εδώ είναι ο Κώδικας της Εργασίας που δημιουργεί τη Δημόσια IP.

{
  "type": "Http",
  "inputs": {
    "uri": "https://management.azure.com/subscriptions/xxxxx/resourceGroups/xxxxx/providers/Microsoft.Network/publicIPAddresses/pip-@{triggerBody()?['VmName']}?api-version=2021-02-01\n",
    "method": "PUT",
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "location": "@{triggerBody()?['Location']}",
      "properties": {
        "publicIPAllocationMethod": "Dynamic",
        "publicIPAddressVersion": "IPv4"
      }
    },
    "authentication": {
      "type": "ManagedServiceIdentity",
      "audience": "https://management.azure.com"
    }
  }
}

Είναι πραγματικά εκπληκτικά τα πράγματα που μπορούμε να κάνουμε με τις ροές εργασίας Logic Apps! Να είστε προσεκτικοί με το Πρόγραμμα Κατανάλωσης, μπορείτε να εξετάσετε ένα Φιλοξενούμενο Πρόγραμμα, ειδικά εάν είστε νέος στις εφαρμογές Logic και σκοπεύετε να κάνετε ΠΟΛΛΕΣ δοκιμές! Η τελική εργασία είναι να ενημερώσετε τον χρήστη ότι το αίτημα εγκρίθηκε ή απορρίφθηκε και να ενημερώσετε τη στήλη SQL ApprovalStatus με το αποτέλεσμα. Πραγματικά εξαρτάται από τον καθένα ποιες λεπτομέρειες θα συμπεριλάβει στο email, μου αρέσει όσο το δυνατόν πιο απλό! Εάν βρείτε μια καταπληκτική ενημέρωση για αυτήν την εφαρμογή Ιστού, απλώς επικοινωνήστε μαζί μου και θα προσπαθήσουμε να την πραγματοποιήσουμε!

Κλείσιμο

Δεν θα μπορούσα να είμαι πιο ενθουσιασμένος που παρουσιάσω αυτήν την εφαρμογή React Azure Container, μια μεταμορφωτική λύση που έχει σχεδιαστεί για να βελτιστοποιήσει τη διαδικασία παροχής VM για χρήστες και διαχειριστές. Ήταν κάπως ένα όνειρό μου να φτάσω στο σημείο να δημιουργήσω αυτήν την εφαρμογή και έχουν περάσει μερικά χρόνια από την πρώτη σκέψη αυτής της ιδέας! Η εφαρμογή όχι μόνο αυτοματοποιεί τη ρύθμιση VM αλλά είναι επίσης επεκτάσιμη για να καλύψει άλλους πόρους και λειτουργικές ανάγκες. Επιπλέον, ενσωματώνεται απρόσκοπτα με τις Logic Apps για αποτελεσματική ροή εργασιών έγκρισης, διασφαλίζοντας ότι η παροχή πληροί τα οργανωτικά πρότυπα και τα πρωτόκολλα. Σας προσκαλώ να εξερευνήσετε αυτήν την εφαρμογή κάνοντας προτάσεις ή ακόμα και να την διαχωρίσετε – κλωνοποιήστε την και χρησιμοποιήστε τη δική σας επέκταση!

Αναφορές:

Architecture

Μοιραστείτε το!

1 Comments

  1. Pingback: How to create Azure AI Assistants with Logic Apps - CloudBlogger@2024

Αφήστε το σχόλιο σας