Using Managed Identity with Azure AD Workload Identity on AKS for Java developers

Jay Lee
5 min readOct 3, 2022

Azure AD workload identity for AKS was announced as a public preview on Oct 1st, 2022, and it finally adds long waited managed identity support. As I promised in my previous article — Azure Workload Identity Preview on AKS with Spring Boot, I’m writing a follow-up to cover the managed identity with Azure AD Workload Identity. Taking one step further, I will demonstrate the application migration from Pod Identity to AAD Workload Identity, which will benefit developers who eventually need to migrate to the latter.

Enable AAD workload identity on AKS

As a public preview feature, AAD workload identity can now be easily enabled as an add-on. Let’s get started by registering the preview feature as below.

$ az extension update --name aks-preview
...
$ az feature register --namespace "Microsoft.ContainerService" --name "EnableWorkloadIdentityPreview"
...
$ az feature list -o table --query "[?contains(name, 'Microsoft.ContainerService/EnableWorkloadIdentityPreview')].{Name:name,State:properties.state}"
...
$ az provider register -n Microsoft.ContainerService
...

Once the feature is registered, you can enable AAD workload identity with a az CLI.

$ az aks update -g RESOURCE_GROUP -n CLUSTER_NAME --enable-oidc-issu er --enable-workload-identity

NOTE: I’ve enabled both Pod Identity and AAD Workload Identity simultaneously on my AKS cluster.

Before the public preview, there was an azwi CLI helps to create a service account and AAD Federated credentials with one command. In the public preview, we need to do it one by one. First, we will create a service account on Kubernetes.

$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
azure.workload.identity/client-id: CLIENT_ID_OF_MANAGED_IDENTITY
labels:
azure.workload.identity/use: "true"
name: jay-mi-serviceaccount
namespace: default
EOF

Then create a federated identity using az CLI. Look at bold values that you will need to customize it. issuer URL created by AKS can be retrieved using simple commands.

$ az aks show -g RESOURCE_GROUP -n AKS_CLUSTER_NAME --query oidcIssuerProfile
{
"enabled": true,
"issuerUrl": "https://oidc.prod-aks.azure.com/***/"
}
$ az identity federated-credential create --name aks-jay-mi \
--identity-name jay-managedidentity \
--resource-group RESOURCE_GROUP \
--issuer https://oidc.prod-aks.azure.com/***/ \
--subject system:serviceaccount:default:jay-mi-serviceaccount

Managed identity now has a page showing federated credentials on the Azure portal.

Federated credentials of managed identity

Java Application Migration from Pod Identity to AAD Workload Identity

The basic setup is done, so let’s see how app migration could be done. I have written an article before — Secure way to use Secrets with Java using Azure Key Vault and Managed Identity that goes through the basic setup and the sample app using Pod Identity. I will take it as a baseline for migration. If you haven’t read it, I recommend doing so before proceeding to the rest. The sample app is here on GitHub.

There are two ways for migration at this moment. The first way is to change your code to use a new version of Azure Identity. In private preview, we needed to implement FederatedCredential , which we don’t need to do it anymore as it’s handled by a newer version of Azure Identity SDK. The second way is to use sidecar, which would be a more favorable approach if you can’t change the source code immediately.

The first approach — source code change

It doesn’t require any significant changes in the code at all, but the version of dependencies. Full federated credentials support has been added since version 1.5.0, which gets rid of the custom FederatedCredential, so manually overriding the version will work.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.microsoft.gbb</groupId>
<artifactId>keyvault-mi</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>keyvault-mi</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
<spring-cloud-azure.version>4.0.0</spring-cloud-azure.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter</artifactId>
</dependency>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-starter-keyvault-secrets</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.6.0</version>
</dependency>

<dependency>
<groupId>com.microsoft.azure</groupId>
<artifactId>msal4j</artifactId>
<version>1.13.1</version>
</dependency>


