Why are release annotations automatically created in my release pipelines? - azure

I noticed that release annotations are now added for a couple of our Application Insights instances when we execute our release pipelines. We have not done any of the things mentioned in the article. Where are those annotations coming from?
It seems to happen only on our application release pipelines, not infrastructure. Most of our applications run on App Service, but not all have release annotations.

The Azure App Service Deploy Task automatically creates release annotations.
The only prerequisite is that the App Service has a valid APPINSIGHTS_INSTRUMENTATIONKEY in its AppSettings.
This is the relevant code block:
https://github.com/microsoft/azure-pipelines-tasks/blob/master/Tasks/Common/AzureRmDeploy-common/operations/ReleaseAnnotationUtility.ts
export async function addReleaseAnnotation(endpoint: AzureEndpoint, azureAppService: AzureAppService, isDeploymentSuccess: boolean): Promise<void> {
try {
var appSettings = await azureAppService.getApplicationSettings();
var instrumentationKey = appSettings && appSettings.properties && appSettings.properties.APPINSIGHTS_INSTRUMENTATIONKEY;
if(instrumentationKey) {
let appinsightsResources: ApplicationInsightsResources = new ApplicationInsightsResources(endpoint);
var appInsightsResources = await appinsightsResources.list(null, [`$filter=InstrumentationKey eq '${instrumentationKey}'`]);
if(appInsightsResources.length > 0) {
var appInsights: AzureApplicationInsights = new AzureApplicationInsights(endpoint, appInsightsResources[0].id.split('/')[4], appInsightsResources[0].name);
var releaseAnnotationData = getReleaseAnnotation(isDeploymentSuccess);
await appInsights.addReleaseAnnotation(releaseAnnotationData);
console.log(tl.loc("SuccessfullyAddedReleaseAnnotation", appInsightsResources[0].name));
}

Related

Azure Function v3 - "Azure Functions runtime is unreachable" when add Identity in Startup

I have an Azure Function (v3) that must interact with DB and also with user management.
It has as a dependency a project that also contains the DAL so all the context configuration.
When in the Startup of the function I add the dependency to the DbContext and deploy it on Azure I have no problems.
When in addition to the DbContext I also add Identity and re-deploy, the Portal says "Azure Functions runtime is unreachable".
This is the function Startup.cs:
[assembly: FunctionsStartup(typeof(Directio.PeopleSee.OrderSynchronizer.Startup))]
namespace Directio.PeopleSee.OrderSynchronizer
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddHttpClient();
builder.Services.AddDbContext<DBContext>(options1 =>
{
options1.UseLazyLoadingProxies(false);
options1.UseSqlServer(Environment.GetEnvironmentVariable("DBConnectionString"), builder =>
{
builder.CommandTimeout(10);
}
);
})
.AddIdentity<AspNetUser, AspNetRole>(opt =>
{
opt.Password.RequireDigit = false;
opt.Password.RequireLowercase = false;
opt.Password.RequireNonAlphanumeric = false;
opt.Password.RequireUppercase = false;
opt.Password.RequiredLength = 0;
opt.Password.RequiredUniqueChars = 0;
opt.User.RequireUniqueEmail = false;
opt.SignIn.RequireConfirmedEmail = false;
opt.SignIn.RequireConfirmedAccount = false;
opt.SignIn.RequireConfirmedPhoneNumber = false;
})
.AddEntityFrameworkStores<DBContext>()
.AddDefaultTokenProviders();
builder.Services.AddOptions<FunctionAppSettings>().Configure<IConfiguration>((settings, configuration) => configuration.GetSection("FunctionAppSettings").Bind(settings));
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IUserService, UserServiceImpl>();
builder.Services.AddScoped<IRoleService, RoleServiceImpl>();
builder.Services.AddScoped<ISubscribersService, SubscriberServiceImpl>();
builder.Services.AddScoped<IOrdersService, OrdersService>();
}
}
}
All services registrations come from the project the dependency is linked to.
The function is under netcore3.1 as a framework, and is deployed in an Azure Func App with a pay-as-you-go plan, Windows server and it's not a docker container.
What could be the problem?
When you add Identity you are also adding the AspNetCore Authentication dependencies. For some reason Functions host checks if these dependencies exist in your applications and stops the Host module from adding the built-in authentication schemes that allows the portal to have access to the functions in your project.
If you still want to load Identity components at startup time I encourage you to look at https://www.nuget.org/packages/DarkLoop.Azure.Functions.Authorize
Here is a post describing how it works: https://blog.darkloop.com/post/functionauthorize-for-azure-functions-v3
you can call
builder.AddAuthentication();
builder.AddAuthorization();
// or
builder.Services.AddFunctionsAuthentication();
builder.Services.AddFunctionsAuthorization();
This call forces the functions built-in authentication to be loaded and allows you to add more authentication dependencies and schemes.
After this call you can make your calls AddDbContext<... and AddIdentity<...
Azure portal should be able to get all the information it needs about your functions.

