Is there any way to setup email alerts for when individual Azure Container Instances succeed or fail (or basically change state)? I have some run-once containers I kick off periodically, and would like to be notified when they complete and the status of the completion. Bonus if the email can include logs from the container, as well. Is this possible using the built-in alerts?
So far I haven't been able to make it work with the provided signals.
Dont believe there is an automatic way so what I did was created a small timer based function that runs every 5 minutes and gets a list of all containers and checks the status. If any are in say a failed state it then uses SendGrid to sent an alert email.
UPDATE for Dan
I use Managed Service Identity in my function so I have a container tasks class like the below, can't remember exactly where I got the help to generate the GetAzure function as obviously when doing local debug you can't use the MSI credentials and the local account on Visual Studio that is meant to work doesn't appear to. However I think it might have been here - https://github.com/Azure/azure-sdk-for-net/issues/4968
public static class ContainerTasks
{
private static readonly IAzure azure = GetAzure();
private static IAzure GetAzure()
{
var tenantId = Environment.GetEnvironmentVariable("DevLocalDbgTenantId");
var clientId = Environment.GetEnvironmentVariable("DevLocalDbgClientId");
var clientSecret = Environment.GetEnvironmentVariable("DevLocalDbgClientSecret");
AzureCredentials credentials;
if (!string.IsNullOrEmpty(tenantId) &&
!string.IsNullOrEmpty(clientId) &&
!string.IsNullOrEmpty(clientSecret))
{
var sp = new ServicePrincipalLoginInformation
{
ClientId = clientId,
ClientSecret = clientSecret
};
credentials = new AzureCredentials(sp, tenantId, AzureEnvironment.AzureGlobalCloud);
}
else
{
credentials = SdkContext
.AzureCredentialsFactory
.FromMSI(new MSILoginInformation(MSIResourceType.AppService), AzureEnvironment.AzureGlobalCloud);
}
var authenticatedAzure = Azure
.Configure()
.WithLogLevel(HttpLoggingDelegatingHandler.Level.Basic)
.Authenticate(credentials);
var subscriptionId = Environment.GetEnvironmentVariable("DevLocalDbgSubscriptionId");
if (!string.IsNullOrEmpty(subscriptionId))
return authenticatedAzure.WithSubscription(subscriptionId);
return authenticatedAzure.WithDefaultSubscription();
}
public static IEnumerable<IContainerGroup> ListTaskContainers(string resourceGroupName, ILogger log)
{
log.LogInformation($"Getting a list of all container groups in Resource Group '{resourceGroupName}'");
return azure.ContainerGroups.ListByResourceGroup(resourceGroupName);
}
}
Then my monitor function is simply
public static class MonitorACIs
{
[FunctionName("MonitorACIs")]
public static void Run([TimerTrigger("%TimerSchedule%")]TimerInfo myTimer, ILogger log)
{
log.LogInformation($"MonitorACIs Timer trigger function executed at: {DateTime.Now}");
foreach(var containerGroup in ContainerTasks.ListTaskContainers(Environment.GetEnvironmentVariable("ResourceGroupName"), log))
{
if(String.Equals(containerGroup.State, "Failed"))
{
log.LogInformation($"Container Group {containerGroup.Name} has failed please investigate");
Notifications.FailedTaskACI(containerGroup.Name, log);
}
}
}
}
Notifications.FailedTaskACI is just a class method that sends an email to one of our Teams channels
It's not perfect but it does the job for now!
Related
Similar issue here. I have checked the answer and try to implement all the possible forms of link in my startup.cs class with the following code:
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithTenantId(tenantId)
.WithClientSecret(appSecret)
.WithAuthority(Authority) // Authority contains the link as mentioned in the page(link attached above)
.Build();
I still get the similar error:
"OpenIdConnectMessage.Error was not null, indicating an error. Error: 'invalid_request'. Error_Description (may be empty): 'AADSTS50194: Application 'xxx-xxx-xxx-xxx-xxxx'(ASPNET-Quickstart) is not configured as a multi-tenant application. Usage of the /common endpoint is not supported for such applications created after '10/15/2018'. Use a tenant-specific endpoint or configure the application to be multi-tenant.
Trace ID: xxx-xxx-xxx-xxx-xxxx
Correlation ID: xxx-xxx-xxx-xxx-xxxx
Timestamp: 2022-06-11 05:33:24Z'. Error_Uri (may be empty): 'error_uri is null'."
The combination of links I have used in variable Authority are the following: "https://login.microsoftonline.com/MY_TENANT_NAME" and "https://login.microsoftonline.com/MY_TENANT_ID"
I am being redirect to login page but after entering credentials OnAuthenticationFailedAsync method is being executed. This is the code of my startup class:
[assembly: OwinStartup(typeof(Web.Startup))]
namespace Web
{
public partial class Startup
{
// Load configuration settings from PrivateSettings.config
private static string appId = ConfigurationManager.AppSettings["ida:AppId"];
private static string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private static string graphScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
private static string tenantId = ConfigurationManager.AppSettings["ida:tenantId"];
private static string aadInstance = EnsureTrailingSlash(ConfigurationManager.AppSettings["ida:AADInstance"]);
public static string Authority = "https://graph.microsoft.com/"+ tenantId;
string graphResourceId = "https://graph.microsoft.com/";
public void Configuration(IAppBuilder app)
{
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/common/v2.0",
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
// For demo purposes only, see below
ValidateIssuer = true
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
private static Task OnAuthenticationFailedAsync(AuthenticationFailedNotification<OpenIdConnectMessage,
OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
string redirect = $"/Home/Error?message={notification.Exception.Message}";
if (notification.ProtocolMessage != null && !string.IsNullOrEmpty(notification.ProtocolMessage.ErrorDescription))
{
redirect += $"&debug={notification.ProtocolMessage.ErrorDescription}";
}
notification.Response.Redirect(redirect);
return Task.FromResult(0);
}
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithTenantId(tenantId)
.WithClientSecret(appSecret)
.WithAuthority(Authority)
.Build();
string email = string.Empty;
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
email = await GraphHelper.GetUserDetailsAsync(result.AccessToken);
}
catch (MsalException ex)
{
System.Diagnostics.Trace.TraceError(ex.Message);
}
notification.HandleResponse();
notification.Response.Redirect($"/Account/SignInAzure?email={email}");
}
private static string EnsureTrailingSlash(string value)
{
if (value == null)
{
value = string.Empty;
}
if (!value.EndsWith("/", StringComparison.Ordinal))
{
return value + "/";
}
return value;
}
}
}
My application is for single tenant so please don't suggest me to change the setting and make it for multi-tenant.
Please check below points:
After trying to change it to specific tenant i.e.;
After changing to Ex: - https://login.microsoftonline.com/contoso.onmicrosoft.com (or tenant id),
please save changes ,refresh portal / everything and try again.
If still it shows the error , check if the Application is registered to the Azure AD Tenant as Multi Tenant Application.
Then if it still remains check if the account is actually on Azure
AD ,as this error can occur when the user credentials you are trying
to use does not belong to the same tenant where the application is
actually registered in.
If it is different tenant and you are trying to access from different
account, then you may need to change its supported account types to
any organizational directory or you need to check for correct
credentials. If not check everything or create a new app registration
.
Also please check this "Use a tenant-specific endpoint or configure the application to be multi-tenant" when signing into my Azure website for possible
ways to solve the issue.
Else you can raise a support request
References:
msal - MsalException: Applicationis not configured as a multi-tenant
application. Android - Stack Overflow
Use single-tenant Azure AD apps with Microsoft Graph Toolkit -
Waldek Mastykarz
I'm using a Azure Function .Net 6 Isolated.
When I run the function on localhost from VS2022, it is 5 times faster then when I deploy it to Azure Function. Localhost is a VM hosted in Azure in the same region as the function.
I tried different Service Plans, but issue remains. (Consumption Plan, Elastic Premium EP3, Premium V2 P3v2)
Results in different regions vs. localhost:
The code is as follows:
DI - using the IHttpClientFactory (here):
public static class DataSourceServiceRegistration
{
public static IServiceCollection RegisterDataSourceServices(this IServiceCollection serviceCollection)
{
serviceCollection.AddHttpClient();
return serviceCollection;
}
}
HttpClient usage:
private readonly HttpClient _httpClient;
public EsriHttpClientAdapter(HttpClient httpClient)
{
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
}
public async Task<JsonDocument> SendPrintServiceMessage(string url, HttpMethod httpMethod, string referer, IEnumerable<KeyValuePair<string, string>> content = null)
{
var watch = System.Diagnostics.Stopwatch.StartNew();
HttpContent httpContent = null;
if (content != null)
{
httpContent = new FormUrlEncodedContent(content);
}
var msg = new HttpRequestMessage(httpMethod, url) { Content = httpContent };
_httpClient.DefaultRequestHeaders.Referrer = new Uri(referer);
_httpClient.DefaultRequestHeaders.Add("some", "config");
_logger.LogInformation($"Before SendAsync - time {watch.ElapsedMilliseconds}");
var result = await _httpClient.SendAsync(msg);
_logger.LogInformation($"After SendAsync - time {watch.ElapsedMilliseconds}");
var response = await result.Content.ReadAsStringAsync();
_logger.LogInformation($"After ReadAsStringAsync - time {watch.ElapsedMilliseconds}");
if (result.StatusCode == HttpStatusCode.OK)
{
//do some stuff here
}
}
Application Insights is as follows:
AZURE:
Localhost:
Not sure if this is applicable to you, but hopefully it helps. If you're running under a Basic (Consumption) plan, your function will always be cold and need to spin up when being invoked by Http trigger. To circumvent this, you can set the function to Always On (if this is within your budget and scope) if on App Service Environment, Dedicated, or Premium plans. (In other words, Free Functions will always run cold.)
You can change this under Configuration > General Settings > Always On.
There's good info on how a Function runs through a cold startup at:
https://azure.microsoft.com/en-us/blog/understanding-serverless-cold-start/
I have an Azure durable function triggered by a message that then uses the client to start an Orchestration trigger which starts several activity functions. I have set breakpoints in Orchestration client , trigger and each activity function.
But, it only hits the breakpoints in Orchestration client function and the others are getting ignored. But underneath it seems to execute the activity functions although the breakpoints are not hit.
Investigative information
Programming language used = C#
Visual Studio Enterprise 2019 version = 16.8.3
Azure Functions Core Tools
Core Tools Version: 3.0.3216
Function Runtime Version: 3.0.15193.0
Below here, I have included a code snippet. (have not added every activity function)
[FunctionName(nameof(InitiateExport))]
public static async Task InitiateExport(
[ServiceBusTrigger("%ExportQueueName%", Connection = "AzureSBConnection")]Message message,
[DurableClient(TaskHub = "%FunctionHubName%")] IDurableOrchestrationClient orchestrationClient,
[Inject]IServiceProvider rootServiceProvider, ILogger log)
{
var DataQueuedDetails = JsonConvert.DeserializeObject<DataQueuedDetails>(Encoding.UTF8.GetString(message.Body));
using (var scope = rootServiceProvider.CreateScope())
{
log.LogInformation($"{nameof(ExportData)} function execution started at: {DateTime.Now}");
var services = scope.ServiceProvider;
services.ResolveRequestContext(message);
var requestContext = services.GetRequiredService<RequestContext>();
await orchestrationClient.StartNewAsync(nameof(TriggerDataExport), null, (DataQueuedDetails, requestContext));
log.LogInformation($"{nameof(ExportData)} timer triggered function execution finished at: {DateTime.Now}");
}
}
[FunctionName(nameof(TriggerDataExport))]
public static async Task TriggerDataExport(
[OrchestrationTrigger] IDurableOrchestrationContext orchestrationContext,
[Inject] IServiceProvider rootServiceProvider, ILogger log)
{
using (var scope = rootServiceProvider.CreateScope())
{
var services = scope.ServiceProvider;
var (DataOperationInfo, requestContext) = orchestrationContext.GetInput<(DataQueuedDetails, RequestContext)>();
if (!orchestrationContext.IsReplaying)
log.LogInformation($"Starting Export data Id {DataOperationInfo.Id}");
var blobServiceFactory = services.GetRequiredService<IBlobServiceFactory>();
requestContext.CustomerId = DataOperationInfo.RelatedCustomerId;
try
{
await orchestrationContext.CallActivityAsync(
nameof(UpdateJobStatus),
(DataOperationInfo.Id, DataOperationStatus.Running, string.Empty, string.Empty, requestContext));
// some other activity functions
---
---
} catch (Exception e)
{
await orchestrationContext.CallActivityAsync(
nameof(UpdateJobStatus),
(DataOperationInfo.Id, DataOperationStatus.Failed, string.Empty, string.Empty, requestContext));
}
}
}
[FunctionName(nameof(UpdateJobStatus))]
public static async Task RunAsync(
[ActivityTrigger] IDurableActivityContext activityContext,
[Inject]IServiceProvider rootServiceProvider)
{
using (var scope = rootServiceProvider.CreateScope())
{
try
{
var (DataOperationId, status, blobReference, logFileBlobId, requestContext) = activityContext.GetInput<(string, string, string, string, RequestContext)>();
var services = scope.ServiceProvider;
services.ResolveRequestContext(requestContext.CustomerId, requestContext.UserId, requestContext.UserDisplayName, requestContext.Culture);
var dataService = services.GetRequiredService<IDataService>();
var DataOperationDto = new DataOperationDto
{
Id = DataOperationId,
OperationStatusCode = status,
BlobReference = blobReference,
LogBlobReference = logFileBlobId
};
await dataService.UpdateAsync(DataOperationDto);
}
catch (Exception e)
{
throw e;
}
}
}
While you are debugging your Function, you should make sure that you are either using the local storage emulator, or an Azure Storage Account that is different from the one that is also being used by the Function already deployed in Azure.
If you are using the same storage account that another running Function is using, then it could be that parts of the execution is actually happening in Azure instead of your dev machine.
If I set connection string of AzureWebJobsServiceBus in local.settings.json, then there is no error. However, I would like to use Azure Key-vault to prevent disclosing connection string.
Below is my error:
Microsoft.Azure.WebJobs.Host: Error indexing method 'MyAzureFunc.Run'. Microsoft.ServiceBus: The Service Bus connection string is not of the expected format. Either there are unexpected properties within the string or the format is incorrect. Please check the string before. trying again.
Here is my code:
public static class MyAzureFunc
{
private static readonly SettingsContext _settings;
static MyAzureFunc()
{
_settings = new SettingsContext(new Settings
{
BaseUrl = Environment.GetEnvironmentVariable("BaseUrl"),
ServiceBusConnectionString = Environment.GetEnvironmentVariable("ServiceBus"),
certThumbprint = Environment.GetEnvironmentVariable("CertThumbprint"),
keyVaultClientId = Environment.GetEnvironmentVariable("KeyVaultClientId"),
ServiceBusSecretUrl = Environment.GetEnvironmentVariable("ServiceBusSecretUrl")
});
Environment.SetEnvironmentVariable("AzureWebJobsServiceBus", _settings.ServiceBusConnectionString);
}
[FunctionName("Func")]
public static async Task Run([ServiceBusTrigger(ServiceBusContext.MyQueueName)] BrokeredMessage msg, TraceWriter log)
{
......
}
}
public SettingsContext(Settings settings)
{
new MapperConfiguration(cfg => cfg.CreateMap<Settings, SettingsContext>()).CreateMapper().Map(settings, this);
if (!string.IsNullOrEmpty(settings.certThumbprint) && !string.IsNullOrEmpty(settings.keyVaultClientId))
{
var cert = Helpers.GetCertificate(settings.certThumbprint);
var assertionCert = new ClientAssertionCertificate(settings.keyVaultClientId, cert);
KeyVaultClient = GetKeyVaultClient(assertionCert);
if (ServiceBusConnectionString == "nil" && !string.IsNullOrEmpty(settings.ServiceBusSecretUrl))
{
ServiceBusConnectionString = KeyVaultClient.GetSecretAsync(settings.ServiceBusSecretUrl).Result.Value;
}
}
}
private static KeyVaultClient GetKeyVaultClient(ClientAssertionCertificate assertionCert)
{
return new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(async (string authority, string resource, string scope) =>
{
var context = new AuthenticationContext(authority, TokenCache.DefaultShared);
var result = await context.AcquireTokenAsync(resource, assertionCert);
return result.AccessToken;
}));
}
This is actually much simpler than what you are trying ;) See here. KeyVault is natively integrated with Azure Functions / App Services for secure storage of settings.
In your local.settings.json you use the connection string as is (in plain text). This file is never checked in.
In Azure you have an app setting with the same name, but instead of putting the plain text connection string, you put the reference to your KeyVault setting like this:
#Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/mysecret/ec96f02080254f109c51a1f14cdb1931)
The only thing you need to do is to enable Managed Identity of your Function and give that identity read permissions in the KeyVault.
I have a following test code that is placed inside a WebJob project. It is triggered after any blob is created (or changed) inside "cBinary/test1/" storage account.
The code works.
public class Triggers
{
public void OnBlobCreated(
[BlobTrigger("cBinary/test1/{name}")] Stream blob,
[Blob("cData/test3/{name}.txt")] out string output)
{
output = DateTime.Now.ToString();
}
}
The question is: how to get rid of ugly hard-coded const string "cBinary/test1/" and ""cData/test3/"?
Hard-coding is one problem, but I need to create and maintain couple of such strings (blob directories) that are created dynamically - depend of supported types. What's more - I need this string value in couple of places, I don't want to duplicate it.
I would like them to be placed in some kind of configuration provider that builds the blob path string depending on some enum, for instance.
How to do it?
You can implement INameResolver to resolve QueueNames and BlobNames dynamically. You can add the logic to resolve the name there. Below is some sample code.
public class BlobNameResolver : INameResolver
{
public string Resolve(string name)
{
if (name == "blobNameKey")
{
//Do whatever you want to do to get the dynamic name
return "the name of the blob container";
}
}
}
And then you need to hook it up in Program.cs
class Program
{
// Please set the following connection strings in app.config for this WebJob to run:
// AzureWebJobsDashboard and AzureWebJobsStorage
static void Main()
{
//Configure JobHost
var storageConnectionString = "your connection string";
//Hook up the NameResolver
var config = new JobHostConfiguration(storageConnectionString) { NameResolver = new BlobNameResolver() };
config.Queues.BatchSize = 32;
//Pass configuration to JobJost
var host = new JobHost(config);
// The following code ensures that the WebJob will be running continuously
host.RunAndBlock();
}
}
Finally in Functions.cs
public class Functions
{
public async Task ProcessBlob([BlobTrigger("%blobNameKey%")] Stream blob)
{
//Do work here
}
}
There's some more information here.
Hope this helps.