</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.azure.spring</groupId>
<artifactId>spring-cloud-azure-dependencies</artifactId>
<version>${spring-cloud-azure.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

This is it for the code change. The next thing to do is switch from Pod Identity to AAD Workload Identity, which is straightforward, as below.

apiVersion: apps/v1
kind: Deployment
metadata:
name: keyvault-mi
labels:
app: keyvault-mi
spec:
replicas: 1
template:
metadata:
name: keyvault-mi
labels:
# aadpodidbinding: jay-managedidentity
app: keyvault-mi
spec:
serviceAccountName: jay-mi-serviceaccount
containers:
- name: keyvault-mi
image: eggboy/keyvault-mi:0.0.11
imagePullPolicy: Always
env:
- name: MANAGED_IDENTITY_CLIENT_ID
value: ***
- name: KEYVAULT_URI
value: ***
restartPolicy: Always
selector:
matchLabels:
app: keyvault-mi

NOTE: At the time of writing this article, any new project generated by Spring initilzr is by default using Azure Identity 1.5.2, so you don’t need to override the version manually.

The second approach — zero code change with sidecar

It’s literally zero code change required as AAD workload identity injects sidecars, and all you need to do is to add annotations for sidecar injection.

apiVersion: apps/v1
kind: Deployment
metadata:
name: keyvault-mi
labels:
app: keyvault-mi
spec:
replicas: 1
template:
metadata:
name: keyvault-mi
labels:
# aadpodidbinding: jay-managedidentity
app: keyvault-mi
annotations:
azure.workload.identity/inject-proxy-sidecar: "true
spec:
serviceAccountName: jay-mi-serviceaccount
containers:
- name: keyvault-mi
image: eggboy/keyvault-mi:0.0.4
imagePullPolicy: Always
env:
- name: MANAGED_IDENTITY_CLIENT_ID
value: ***
- name: KEYVAULT_URI
value: ***
restartPolicy: Always
selector:
matchLabels:
app: keyvault-mi

Let’s see the injected sidecars. kubectl describe shows the injected azwi-proxy.

$ kubectl describe pod keyvault-mi-5df77fb9b5-vxb6q
...
Containers:
keyvault-mi:
Container ID: containerd://2396e5713012bed97fe289650a8696b6c9e69061106988fa897e2f702c844281
Image: eggboy/keyvault-mi:0.0.4
Image ID: docker.io/eggboy/keyvault-mi@sha256:6da320c9e173949eef3cc359ab0900b10cc393f16e2a7c7937cc254130937ed9
Port: <none>
Host Port: <none>
State: Running
Started: Mon, 03 Oct 2022 09:55:19 +0800
Ready: True
Restart Count: 0
Environment:
...
Mounts:
/var/run/secrets/azure/tokens from azure-identity-token (ro)
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-krmn2 (ro)
azwi-proxy:
Container ID: containerd://58cfb674f9405dcbb3af89b29a2c7f779079482807e10905dc0c4b098e9ab798
Image: mcr.microsoft.com/oss/azure/workload-identity/proxy:v0.13.0
Image ID: mcr.microsoft.com/oss/azure/workload-identity/proxy@sha256:eda12d92d1f5a19b02bd893e738b0822c5b8e3f753081fcdc442a26aafa9230c
Port: 8000/TCP
Host Port: 0/TCP
Args:
--proxy-port=8000
State: Running
Started: Mon, 03 Oct 2022 09:55:24 +0800
Ready: True
Restart Count: 0
Environment:
...
Mounts:
/var/run/secrets/azure/tokens from azure-identity-token (ro)
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-krmn2 (ro)
...

One thing to note is that sidecar is using a proxy port as “8000” as a default so be careful about the potential port conflicts.

Wrapping Up

This is a big step forward for Java developers using AKS as Pod Identity has been only a preview feature for the last few years which makes developers anxious about using it in production. I do see some minor issues in the preview version, but I’m super happy to see it works fine in general. Do remember that the sidecar is not meant for the long-term solution, so be prepared to upgrade your application in the future.

If you like my article, please leave some claps here or maybe even start following me. You can hit me up on Linkedin. Thanks!

--

--

Jay Lee

Cloud Native Enthusiast. Java, Spring, Python, Golang, Kubernetes.