@Matthias, Thank you for your question.
As the architectural diagram shows, an Azure Functions app is used between both services to translate the payload received from the ACR webhook. It’s also the custom function, which will call into Microsoft Teams to create the custom message.

Configure incoming Webhook in Microsoft Teams
To send messages into Microsoft Teams, an Incoming Webhook is required. To create and configure a new Incoming Webhook, navigate to the desired team and select CONNECTORS from the Options Menu.

Look for Incoming Webhook, and ADD it. Once added, the Incoming Webhook has to be configured. Using the official ACR logo for demonstration purposes and set the name to “Azure Container Registry”.

Add Webhook URL to the Azure Functions app
Having the Incoming Webhook connector configured in Microsoft Teams, you must store the webhook URL on the Azure Functions app. Again, use Azure CLI to get this done:
# Variables
FUNCTION_APP_NAME=fnapp-acr-teams
RESOURCE_GROUP_NAME=rg-acr-teams
WEBHOOK_URL=<your webhook url provided by Microsoft Teams>
# Set appSettings on Azure Functions App
az functionapp config appsettings set \
-n $FUNCTION_APP_NAME \
-g $RESOURCE_GROUP_NAME \
--settings "ContainerRegistryTeams__TeamsWebhookUrl=$WEBHOOK_URL"
Implement Azure Functions
We have used Azure Functions Core Tools (func
) to get everything done when it comes to Azure Functions. If you are more on the UI side of things, use your preferred IDE or editor to create a new Azure Functions project.
# Create a project dir
mkdir -p ~/projects/acr-teams-integration
cd ~/projects/acr-teams-integration
# Create an Azure Functions project
func init --worker-runtime dotnet
# Create a new function
func new -l csharp --template "Http Trigger" \
-n FnNotifyTeams -a function
To use dependency injection (DI) and configuration capabilities from .NET in an Azure Functions project, you have to install the following NuGet packages:
dotnet add package Microsoft.NET.Sdk.Functions
dotnet add package Microsoft.Azure.Functions.Extensions
dotnet add package Microsoft.Extensions.DependencyInjection
Add a custom FunctionsStartup class
If you want to use IOptions<T>
to access configuration data using strongly type C# classes, consider adding a custom implementation of FunctionsStartup
as shown here:
[assembly: FunctionsStartup(typeof(Thns.Functions.Startup))]
namespace Thns.Functions
{
public class Startup: FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddOptions<TeamsConfig>()
.Configure<IConfiguration>((settings, configuration) =>
{
configuration.GetSection("ContainerRegistryTeams")
.Bind(settings);
});
}
}
}
The configuration for our function is pretty simple. It’s just the webhook URL. That said, the TeamsConfig
class is simple:
public class TeamsConfig
{
public string TeamsWebhookUrl { get; set; }
}
You can now use regular DI patterns to get the configuration data during a request:
public class FnNotifyTeams
{
public FnNotifyTeams(IOptions<TeamsConfig> options)
{
Config = options.Value;
}
protected TeamsConfig Config { get; }
// omitted
}
The FnNotifyTeams function
Let’s implement our Azure function based on an HTTPTrigger now. The function is responsible for:
Validating incoming requests from ACR
Constructing the payload for the Microsoft Teams message
Calling into the Microsoft Teams webhook
[FunctionName("FnNotifyTeams")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "POST", Route = "images")] HttpRequest req, ILogger log)
{
string body = await new StreamReader(req.Body).ReadToEndAsync();
dynamic d = JsonConvert.DeserializeObject(body);
string action = d?.action;
if (action != "push" && action != "chart_push")
{
log.LogWarning($"Action {action} is not supported.");
return new BadRequestResult();
}
var metadata = ImageMetadata.FromPayload(d);
if(metadata == null)
{
log.LogWarning($"Received invalid request. Got {body}");
return new BadRequestResult();
}
var message = new {
Title = $"New Container Image published in ACR",
Text = $"`{metadata.Repository}:{metadata.Tag}` has been published at `{metadata.Registry}`. You can pull it now using: {Environment.NewLine}`docker pull {metadata.Registry}/{metadata.Repository}:{metadata.Tag}{Environment.NewLine}`"
};
var h = new HttpClient();
var content = new StringContent(JsonConvert.SerializeObject(message), Encoding.UTF8, "application/json");
var r = await h.PostAsync(Config.TeamsWebhookUrl, content);
if (r.IsSuccessStatusCode){
return new OkResult();
}
log.LogError($"Teams response -> {r.StatusCode}: {r.ReasonPhrase}");
return new StatusCodeResult(500);
}
The preceding code takes the raw payload received from ACR, extracts and validates essential metadata. Suppose all necessary information is available and ACR issued the call because of a push
or chart_push
action. In that case, the request payload for calling into Microsoft Teams is constructed and send using the HttpClient::PostAsync
method.
Encapsulated inbound message validation mainly into the ImageMetadata
class, as you can see here:
public class ImageMetadata
{
public string Registry { get; private set; }
public string Repository { get; private set; }
public string Tag { get; private set; }
public static ImageMetadata FromPayload(dynamic d)
{
string repository = d?.target?.repository;
string tag = d?.target?.tag;
string registry = d?.request?.host;
if (string.IsNullOrWhiteSpace(repository) ||
string.IsNullOrWhiteSpace(tag) ||
string.IsNullOrWhiteSpace(registry))
{
return null;
}
return new ImageMetadata
{
Registry = registry,
Repository = repository,
Tag = tag,
};
}
}
Publish it to the cloud now:
# Variables
FUNCTION_APP_NAME=fnapp-acr-teams
# go to the functions directory
cd projects/acr-teams-integration
# deploy the current code to Azure Functions app
func azure functionapp publish $FUNCTION_APP_NAME -b local
Grab the Azure Function URL with the authentication key
Now that the code is deployed to the Azure Functions app, you have to grab the function’s URL, including the function key used for authentication. Again you can do that straight from the command line using the func CLI:
# Variables
FUNCTION_APP_NAME=fnapp-acr-teams
# Grab function url with function key
func azure functionapp list-functions $FUNCTION_APP_NAME --show-keys
Copy the invoke URL from the output.
Add a Webhook to ACR
Last but not least, we have to create a new outgoing webhook in ACR. At this point, we’re interested in two events exposed from ACR via webhook push
and chart_push
:
# Variables
FUNCTION_URL=<paste function app url including function-key>
ACR_NAME=acrteams2021
# create a webhook in ACR
az acr webhook create -n webhook-notify-teams \
-r $ACR_NAME \
--uri $FUNCTION_URL \
--actions push chart_push
Test the integration of Azure Container Registry and Microsoft Teams
You can use any Docker image or OCI compliant artifact to test the integration. We’ll use an alpine Linux container image as an example here:
# Variables
ACR_NAME=acrteams2021
docker pull alpine:latest
docker tag alpine:latest $ACR_NAME.azurecr.io/alpine:0.0.1
# Login to ACR
az acr login -n $ACR_NAME
# Push the container image to ACR
docker push $ACR_NAME.azurecr.io/alpine:0.0.1
And after a few seconds, you should see a message appearing in Microsoft Teams.

Reference: https://www.thorsten-hans.com/send-microsoft-teams-message-oci-artifacts-azure-container-registry/