Loading...
creating azure resources part3
Author Cloudapp
E.G.

Automated creation of Azure resources via CLI in Next.js - Part 3

November 6, 2024
Table of Contents

We created a Linux App Service instance in the F1 plan (Free) in the first story. In the second story, we secured the App Service to accept only authorized requests. In the third story, we will use our Next.js app to get the Azure AD B2C Access Token to create Resources on Azure via the App Service created initially.

I must admit that the ramp-up of the app service instance in the free plan is not very fast, so consider using a paid plan where the instance is “always on” if you use the service beyond the development.

First API Calls to App Service via Postman

Before integrating our Next.js project, I recommend making some test requests via Postman, which will significantly speed up the later development.

Full example to get the access token in Postman from Azure AD B2C

We create a new “POST” Request in Postman, and we use this endpoint: https://yourAppServiceName.azurewebsites.net/api/storage/create-storage-account

  1. In the Authorization Tab, we select “OAuth 2.0” and add authorization data to “Request Headers”

  2. You can define a name for the Token

Postman Create Request 1
Postman Create Request 1

Postman Create Request 2
Postman Create Request 2

If you click on the button “Get New Access Token” Postman will open a pop-up window with the default “Azure AD B2C User Flow Web Interface”, where we can enter the username and password to get the needed Access Token via Callback to https://oauth.pstmn.io/v1/browser-callback

Setting Redirect-URI in Azure APP Registration

To make this happen, we must add a new setting in the previously created App Registration. We click on “Authentication” -> “Add a platform” -> “Single Page Application,” and then we add the Postman URI.

App Registration Redirect URL
App Registration Redirect URL

POST Request Body Post

The Json in the Raw body should look like the example data below.

“user_az_id”: -> ObjectID from an existing Azure AD B2C User

“user_email”: -> Email from an existing Azure AD B2C User, which we will use to send a confirmation mail via Resend.com

{
    "resourceGroupName": "myResourceSta",
    "accountName": "cloudappstat12",
    "location": "westeurope",
    "containerName": "mycontainer",
    "user_az_id": "xxx-xxxx-xxxx",
    "user_email": "xxxxx@gmail.com", 
    "tags": {
        "environment": "dev"
    }
}

With this JSON Body in place, APP Service will create the resource.

Providing Environment Variables to App Service

But wait. We forgot to provide the right environment variables to the App Service because since the logic is creating Azure Resources, we need an APP Registration with proper rights.

Env Variables App Service
Env Variables App Service

Here, we have to distinguish between the App Registration that we used to Secure the App Service from the outside and the App Registration that we will use to create Resources within our Azure Subscription.

Since the securing part was handled in the previous story, you can get all the details here:

https://www.cloudapp.dev/next-js-14-building-a-saas-solution-in-azure-authentication-part-2

New App Registration for Resource Creation

Let’s create a new app registration called “iac_azure.” For “Supported account types” select -> Accounts in this organizational directory only (xxxxx only — Single tenant)

Enable “Access tokens” and “ID tokens” in the “Authentication tab. No special setting in “API permissions” is needed. Create a new Client Secret under “Certificates & secrets”.

After creating the app registration, I added the service principal (app registration) to the section “Access control (IAM)” in the subscription with the “contributor” role.

With the new app registration, we have these four env variables:

  1. AZURE_CLIENT_ID

  2. AZURE_CLIENT_SECRET

  3. AZURE_SUBSCRIPTION_ID

  4. AZURE_TENANT_ID

The variable “aadb2c_AUTHENTICATION_SECRET” was automatically added when we set up the AD Auth for the App Service. DATABASE_URL is only needed if you want to save data to a Postgres DB; if not, remove the corresponding code from the logic. RESEND_API_KEY is required by the mail sent out after successfully creating the resource.

The logic takes the values from the AZURE env variables for the authentication.

Nextjs 14 logic for calling App Service

I start with the creation of a new component, followed by several API Routes.

Component - createstorageaccountform_appservice.component.tsx

With the function checkIfUserHasStorageAccount, I check if the user already has a storage account/resource group. If yes I show the button for deleting the resourcegroup/storage account, otherwise the user can create them.

With the function generateRandomName I generate Random names for the resourcegroup/storgeaccount.

I use the accesstoken from Azure AD B2C which I get with “usesession” -> const token = session?.accessToken;

"use client";
import React, { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import LoadingButton from "@/components/tools/loadingbutton/loadingbutton.component";

// Utility function to generate a random alphanumeric string of a specified length
const generateRandomName = (length: number) => {
  const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
  let result = "";
  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * characters.length));
  }
  return result;
};