Azure AppConfiguration (using DefaultAzureCredential on local) returns no keys

When I inject IConfiguration in a function, it does not find any keys that only live in my "Azure App Configuration".
I have a functionApp (V3) that accesses App Configuration using the DefaultAzureCredential. I am running this locally in debug hence the need for a default credential. I also have multiple Tenants so I had to set the VisualStudioTenantId and SharedTokenCacheTenantId on DefaultAzureCredentialOptions. My Visual studio user was also given the role "App Configuration Data Reader" to be able to debug.
When connecting to App configuration I get no errors.
Editedto add: I have setup AppConfiguration to authenticate with AzureAD.
See code below:
public override async void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
var credOptions = new DefaultAzureCredentialOptions();
var tenantId = Environment.GetEnvironmentVariable("Tenant_Id");
credOptions.VisualStudioTenantId = tenantId;
credOptions.SharedTokenCacheTenantId = tenantId;
var cred = new DefaultAzureCredential(credOptions);
/*Works but requires SharedTokenCacheTenantId*/
var secretClient = new SecretClient(new Uri(vaultURI), cred);
var secret = await secretClient.GetSecretAsync("<secret name>");
/*Works but where are my keys when I try to access them?*/
builder.ConfigurationBuilder.AddAzureAppConfiguration(options =>
{
options.Connect(new Uri(appConfigURI), cred);
}).Build(); //Should I be building this??
}
In my function
public FunctName(IConfiguration configuration)
{
_configuration = configuration;
}
And when I access the property
var prop = _configuration["PropertyName"];
There is an example function app that uses IFunctionsConfigurationBuilder here https://github.com/Azure/AppConfiguration/blob/main/examples/DotNetCore/AzureFunction/FunctionApp/Startup.cs . I would recommend taking a look and seeing if there are any missing pieces.
The title mentions "using DefaultAzureCredential on local". Does that mean that this works as expected if you use a connection string?
Notice the async void ConfigureAppConfiguration. This caused my ConfigureAppConfiguration to not execute synchronously, causing configure to add my App Configuration before it was populated.

Azure Storage Queue Trigger - Use remote queue name

