Muitas vezes temos a necessidade de integrar as APIs da Azure em nossas aplicações e fluxos de trabalho. A Azure nos oferece uma série de APIs especializadas!
Hoje vamos fazer algumas chamadas a API de cost management, com objetivo de obter os gastos diários de qualquer subscription pertencente a sua tenant. Inicialmente vamos registrar uma nova application em nosso Azure AD.
Nossa aplicação ira interagir com o Azure por conta própria, não necessitando que nenhum usuário esteja autenticado. Por isso, será preciso gerar um certificado ou segredo para essa aplicação.
Copie o valor do segredo. Além disso, devemos também salvar outros valores referentes a nossa tenant Id, subscription Id e client Id.
Para que nossa aplicação possa ler os valores dos gastos diários, devemos conceder a permissão de Reader da nossa subscription para nossa aplicação. Vamos utilizar o Azure RBAC!
Dentro da subscription, devemos ir menu "Access control (IAM)" e clicar em "Add role assignment".
Basta selecionarmos a permissão de Reader e a nossa aplicação como alvo da atribuição.
Com isso pronto, já podemos criar nosso aplicação. Vamos criar um novo diretório, uma solution do .NET e um projeto web básico.
mkdir AzureCostManagement
dotnet new sln
dotnet new web -o AzureCostManagement.Api
Devemos adicionar o projeto a solution recém criada.
dotnet sln add AzureCostManagement.Api/AzureCostManagement.Api.csproj
Podemos adicionar algumas configurações ao appsettings.json do nosso projeto.
"Application": {
"AzureAdLoginUrlResource" : "https://login.microsoftonline.com",
"AzureAdLoginUrlPath" : "/{0}/oauth2/v2.0/token",
"AzureCostManagementUrlResource": "https://management.azure.com",
"AzureCostManagementUrlPath": "/subscriptions/{0}/providers/Microsoft.CostManagement/forecast?api-version=2022-10-01",
"ClientId" : "<your-client-id>",
"ClientSecret" : "<your-client-secret>",
"TenantId" : "<your-tenant-id>"
}
Teremos dois serviços implementados em nosso projeto:
- IEnterpriseIdentityService: responsável por autenticar a aplicação, e consequentemente obter o access token;
- ICostManagementService: serviço responsável em fazer a chamada à API https://management.azure.com.
Vamos as interfaces e implementações:
IEnterpriseIdentityService
using AzureCostManagement.Api.Models;
namespace AzureCostManagement.Api
{
public interface IEnterpriseIdentityService
{
Task<AccessToken> GetAccessToken(string scope);
}
}
using System.Text.Json;
using AzureCostManagement.Api.Models;
namespace AzureCostManagement.Api
{
public class AzureAdService : IEnterpriseIdentityService
{
private readonly IConfiguration _configuration;
private readonly HttpClient _httpClient;
public AzureAdService(
IConfiguration configuration,
IHttpClientFactory httpclientFactory)
{
_configuration = configuration;
_httpClient = httpclientFactory.CreateClient();
}
public async Task<AccessToken> GetAccessToken(string scope)
{
var url = _configuration.GetSection("Application:AzureAdLoginUrlResource").Value!;
var path = _configuration.GetSection("Application:AzureAdLoginUrlPath").Value!;
var dict = new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", _configuration.GetSection("Application:ClientId").Value! },
{ "client_secret", _configuration.GetSection("Application:ClientSecret").Value! },
{ "scope", $"{scope}/.default" }
};
var fullUrl = string.Format(
$"{url}{path}",
_configuration.GetSection("Application:TenantId").Value!);
var requestBody = new FormUrlEncodedContent(dict);
var response = await _httpClient.PostAsync(fullUrl, requestBody);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<AccessToken>(result)!;
}
}
}
using System.Text.Json.Serialization;
namespace AzureCostManagement.Api.Models
{
public record AccessToken
{
[JsonPropertyName("token_type")]
public string TokenType { get; set; } = default!;
[JsonPropertyName("expires_in")]
public long ExpiresIn { get; set; }
[JsonPropertyName("access_token")]
public string Token { get; set; } = default!;
}
}
ICostManagementService
namespace AzureCostManagement.Api.Models
{
public interface ICostManagementService
{
Task<CostManagementResult> GetUsageCostsAync(Guid subscriptionId, DateTime endDate, DateTime startDate);
}
}
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AzureCostManagement.Api.Models
{
public record CostManagementResult
{
[JsonPropertyName("id")]
public string Id { get; init; } = default!;
[JsonPropertyName("name")]
public string Name { get; init; } = default!;
[JsonPropertyName("type")]
public string Type { get; init; } = default!;
[JsonPropertyName("properties")]
public CostManagementProperties Properties { get; init; } = default!;
}
public record CostManagementProperties
{
[JsonPropertyName("nextLink")]
public string? NextLink { get; init; }
[JsonPropertyName("columns")]
public IEnumerable<CostManagementColumn> Columns { get; init; } = default!;
[JsonPropertyName("rows")]
public IEnumerable<IEnumerable<dynamic>> Rows { get; init; } = default!;
public IEnumerable<CostManagementRow> RowsFormated
{
get
{
var dateFormat = "yyyyMMdd";
return Rows.Select(x => new CostManagementRow
{
// Amount
Amount = ((JsonElement)x.ToArray<object>()[0]).GetDouble(),
// Date
Date = DateTime.ParseExact(
((JsonElement)x.ToArray<object>()[1]).GetInt64().ToString(),
dateFormat,
CultureInfo.InvariantCulture),
// CostStatus
CostStatus = ((JsonElement)x.ToArray<object>()[2]).GetString()!,
// Currency
Currency = ((JsonElement)x.ToArray<object>()[3]).GetString()!,
});
}
}
}
public record CostManagementRow
{
public double Amount { get; init; }
public DateTime Date { get; init; }
public string CostStatus { get; init; } = default!;
public string Currency { get; init; } = default!;
}
public record CostManagementColumn
{
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("type")]
public string Type { get; set; } = null!;
}
}
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using AzureCostManagement.Api.Models;
namespace AzureCostManagement.Api
{
public class AzureCostManagementService : ICostManagementService
{
private readonly IEnterpriseIdentityService _enterpriseIdentityService;
private readonly IConfiguration _configuration;
private readonly HttpClient _httpClient;
public AzureCostManagementService(
IEnterpriseIdentityService enterpriseIdentityService,
IConfiguration configuration,
IHttpClientFactory httpClientFactory)
{
_enterpriseIdentityService = enterpriseIdentityService;
_configuration = configuration;
_httpClient = httpClientFactory.CreateClient();
}
public async Task<CostManagementResult> GetUsageCostsAync(Guid subscriptionId, DateTime endDate, DateTime startDate)
{
var url = _configuration.GetSection("Application:AzureCostManagementUrlResource").Value!;
var path = _configuration.GetSection("Application:AzureCostManagementUrlpath").Value!;
var acessToken = await _enterpriseIdentityService.GetAccessToken(url);
var request = new CostManagementRequestBuilder()
.WithStartDate(startDate)
.WithEndDate(endDate)
.Build();
var json = JsonSerializer.Serialize(request);
var data = new StringContent(json, Encoding.UTF8, "application/json");
var fullUrl = string.Format(
$"{url}{path}",
subscriptionId);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", acessToken.Token);
var response = await _httpClient.PostAsync(fullUrl, data);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<CostManagementResult>(result)!;
}
}
}
Observe que injetamos via DI o serviço IEnterpriseIdentityService dentro da implementação AzureCostManagementService.
Com os serviços prontos, podemos implementar a classe principal da nossa aplicação!
Program.cs
Como estamos utilizando uma miniaml API, todas as configurações e métodos HTTP irão estar no mesmo arquivo. Iremos injetar via depedency injection o serviço de cost management no método GET e retornar aos usuários os valores de gastos da subscription formatados.
using AzureCostManagement.Api;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<ICostManagementService, AzureCostManagementService>();
builder.Services.AddScoped<IEnterpriseIdentityService, AzureAdService>();
builder.Services.AddHttpClient();
var app = builder.Build();
app.MapGet("/cost-management", async (
[FromQuery] Guid subscritionId,
ICostManagementService costManagementService) =>
{
var today = DateTime.Today;
var firstDayOfCurrentMonth = new DateTime(today.Year, today.Month, 1);
var lastDayOfCurrentMonth = firstDayOfCurrentMonth.AddMonths(1).AddDays(-1);
var data = await costManagementService.GetUsageCostsAync(
subscritionId,
lastDayOfCurrentMonth,
firstDayOfCurrentMonth);
return Results.Ok(new
{
Values = data.Properties.RowsFormated
});
});
app.Run();
Podemos executar nosso projeto e fazer a chamada HTTP, passando como query string o valor da subscriptionId.
dotnet run
Temos o resultado:
Isso é tudo pessoal! Você já pode baixar o projeto por esse link, e não esquece de me seguir no LinkedIn!
Até a próxima, abraços!