const CreateStorageAccountFormAppService: React.FC = () => {
  const { data: session } = useSession();
  const user_az_id = session?.user?.sub;
  const user_email = session?.user?.email;

  //Get Access Token
  const token = session?.accessToken;

  const [resourceGroupName, setResourceGroupName] = useState(
    generateRandomName(24)
  ); // Random default value
  const [accountName, setAccountName] = useState(generateRandomName(24)); // Random default value
  const [location, setLocation] = useState("westeurope"); // Default to "westeurope"
  const [tags, setTags] = useState<{ [key: string]: string }>({
    env: "production",
    user: user_az_id || "",
  }); // Default value with example tag
  const [containerName, setContainerName] = useState("mycontainer"); // Default value
  const [loading, setLoading] = useState(false);
  const [message, setMessage] = useState("");
  const [containers, setContainers] = useState<string[]>([]); // State to store fetched containers
  const [azureops, setAzureOps] = useState(""); // State for Azure Operations
  const [hasStorageAccount, setHasStorageAccount] = useState(false); // State to check if user already has a storage account

  // Function to check if user already has a storage account
  const checkIfUserHasStorageAccount = async () => {
    if (!user_az_id) return; // No user ID available

    try {
      const response = await fetch(
        "/api/azure/storageaccount/check-storage-account",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ userId: user_az_id }),
        }
      );

      if (response.ok) {
        const data: { hasStorageAccount: boolean } = await response.json();
        setHasStorageAccount(data.hasStorageAccount);
      } else {
        console.error("Error checking storage account existence.");
      }
    } catch (error) {
      console.error("Error:", error);
    }
  };

  // Funktion zum Abrufen des Werts aus der Datenbank
  const fetchData = async () => {
    try {
      const operation = azureops;
      const value = "status";

      const responseOps = await fetch("/api/azure/resources/manageoperations", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          resourceGroupName,
          operation,
          value,
        }),
      });

      const data = await responseOps.json();

      if (azureops === "create") {
        console.log("Data: ", data.data.creation);
        if (data.data.creation === "Success") {
          setMessage("Storage account created successfully!");
          setHasStorageAccount(true); // Update the state to reflect the creation
          setLoading(false);
        }
      }
      if (azureops === "delete") {
        console.log("Data: ", data.data.deletion);
        if (data.data.deletion === "Success") {
          setMessage("Storage account successfully deleted!");
          setHasStorageAccount(false); // Update the state to reflect the creation
          setLoading(false);
        }
      }
    } catch (error) {
      console.error("Fehler beim Abrufen der Daten:", error);
    }
  };

  useEffect(() => {
    checkIfUserHasStorageAccount();
  }, [session]);

  // Überwacht Datenbankwertänderungen
  useEffect(() => {
    // Datenbankabfrage alle 5 Sekunden
    if (loading === false) return; // Nur abfragen, wenn der Ladezustand true ist
    const intervalId = setInterval(fetchData, 5000);

    return () => clearInterval(intervalId); // Bereinigen, wenn die Komponente unmontiert wird
  }, [loading === true]);

  // Delete storage account

  const handleDeleteStorageAccount = async () => {
    if (!accountName) return; // No storage account name available

    setLoading(true);
    setMessage("");
    setAzureOps("delete");

    try {
      // Creating Records in Operationstable
      const operation = "delete";
      const value = "pending";

      // Call the function to create a record in the operation table
      const responseOps = await fetch("/api/azure/resources/manageoperations", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          resourceGroupName,
          operation,
          value,
        }),
      });

      const response = await fetch(
        "/api/azure/resources/deleteresourcegroupappservice",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: "Bearer " + token,
          },
          body: JSON.stringify({
            tagKey: "user",
            tagValue: user_az_id,
            resourceGroupName: resourceGroupName,
          }),
        }
      );

      if (response.ok) {
        const successData = await response.text();
        setMessage(`Success: ${successData}`);
      } else {
        const errorData = await response.text();
        setMessage(`Error: ${errorData}`);
      }
    } catch (error) {
      setMessage(`Error: ${error}`);
    }
  };

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);
    setMessage("");
    setAzureOps("create");
    setContainers([]); // Clear previous container list

    // Check if storageaccount and/or Resource Group already exists

    try {
      // Creating Records in Operationstable
      const operation = "create";
      const value = "pending";

      // Call the function to create a record in the operation table
      const responseOps = await fetch("/api/azure/resources/manageoperations", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          resourceGroupName,
          operation,
          value,
        }),
      });

      const response = await fetch(
        "/api/azure/resources/createstorageaccountappservice",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: "Bearer " + token,
          },
          body: JSON.stringify({
            resourceGroupName,
            accountName,
            location,
            tags,
            containerName,
            user_az_id,
            user_email,
          }),
        }
      );

      if (response.ok) {
        const successData = await response.text();
        setMessage(`Success: ${successData}`);
      } else {
        const errorData = await response.text();
        setMessage(`Error: ${errorData}`);
      }
    } catch (error) {
      setMessage(`Error: ${error}`);
    }
  };

  return (
    <div className="max-w-lg p-6 mx-auto mt-6 bg-gray-400 rounded-lg shadow-md">
      <h2 className="mb-4 text-2xl font-bold text-center">
        {hasStorageAccount
          ? "You already have a storage account"
          : "Create Storage Account"}
      </h2>
      {/* Delete Storage Account Button */}
      {hasStorageAccount && (
        <LoadingButton
          loading={loading}
          onClick={handleDeleteStorageAccount}
          startlabel="Deleting Storage Account"
          loadinglabel="Deleting ..."
        />
      )}
      {!hasStorageAccount && (
        <LoadingButton
          loading={loading}
          onClick={handleSubmit}
          startlabel="Creating Storage Account"
          loadinglabel="Creating ..."
        />
      )}

      {/* Message display */}
      {message && (
        <p className="mt-4 text-sm font-medium text-center">{message}</p>
      )}
      {/* Containers list display */}
      {containers.length > 0 && (
        <div className="mt-6">
          <h3 className="mb-2 text-lg font-semibold">Containers:</h3>
          <ul className="list-disc list-inside">
            {containers.map((container) => (
              <li className="text-gray-600" key={container}>
                {container}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
};

export default CreateStorageAccountFormAppService;

API Route CreateStorageAccountAppservice

As the Payload I send this data:

resourceGroupName,
      accountName,
      location,
      containerName,
      user_email,
      user_az_id,
      tags,
import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";

export const dynamic = "force-dynamic";

export async function POST(req: NextRequest) {
  const authtoken = await getToken({ req });
  const isAdmin = authtoken?.role === "Admin";
  if (!authtoken || !isAdmin) {
    return NextResponse.json(
      { error: "Unauthorized Create Storage API" },
      { status: 401 }
    );
  }

  // Respond with a success message
  const accessToken = authtoken?.accessToken;

  if (!accessToken) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  try {
    const {
      resourceGroupName,
      accountName,
      location,
      containerName,
      user_email,
      user_az_id,
      tags,
    } = await req.json();

    // Payload to send
    const payload: any = {
      resourceGroupName,
      accountName,
      location,
      tags,
      containerName,
      user_email,
      user_az_id,
    };
    // Convert the payload to a JSON string
    const message = payload;

    if (!message) {
      return NextResponse.json(
        { error: "Message body is required." },
        { status: 400 }
      );
    }

    // Rest API URL Service Bus

    const url = `https://${process.env.AZURE_APP_SERVICE_NAME}.azurewebsites.net/api/storage/create-storage-account`;

    try {
      // Send message to Service Bus Queue using Azure REST API

      const response = await fetch(url, {
        method: "POST",
        body: JSON.stringify(payload),
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json",
        },
      });

      if (!response.ok) {
        throw new Error(`HTTP error ServiceBus! status: ${response.status}`);
      }

      // Check if the response body exists and is not empty
      const responseText = await response.text();
      if (responseText) {
        return NextResponse.json({ message: "Message sent successfully" });
        // const data = JSON.parse(responseText);
        // return data;
      } else {
        // console.warn("No response body received");
        // return NextResponse.json({ message: responseText });
        return NextResponse.json({
          message: "Message sent successfully via Text" + responseText,
        });
      }

      // I removed resopnse.json() because it was causing an error, since the StatusCode is 201 and there is no response body
      // const data = await response.json();
    } catch (err) {
      console.error("Error sending message to App Service", err);
      return NextResponse.json(
        { error: "Error sending message to App Service" },
        { status: 500 }
      );
    }
  } catch (err) {
    console.error("Error processing request:", err);
    return NextResponse.json({ error: "Invalid request" }, { status: 400 });
  }
}

API Route DeleteStorageAccountAppservice

As the payload, I send the Resourcegroupname, TagKey, and TagValue

import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";

export const dynamic = "force-dynamic";

export async function POST(req: NextRequest) {
  const authtoken = await getToken({ req });
  const isAdmin = authtoken?.role === "Admin";
  if (!authtoken || !isAdmin) {
    return NextResponse.json(
      { error: "Unauthorized Create Storage API" },
      { status: 401 }
    );
  }

  // Respond with a success message
  const accessToken = authtoken?.accessToken;

  if (!accessToken) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  try {
    const { tagKey, tagValue, resourceGroupName } = await req.json();

    // Payload to send
    const payload: any = {
      tagKey,
      tagValue,
      resourceGroupName,
    };
    // Convert the payload to a JSON string
    const message = payload;

    if (!message) {
      return NextResponse.json(
        { error: "Message body is required." },
        { status: 400 }
      );
    }

    // Rest API URL Service Bus
    const url = `https://${process.env.AZURE_APP_SERVICE_NAME}.azurewebsites.net/api/storage/delete-resource-groups-by-tag`;

    try {
      const response = await fetch(url, {
        method: "DELETE",
        body: JSON.stringify(payload),
        headers: {
          Authorization: `Bearer ${accessToken}`,
          "Content-Type": "application/json",
        },
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      // Check if the response body exists and is not empty
      const responseText = await response.text();
      if (responseText) {
        return NextResponse.json({ message: "Message sent successfully" });
        // const data = JSON.parse(responseText);
        // return data;
      } else {
        // console.warn("No response body received");
        return NextResponse.json({ message: responseText });
        // return NextResponse.json({ message: "Message sent successfully" });
      }
    } catch (err) {
      console.error("Error sending message to App Service:", err);
      return NextResponse.json(
        { error: "Error sending message to App Service" },
        { status: 500 }
      );
    }
  } catch (err) {
    console.error("Error processing request:", err);
    return NextResponse.json({ error: "Invalid request" }, { status: 400 });
  }
}

API Route manageoperations

It is used to interact with the Postgres DB via Prisma

import { NextRequest, NextResponse } from "next/server";
import { getToken } from "next-auth/jwt";
import prisma_storageaccount from "@/lib/prisma_storageaccounts";

export const dynamic = "force-dynamic";

export async function POST(req: NextRequest) {
  const authtoken = await getToken({ req });
  const isAdmin = authtoken?.role === "Admin";
  if (!authtoken || !isAdmin) {
    return NextResponse.json(
      { error: "Unauthorized Operations API" },
      { status: 401 }
    );
  }

  try {
    const { resourceGroupName, operation, value } = await req.json();

    if (operation == "") {
      return NextResponse.json(
        { error: "Invalid request. Operation is missing" },
        { status: 400 }
      );
    }

    const creationvalue = operation === "create" ? value : "NoOps"; // If the operation is create, set the creation value to the value, otherwise set it to null
    const deleteionvalue = operation === "delete" ? value : "NoOps"; // If the operation is delete, set the deletion value to the value, otherwise set it to null

    const operationGetResult = await prisma_storageaccount.operations.findFirst(
      {
        where: {
          resourcegroup: resourceGroupName,
        },
      }
    );

    // If value is status, return the operation result
    if (value === "status") {
      return NextResponse.json({ data: operationGetResult }, { status: 200 });
    }

    let operationResult: any = "";

    if (!operationGetResult) {
      operationResult = await prisma_storageaccount.operations.create({
        data: {
          resourcegroup: resourceGroupName,
          creation: creationvalue,
          deletion: deleteionvalue,
        },
      });
    } else {
      if (operation === "create") {
        operationResult = await prisma_storageaccount.operations.update({
          where: {
            id: operationGetResult.id,
          },
          data: {
            creation: creationvalue,
          },
        });
      } else if (operation === "delete") {
        operationResult = await prisma_storageaccount.operations.update({
          where: {
            id: operationGetResult.id,
          },
          data: {
            deletion: deleteionvalue,
          },
        });
      }
    }

    return NextResponse.json({ message: operationResult }, { status: 200 });
  } catch (err) {
    console.error("Error processing request:", err);
    return NextResponse.json({ error: "Invalid request" }, { status: 400 });
  }
}

I integrated the component into a “dashboard” page where the Next-Auth integration with AZURE AD B2C was already present, so I could easily use the UserID, EmailAddress, AccessToken, etc.

Now, we have a nice-looking component for creating and deleting Azure Resources, which you can extend as you want.

Here is the GitHub Repo for the AppService

https://github.com/cloudapp-dev/azure-app-service-iac

Here is the GitHub Repo for the front-end

https://github.com/cloudapp-dev/nextjs14-advanced-algoliasearch/tree/nextjs1

Play around with the code, and stay tuned for the next part of the tutorial.

Cloudapp-dev, and before you leave us

Thank you for reading until the end. Before you go:

Please consider clapping and following the writer! 👏 on our Medium Account

Or follow us on twitter -> Cloudapp.dev

Related articles