I am using Azure App Configuration Store to store configuration. I am using the following code in startup.cs to load my config from Azure.
var builder = new ConfigurationBuilder();
builder.AddAzureAppConfiguration(options =>
{
options.Connect(this.Values.AppConfigConnectionString);
options.Select(keyFilter: KeyFilter.Any, labelFilter: this.Values.Env);
});
var config = builder.Build();
Now this config variable contains my queue names. I need this dynamic so to create and handle it in 4 different environments. Dev / Stage / QA / Prod.
public async Task Run(
[QueueTrigger("%QueueName%", Connection = "StorageConnection")]VoiceHubEvent item)
This isn't working as my local.settings.json file doesn't contain QueueName entry.
Is it possible to make use of config variable in Run() to resolve queuename? By reloading queue trigger function or something?
Thanks,
Kiran.
Is it possible to make use of config variable in Run() to resolve queuename? By reloading queue trigger function or something?
Yes, you can.
Create an extensions method for the IWebJobsBuilder interface to set up a connection to AzureAppConfiguration.
public static IWebJobsBuilder AddAzureConfiguration(this IWebJobsBuilder webJobsBuilder)
{
//-- Get current configuration
var configBuilder = new ConfigurationBuilder();
var descriptor = webJobsBuilder.Services.FirstOrDefault(d => d.ServiceType == typeof(IConfiguration));
if (descriptor?.ImplementationInstance is IConfigurationRoot configuration)
configBuilder.AddConfiguration(configuration);
var config = configBuilder.Build();
//-- Add Azure Configuration
configBuilder.AddAzureAppConfiguration(options =>
{
var azureConnectionString = config[TRS.Shared.Constants.CONFIGURATION.KEY_AZURECONFIGURATION_CONNECTIONSTRING];
if (string.IsNullOrWhiteSpace(azureConnectionString)
|| !azureConnectionString.StartsWith("Endpoint=https://"))
throw new InvalidOperationException($"Missing/wrong configuration value for key '{TRS.Shared.Constants.CONFIGURATION.KEY_AZURECONFIGURATION_CONNECTIONSTRING}'.");
options.Connect(azureConnectionString);
});
//build the config again so it has the key vault provider
config = configBuilder.Build();
return webJobsBuilder;
}
Where the azureConnectionString is read from you appsetting.json and should contain the url to the Azure App Configuration.
In startup.cs:
public void Configure(IWebJobsBuilder builder)
{
builder.AddAzureConfiguration();
ConfigureServices(builder.Services)
.BuildServiceProvider(true);
}
For more details, you could refer to this SO thread.

Using Azure Managed Service Identities to scale App Service Plan, but none listed

I am attempting to set up an Azure Function that will scale our App Service Plan to be larger during business hours, and smaller outside of them. I drew from the answer to this question.
public static class ScaleUpWeekdayMornings
{
[FunctionName(nameof(ScaleUpWeekdayMornings))]
public static async Task Run([TimerTrigger("0 0 13 * * 1-5")]TimerInfo myTimer, ILogger log) // Uses GMT timezone
{
var resourceGroup = "DesignServiceResourceGroup";
var appServicePlanName = "DesignService";
var webSiteManagementClient = await Utilities.GetWebSiteManagementClient();
var appServicePlanRequest = await webSiteManagementClient.AppServicePlans.ListByResourceGroupWithHttpMessagesAsync(resourceGroup);
appServicePlanRequest.Body.ToList().ForEach(x => log.LogInformation($">>>{x.Name}"));
var appServicePlan = appServicePlanRequest.Body.Where(x => x.Name.Equals(appServicePlanName)).FirstOrDefault();
if (appServicePlan == null)
{
log.LogError("Could not find app service plan.");
return;
}
appServicePlan.Sku.Family = "P";
appServicePlan.Sku.Name = "P2V2";
appServicePlan.Sku.Size = "P2V2";
appServicePlan.Sku.Tier = "Premium";
appServicePlan.Sku.Capacity = 1;
var updateResult = await webSiteManagementClient.AppServicePlans.CreateOrUpdateWithHttpMessagesAsync(resourceGroup, appServicePlanName, appServicePlan);
}
}
public static class Utilities
{
public static async Task<WebSiteManagementClient> GetWebSiteManagementClient()
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var accessToken = await azureServiceTokenProvider.GetAccessTokenAsync("https://management.azure.com/");
var tokenCredentials = new TokenCredentials(accessToken);
return new WebSiteManagementClient(tokenCredentials)
{
SubscriptionId = "<realSubscriptionIdHere>",
};
}
}
When I run this locally, it works, and actually performs the scale up on our Azure App Service Plan (I believe via Azure CLI). However, when this Azure Function is deployed to Azure, it does not find any App Service Plans. It hits the appservicePlan == null block and returns. I have turned on the Managed Service Identity for the deployed Azure Function, and granted it contributor permissions in the App Service I want to scale for.
Am I missing something? Why does
await webSiteManagementClient.AppServicePlans.ListByResourceGroupWithHttpMessagesAsync(resourceGroup);
not return anything when run as part of a published Azure Function?
The problem was the permission was set on the App Service (which was an app on the App Service Plan). It needed to be set on the Resource Group to be able to read the App Service Plans.

