Manage Azure resource groups with TypeScript Function API
In this tutorial, you'll create a local TypeScript Azure Function app with APIs to manage Azure resource groups and deploy the app to Azure.
Features and functionality:
- Create local TypeScript Azure Function app project in Visual Studio Code
- Create function API boilerplate code in Visual Studio Code
- Deploy to Azure Functions from Visual Studio Code
- Create service principal with Azure CLI
- Configure local and remote application settings with Visual Studio Code
- Use DefaultAzureCredential in both local and remote environments for passwordless connections
- Use Azure Identity and Azure Resource Management SDKs to manage Azure resources
- Use your local and cloud APIs to create, delete, and list resource groups in your subscription
Warning
This tutorial is meant for quick adoption and as such it doesn't follow secure-by-default requirements. To understand more about this scenario with a secure-by-default goal, go to Security considerations.
While the source code is written with TypeScript, the source code is simple. If you're comfortable with modern JavaScript using async/await, the code will be familiar to you.
Prerequisites
- An Azure user account and subscription: create a free subscription.
- Node.js LTS 18+ and npm installed to your local machine. Your local development environment version of Node.js should match one of the available Azure Function cloud runtime versions.
- Visual Studio Code installed to your local machine.
- Azure Function extension v1.10.4 or above.
- Azure Functions Core Tools v4.0.5095 or above
- Azure Cloud Shell or Azure CLI installed to your local machine.
Application architecture
The app provides the following API endpoints.
Method | URL | Description |
---|---|---|
POST,DELETE | http://localhost:7071/api/resourcegroup | Add or delete a resource group. While adding, include tags (key/value pairs) to identify the purpose of the group later. |
GET | http://localhost:7071/api/resourcegroups | List all resource groups in subscription. |
GET | http://localhost:7071/api/resources | List all resources in a subscription or resource group. |
While these endpoints are public, you should secure your API endpoints with authentication and authorization before deploying to your live environment.
This app is limited to a subscription because that is the scope specified when creating the service principal.
1. Preparing your environment
You must prepare your local and cloud environments to use the Azure Identity SDK.
Sign in to Azure CLI
In a bash terminal, sign in to the Azure CLI with the following command:
az login
Get your Azure subscription ID
In a bash terminal, get your subscriptions and find the subscription ID you want to use. The following query returns the subscription ID, subscription name, and tenant ID sorted by subscription name.
az account list --query "sort_by([].{Name:name, SubscriptionId:id, TenantId:tenantId}, &Name)" --output table
Copy the subscription ID to the previous temporary file. You'll need this setting later.
Create an Azure service principal
An Azure service principal provides access to Azure without having to use your personal user credentials. For this tutorial, the service principal can be used both in your local and cloud environments. In an enterprise environment, you would want separate service principals for each environment.
Determine a service principal name format so you can easily find your service principal later. For example, several format ideas are:
- Your project and owner:
resource-management-john-smith
. - Your department and date:
IT-2021-September
- Your project and owner:
In a bash terminal, create your service principal with az ad sp create-for-rbac. Replace
<SUBSCRIPTION-ID>
with your subscription ID.az ad sp create-for-rbac --name YOUR-SERVICE-PRINCIPAL-NAME --role Contributor --scopes /subscriptions/<SUBSCRIPTION-ID>
Copy the entire output results to a temporary file. You'll need these settings later.
{ "appId": "YOUR-SERVICE-PRINCIPAL-ID", "displayName": "YOUR-SERVICE-PRINCIPAL-NAME", "name": "http://YOUR-SERVICE-PRINCIPAL-NAME", "password": "YOUR-SERVICE-PRINCIPAL-PASSWORD", "tenant": "YOUR-TENANT-ID" }
2. Create local Azure Function app in Visual Studio Code
Create an Azure Function app in Visual Studio Code to manage Azure resource groups.
Create function app
Use Visual Studio Code to create a local Function app.
In a bash terminal, create and change into a new directory:
mkdir my-function-app && cd my-function-app
In a bash terminal, open Visual Studio Code:
code .
Open the Visual Studio Code command palette: Ctrl + Shift + p.
Enter
Azure Functions: create new project
. Use the following table to finish the prompts:Prompt Value Select the folder that will contain your function project Select the default (current) directory Select a language Select TypeScript. Select a TypeScript programming model Select Model V4 (Preview) Select a template for your project's first function Select HTTP trigger. Create new HTTP trigger Enter the API name of resourcegroups
.Authorization level Select anonymous. If you continue with this project after this article, change the authorization level to the function. Learn more about Function-level authorization. The project boilerplate is created and the dependencies are installed.
Add service principal settings to local.settings.json file
Open the
./local.settings.json
file in the project root directory and add your VALUES section with the five following environment variables.{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "", "FUNCTIONS_WORKER_RUNTIME": "node", "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", "AZURE_CLIENT_ID": "REPLACE-WITH-SERVICE-PRINCIPAL-APPID", "AZURE_CLIENT_SECRET": "REPLACE-WITH-SERVICE-PRINCIPAL-PASSWORD", "AZURE_SUBSCRIPTION_ID":"REPLACE-WITH-SUBSCRIPTION-ID", "AZURE_TENANT_ID":"REPLACE-WITH-SERVICE-PRINCIPAL-TENANT", "NODE_ENV":"development" } }
Refer to your settings from the previous section to add the values. These environment variables are REQUIRED for the context to use DefaultAzureCredential.
AZURE_TENANT_ID
:tenant
from the service principal output above.AZURE_CLIENT_ID
:appId
from the service principal output above.AZURE_CLIENT_SECRET
:password
from the service principal output above.
You also need to set the subscription ID. It's required to use the Azure SDK for resource management.
AZURE_SUBSCRIPTION_ID
: Your default subscription containing your resource groups.
This local.settings.json
file is ignored by your local git on purpose so you don't accidentally commit it to your source code.
Install npm dependencies for Azure Identity and Resource management
In a Visual Studio Code integrated bash terminal, install the Azure SDK dependencies for Azure Identity and Resource management.
npm install @azure/identity @azure/arm-resources
List all resource groups in subscription with JavaScript
Open the
./src/functions/resourcegroups.ts
file and replace the contents with the following:import { ResourceGroup } from '@azure/arm-resources'; import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions'; import { createResourceGroup, deleteResourceGroup } from '../lib/azure-resource-groups'; import { processError } from '../lib/error'; export async function resourcegroup( request: HttpRequest, context: InvocationContext ): Promise<HttpResponseInit> { try { console.log(JSON.stringify(request.query)); console.log(JSON.stringify(request.params)); const name: string = request.query.get('name'); const location: string = request.query.get('location'); console.log(`name: ${name}`); console.log(`location: ${location}`); switch (request.method) { case 'POST': // wait for create to complete before returning if (!name || !location) { return { body: 'Missing required parameters.', status: 400 }; } if (request.headers.get('content-type') === 'application/json') { // create with tags const body: Record<string, unknown> = (await request.json()) as Record<string, string>; const tags: Record<string, string> = body?.tags ? (body?.tags as Record<string, string>) : null; const resourceGroup: ResourceGroup = await createResourceGroup( name, location, tags ); return { jsonBody: resourceGroup, status: 200 }; } else { // create without tags const resourceGroup: ResourceGroup = await createResourceGroup( name, location, null ); return { jsonBody: resourceGroup, status: 200 }; } case 'DELETE': // wait for delete to complete before returning if (!name) { return { body: 'Missing required parameters.', status: 400 }; } await deleteResourceGroup(name); return { status: 204 }; } } catch (err: unknown) { return processError(err); } } app.http('resourcegroup', { methods: ['DELETE', 'POST'], authLevel: 'anonymous', handler: resourcegroup });
This file responds to API requests to
/api/resourcegroups
and returns a list of all resource groups in the subscription.Create a subdirectory in
src
namedlib
and create a new file in that directory namedazure-resource-groups.ts
.Copy the following code into the
./src/lib/azure-resource-groups.ts
file:// Include npm dependencies import { ResourceGroup, ResourceManagementClient } from '@azure/arm-resources'; import { DefaultAzureCredential } from '@azure/identity'; import { getSubscriptionId } from './environment-vars'; const subscriptionId = getSubscriptionId(); // Create Azure authentication credentials const credentials = new DefaultAzureCredential(); // Create Azure SDK client for Resource Management such as resource groups const resourceManagement = new ResourceManagementClient( credentials, subscriptionId ); // all resources groups in subscription export const listResourceGroups = async (): Promise<{ list: ResourceGroup[]; subscriptionId: string; }> => { const list: ResourceGroup[] = []; for await (const resourceGroup of resourceManagement.resourceGroups.list()) { list.push(resourceGroup); } return { subscriptionId, list }; }; export const createResourceGroup = async ( resourceGroupName: string, location: string, tags: { [propertyName: string]: string } ): Promise<ResourceGroup> => { const resourceGroupParameters = { location: location, tags }; return await resourceManagement.resourceGroups.createOrUpdate( resourceGroupName, resourceGroupParameters ); }; export const deleteResourceGroup = async ( resourceGroupName: string ): Promise<void> => { return await resourceManagement.resourceGroups.beginDeleteAndWait( resourceGroupName ); };
This file completes the following:
- Gets the subscription ID
- Creates the DefaultAzureCredential context
- Creates the ResourceManagementClient required to use the Resource management SDK.
- Gets all the resource groups in the subscription.
Create a new file in the
./src/lib
directory namedenvironment-vars.ts
and copy the following code into that file.export const checkAzureAuth = () => { // The following code is only used to check you have environment // variables configured. The DefaultAzureCredential reads your // environment - it doesn't read these variables. const tenantId = process.env['AZURE_TENANT_ID']; if (!tenantId) throw Error('AZURE_TENANT_ID is missing from environment variables.'); const clientId = process.env['AZURE_CLIENT_ID']; if (!clientId) throw Error('AZURE_CLIENT_ID is missing from environment variables.'); const secret = process.env['AZURE_CLIENT_SECRET']; if (!secret) throw Error('AZURE_CLIENT_SECRET is missing from environment variables.'); }; export const getSubscriptionId = (): string => { checkAzureAuth(); // Get subscription from environment variables const subscriptionId = process.env['AZURE_SUBSCRIPTION_ID']; if (!subscriptionId) throw Error('Azure Subscription is missing from environment variables.'); return subscriptionId; };
This file checks the environment variables before returning the subscription ID.
Create a new file in the
./src/lib
directory namederror.ts
and copy the following code into that file.export function processError(err: unknown): any { if (typeof err === 'string') { return { body: err.toUpperCase(), status: 500 }; } else if ( err['stack'] && process.env.NODE_ENV.toLowerCase() !== 'production' ) { return { jsonBody: { stack: err['stack'], message: err['message'] } }; } else if (err instanceof Error) { return { body: err.message, status: 500 }; } else { return { body: JSON.stringify(err) }; } }
This file returns a 500 error with the error message. The stack is returned if the
NODE_ENV
variable isn't set toproduction
.
Test local functions
In the Visual Studio Code integrated terminal, run the local project:
npm start
Wait until the integrated bash terminal displays the running function's URL.
Open a second integrated bash terminal in Visual Studio Code, Ctrl + Shift + 5, and use the following GET cURL command to use the API:
curl http://localhost:7071/api/resourcegroups
If you have many resource groups in your subscription, you may want to pipe the output to a file for easier review.
curl http://localhost:7071/api/resourcegroups > resourcegroups.json
The response includes
subscriptionId
and alist
of all resource groups in that subscription.{ "subscriptionId": "ABC123", "list": [ { "id": "/subscriptions/ABC123/resourceGroups/vmagelo-cloudshell", "name": "jsmith-cloudshell", "type": "Microsoft.Resources/resourceGroups", "properties": { "provisioningState": "Succeeded" }, "location": "westeurope" }, ... REMOVED FOR BREVITY ... ] }
Troubleshooting
If you couldn't complete this article, check the following table for issues. If your issue isn't listed in the table, open an issue on this documentation page.
Issue | Fix |
---|---|
The app didn't start. | Review the errors. Make sure you installed the required dependencies. |
The app started but you can't get a 200 response. | Make sure your curl command is requesting from the correct local route. |
The API returned a 200 response but returned no results. | Use the Visual Studio Code extension for Azure Resources to verify that your subscription has any resource groups. If you don't see any resource groups, don't worry. This tutorial adds an API to create and delete resource groups in your subscription. This API is added after the first deployment of the source code to Azure, so that you learn how to redeploy your code. |
3. Create cloud-based Azure Function app
In Visual Studio Code, select the Azure icon to open the Azure Explorer.
Select the + icon to create a new Azure Function app in the Azure cloud.
Select Create Function App in Azure.
Enter a globally unique name for the new function app. The name must be unique across all Azure functions. For example,
jsmith-rg-management
.Select the same Node.js 18+ LTS runtime you selected when you created your local function app.
Select a geographical location close to you such as West US 3.
Wait until the resource is created. You can watch the Azure: Activity Log for details.
4. Configure cloud-based Azure Function app
You need to configure your Azure app settings to connect to the Azure Function app. Locally, these settings are in your local.settings.json
file. This process adds those values to your cloud app.
In Visual Studio Code, in the Azure explorer, in the Resources section, expand Function App then select your function app.
Right-click on Application Settings and select Add New Setting.
Add the four values from your
local.settings.json
with the exact same name and values.AZURE_TENANT_ID
:tenant
from the service principal output above.AZURE_CLIENT_ID
:appId
from the service principal output above.AZURE_CLIENT_SECRET
:password
from the service principal output above.AZURE_SUBSCRIPTION_ID
: Your default subscription containing your resource groups.AzureWebJobsFeatureFlags
:EnableWorkerIndexing
5. Deploy Resource Manager function app
Deploy an Azure Function app in Visual Studio Code to manage Azure resource groups.
Use Visual Studio Code extension to deploy to hosting environment
In VS Code, open the
local.settings.json
file so it is visible. This will make the next steps of copying those names and values easier.Select the Azure logo to open the Azure Explorer, then under Functions, select the cloud icon to deploy your app.
Alternately, you can deploy by opening the Command Palette with Ctrl + Shift + p, entering
deploy to function app
, and running the Azure Functions: Deploy to Function App command.Select Deploy to Function app.
Select the Function App name your created in the previous section.
When asked if you're sure you want to deploy, select Deploy.
The VS Code Output panel for Azure Functions shows progress. When deploying, the entire Functions application is deployed, so changes to all individual functions are deployed at once.
Verify Functions app is available with browser
While still in Visual Studio Code, use the Azure Functions explorer, expand the node for your Azure subscription, expand the node for your Functions app, then expand Functions (read only). Right-click the function name and select Copy Function Url:
Paste the URL into a browser and press Enter to request the resource group list from the cloud API.
6. Add APIs to function app and redeploy to Azure
Add the following APIs then redeploy your Azure Function app in Visual Studio Code:
- Add and delete resource groups
- List resources in resource group or subscription.
At this point in the tutorial, you created a local function app with one API to list your subscription's resource groups and you deployed that app to Azure. As an Azure developer, you may want to create or delete resource groups as part of your process automation pipeline.
Create resourcegroup API for your function app
Use the Visual Studio Code extension for Azure Functions to add the TypeScript files to your function app to create and delete resource groups.
Open the Visual Studio Code command palette: Ctrl + Shift + p.
Enter
Azure Functions: Create Function
then press enter to begin the process.Use the following table to create the /api/resourcegroup API:
Prompt Value Select a template for your function HTTP trigger Provide a function name resourcegroup
Authorization level Select anonymous. If you continue with this project, change the authorization level to the function. Learn more about Function-level authorization. Open the
./src/functions/resourcegroup.ts
and replace the entire file with the following source code.import { ResourceGroup } from '@azure/arm-resources'; import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions'; import { createResourceGroup, deleteResourceGroup } from '../lib/azure-resource-groups'; import { processError } from '../lib/error'; export async function resourcegroup( request: HttpRequest, context: InvocationContext ): Promise<HttpResponseInit> { try { console.log(JSON.stringify(request.query)); console.log(JSON.stringify(request.params)); const name: string = request.query.get('name'); const location: string = request.query.get('location'); console.log(`name: ${name}`); console.log(`location: ${location}`); switch (request.method) { case 'POST': // wait for create to complete before returning if (!name || !location) { return { body: 'Missing required parameters.', status: 400 }; } if (request.headers.get('content-type') === 'application/json') { // create with tags const body: Record<string, unknown> = (await request.json()) as Record<string, string>; const tags: Record<string, string> = body?.tags ? (body?.tags as Record<string, string>) : null; const resourceGroup: ResourceGroup = await createResourceGroup( name, location, tags ); return { jsonBody: resourceGroup, status: 200 }; } else { // create without tags const resourceGroup: ResourceGroup = await createResourceGroup( name, location, null ); return { jsonBody: resourceGroup, status: 200 }; } case 'DELETE': // wait for delete to complete before returning if (!name) { return { body: 'Missing required parameters.', status: 400 }; } await deleteResourceGroup(name); return { status: 204 }; } } catch (err: unknown) { return processError(err); } } app.http('resourcegroup', { methods: ['DELETE', 'POST'], authLevel: 'anonymous', handler: resourcegroup });
The
./src/lib/azure-resource-groups.ts
file already contains the code to add and delete resource groups.
Create resources API for your function app
Use the Visual Studio Code extension for Azure Functions to add the TypeScript files to your function app to list resources in a resource group.
Open the Visual Studio Code command palette: Ctrl + Shift + p.
Enter
Azure Functions: Create Function
then press enter to begin the process.Use the following table to create the /api/resources API:
Prompt Value Select a template for your function HTTP trigger Provide a function name resources
Authorization level Select anonymous. If you continue with this project, change the authorization level to the function. Learn more about Function-level authorization. Open the
./src/functions/resources.ts
and replace the entire file with the following source code.import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions'; import { listResourceByResourceGroup, listResourceBySubscription } from '../lib/azure-resource'; import { processError } from '../lib/error'; export async function resources( request: HttpRequest, context: InvocationContext ): Promise<HttpResponseInit> { try { const resourceGroupName: string = request.query.get('resourceGroupName'); context.log(`resourceGroupName: '${resourceGroupName}'`); if (resourceGroupName) { const resourcesByName = await listResourceByResourceGroup( resourceGroupName ); return { jsonBody: resourcesByName }; } else { const resourcesBySubscription = await listResourceBySubscription(); return { jsonBody: resourcesBySubscription }; } } catch (err: unknown) { return processError(err); } } app.http('resources', { methods: ['GET'], authLevel: 'anonymous', handler: resources });
Create the
./src/lib/azure-resource.ts
file and copy the following code into it to list the resources in a resource group.// Include npm dependencies import { Resource, ResourceManagementClient } from '@azure/arm-resources'; import { DefaultAzureCredential } from '@azure/identity'; import { getSubscriptionId } from './environment-vars'; const subscriptionId = getSubscriptionId(); // Create Azure authentication credentials const credentials = new DefaultAzureCredential(); // Create Azure SDK client for Resource Management such as resource groups const resourceManagement = new ResourceManagementClient( credentials, subscriptionId ); // all resources groups in subscription export const listResourceBySubscription = async (): Promise<{ list: Resource[]; subscriptionId: string; }> => { const list: Resource[] = []; for await (const resource of resourceManagement.resources.list()) { list.push(resource); } return { subscriptionId, list }; }; // all resources groups in resource group export const listResourceByResourceGroup = async ( resourceGroupName: string ): Promise<{ list: Resource[]; subscriptionId: string; resourceGroupName: string; }> => { const list: Resource[] = []; for await (const resource of resourceManagement.resources.listByResourceGroup( resourceGroupName )) { list.push(resource); } return { subscriptionId, resourceGroupName, list }; };
Start your local function app and test the new API
In the Visual Studio Code integrated terminal, run the local project:
npm start
Wait until the integrated bash terminal displays the running function's URL.
Use the following curl commands in a different integrated bash terminal, to call your API, to add a resource group to your subscription. Change the name of the resource group to use your own naming conventions.
curl -X POST 'http://localhost:7071/api/resourcegroup?name=my-test-1&location=westus' curl -X POST 'http://localhost:7071/api/resourcegroup?name=my-test-1&location=westus' \ -H 'content-type: application/json' \ -d '{"tags": {"a":"b"}}'
Use the following curl command to see the new resource group listed in your subscription.
curl http://localhost:7071/api/resource-groups
Use the following curl command to delete the resource group you just added.
curl -X DELETE 'http://localhost:7071/api/resourcegroup?name=my-test-1' \ -H 'Content-Type: application/json'
Redeploy your function app with new APIs to Azure
In VS Code, deploy by opening the Command Palette with Ctrl + Shift + p, entering
deploy to function app
, and running the Azure Functions: Deploy to Function App command.Select your function app from the list of apps.
Select Deploy from the pop-up window.
Wait until the deployment completes.
Verify Function APIs with browser
Use the previous cURL commands, replacing the localhost address, http://localhost:7071
with your Azure Function resource name such as https://myfunction.azurewebsites.net
.
7. View and query your Function app logs
View and query Azure Function app logs in the Azure portal.
Query your Azure Function logs
Use the Azure portal to view and query your function logs.
In VS Code, select the Azure logo to open the Azure Explorer, then under Functions, right-click on your function app, then select Open in Portal.
This opens the Azure portal to your Azure Function.
Select Application Insights from the Settings, then select View Application Insights data.
This link takes you to your separate metrics resource created for you when you created your Azure Function with VS Code.
Select Logs in the Monitoring section. If a Queries pop-up window appears, select the X in the top-right corner of the pop-up to close it.
In the Schema and Filter pane, on the Tables tab, double-click the traces table.
This enters the Kusto query,
traces
into the query window.Edit the query to search for API calls:
traces | where message startswith "Executing "
Select Run.
If the log doesn't display any results, it may be because there's a few minutes delay between the HTTP request to the Azure Function and the log availability in Kusto. Wait a few minutes and run the query again.
Because an Application Insights resource was added for you when you created the Azure Function app, you didn't need to do anything extra to get this logging information:
- The Function app added Application Insights for you.
- The Query tool is included in the Azure portal.
- You can select
traces
instead of having to learn to write a Kusto query to get even the minimum information from your logs.
8. Clean up Azure resources
Delete the resource group
In VS Code, select the Azure logo to open the Azure Explorer, then under Functions, right-click on your function app, then select Open in Portal.This opens the Azure portal to your Azure Function.
In the Overview section, find and select the resource group name. This action takes you to the resource group in the Azure portal.
The resource group page lists all resources associated with this tutorial.
In the top menu, select Delete resource group.
In the side menu, enter the name of the resource group then select Delete.
Delete the service principal
To delete the service principal, run the following command. Replace <YOUR-SERVICE-PRINCIPAL-NAME>
with the name of your service principal.
az ad sp delete --id <YOUR-SERVICE-PRINCIPAL-NAME>
Sample code
Security considerations
This solution, as a beginner tutorial, doesn't demonstrate secure-by-default practices. This is intentional to allow you to be successful in deploying the solution. The next step after that successful deployment is to secure the resources. This solution uses three Azure services, each has its own security features and considerations for secure-by-default configuration:
- Azure Functions - Securing Azure Functions
- Azure Storage - Security recommendations for Blob storage