Staging or Production Instance?

Is there anywhere in the service runtime that would tell me if I'm currently running on 'Staging' or 'Production'? Manually modifying the config to and from production seems a bit cumbersome.
You should really not change your configurations when you're based upon if you're in Prod or Staging. Staging area is not designed to be a "QA" environment but only a holding-area before production is deployed.
When you upload a new deployment, current deployment slot where you upload your package to is destroyed and is down for 10-15minutes while upload and start of VM's is happening. If you upload straight into production, that's 15 minutes of production downtime. Thus, Staging area was invented: you upload to staging, test the stuff, and click "Swap" button and your Staging environment magically becomes Production (virtual IP swap).
Thus, your staging should really be 100% the same as your production.
What I think you're looking for is QA/testing environment? You should open up a new service for Testing environment with its own Prod/Staging. In this case, you will want to maintain multiple configuration file sets, one set per deployment environment (Production, Testing, etc.)
There are many ways to manage configuration-hell that occurs, especially with Azure that has on top of .config files, its own *.cscfg files. The way I prefer to do it with Azure project is as follows:
Setup a small Config project, create folders there that match Deployment types. Inside each folder setup sets of *.config & *.cscfg files that match to particular deployment environment: Debug, Test, Release... these are setup in Visual Studio as well , as build target types. I have a small xcopy command that occurs during every compile of the Config project that copies all the files from Build Target folder of Config project into root folder of the Config project.
Then every other project in the solution, LINKS to the .config or .cscfg file from the root folder of the Config project.
Voila, my configs magically adapt to every build configuration automatically. I also use .config transformations to manage debugging information for Release vs. non-Release build targets.
If you've read all this and still want to get at the Production vs. Staging status at runtime, then:
Get deploymentId from RoleEnvironment.DeploymentId
Then use Management API with a proper X509 certificate to get at the Azure structure of your Service and call the GetDeployments method (it's rest api but there is an abstraction library).
Hope this helps
Edit: blog post as requested about the setup of configuration strings and switching between environments # http://blog.paraleap.com/blog/post/Managing-environments-in-a-distributed-Azure-or-other-cloud-based-NET-solution
Sometimes I wish people would just answer the question.. not explain ethics or best practices...
Microsoft has posted a code sample doing exactly this here: https://code.msdn.microsoft.com/windowsazure/CSAzureDeploymentSlot-1ce0e3b5
protected void Page_Load(object sender, EventArgs e)
{
// You basic information of the Deployment of Azure application.
string deploymentId = RoleEnvironment.DeploymentId;
string subscriptionID = "<Your subscription ID>";
string thrumbnail = "<Your certificate thumbnail print>";
string hostedServiceName = "<Your hosted service name>";
string productionString = string.Format(
"https://management.core.windows.net/{0}/services/hostedservices/{1}/deploymentslots/{2}",
subscriptionID, hostedServiceName, "Production");
Uri requestUri = new Uri(productionString);
// Add client certificate.
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.OpenExistingOnly);
X509Certificate2Collection collection = store.Certificates.Find(
X509FindType.FindByThumbprint, thrumbnail, false);
store.Close();
if (collection.Count != 0)
{
X509Certificate2 certificate = collection[0];
HttpWebRequest httpRequest = (HttpWebRequest)HttpWebRequest.Create(requestUri);
httpRequest.ClientCertificates.Add(certificate);
httpRequest.Headers.Add("x-ms-version", "2011-10-01");
httpRequest.KeepAlive = false;
HttpWebResponse httpResponse = httpRequest.GetResponse() as HttpWebResponse;
// Get response stream from Management API.
Stream stream = httpResponse.GetResponseStream();
string result = string.Empty;
using (StreamReader reader = new StreamReader(stream))
{
result = reader.ReadToEnd();
}
if (result == null || result.Trim() == string.Empty)
{
return;
}
XDocument document = XDocument.Parse(result);
string serverID = string.Empty;
var list = from item
in document.Descendants(XName.Get("PrivateID",
"http://schemas.microsoft.com/windowsazure"))
select item;
serverID = list.First().Value;
Response.Write("Check Production: ");
Response.Write("DeploymentID : " + deploymentId
+ " ServerID :" + serverID);
if (deploymentId.Equals(serverID))
lbStatus.Text = "Production";
else
{
// If the application not in Production slot, try to check Staging slot.
string stagingString = string.Format(
"https://management.core.windows.net/{0}/services/hostedservices/{1}/deploymentslots/{2}",
subscriptionID, hostedServiceName, "Staging");
Uri stagingUri = new Uri(stagingString);
httpRequest = (HttpWebRequest)HttpWebRequest.Create(stagingUri);
httpRequest.ClientCertificates.Add(certificate);
httpRequest.Headers.Add("x-ms-version", "2011-10-01");
httpRequest.KeepAlive = false;
httpResponse = httpRequest.GetResponse() as HttpWebResponse;
stream = httpResponse.GetResponseStream();
result = string.Empty;
using (StreamReader reader = new StreamReader(stream))
{
result = reader.ReadToEnd();
}
if (result == null || result.Trim() == string.Empty)
{
return;
}
document = XDocument.Parse(result);
serverID = string.Empty;
list = from item
in document.Descendants(XName.Get("PrivateID",
"http://schemas.microsoft.com/windowsazure"))
select item;
serverID = list.First().Value;
Response.Write(" Check Staging:");
Response.Write(" DeploymentID : " + deploymentId
+ " ServerID :" + serverID);
if (deploymentId.Equals(serverID))
{
lbStatus.Text = "Staging";
}
else
{
lbStatus.Text = "Do not find this id";
}
}
httpResponse.Close();
stream.Close();
}
}
Staging is a temporary deployment slot used mainly for no-downtime upgrades and ability to roll back an upgrade.
It is advised not to couple your system (either in code or in config) with such Azure specifics.
Since Windows Azure Management Libraries and thanks to #GuaravMantri answer to another question you can do it like this :
using System;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Azure;
using Microsoft.WindowsAzure.Management.Compute;
using Microsoft.WindowsAzure.Management.Compute.Models;
namespace Configuration
{
public class DeploymentSlotTypeHelper
{
static string subscriptionId = "<subscription-id>";
static string managementCertContents = "<Base64 Encoded Management Certificate String from Publish Setting File>";// copy-paste it
static string cloudServiceName = "<your cloud service name>"; // lowercase
static string ns = "http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration";
public DeploymentSlot GetSlotType()
{
var managementCertificate = new X509Certificate2(Convert.FromBase64String(managementCertContents));
var credentials = new CertificateCloudCredentials(subscriptionId, managementCertificate);
var computeManagementClient = new ComputeManagementClient(credentials);
var response = computeManagementClient.HostedServices.GetDetailed(cloudServiceName);
return response.Deployments.FirstOrDefault(d => d.DeploymentSlot == DeploymentSlot.Production) == null ? DeploymentSlot.Staging : DeploymentSlot.Production;
}
}
}
An easy way to solve this problem is setting at your instances an key to identify which environment it is running.
1) Set at your production slot:
Set it Settings >> Application settings >> App settings
And create a key named SLOT_NAME and value "production". IMPORTANT: check Slot setting.
2) Set at your staging slot:
Set it Settings >> Application settings >> App settings
And create a key named SLOT_NAME and value "staging". IMPORTANT: check Slot setting.
Access from your application the variable and identify which environment the application is running. In Java you can access:
String slotName = System.getenv("APPSETTING_SLOT_NAME");
Here are 4 points to consider
VIP swap only makes sense when your service faces the outside world. AKA, when it exposes an API and reacts to requests.
If all your service does is pull messages from a queue and process them, then your services is proactive and VIP swap is not a good solution for you.
If your service is both reactive and proactive, you may want to reconsider your design. Perhaps split the service into 2 different services.
Eric's suggestion of modifying the cscfg files pre- and post- VIP swap is good if the proactive part of your service can take a short down time (Because you first configure both Staging and Production to not pull messages, then perform VIP Swap, and then update Production's configuration to start pulling messages).

Resources