In this post, you'll learn how to run a Java Spring Boot application on Azure Kubernetes Service (AKS) and connects to Azure PostgreSQL using Azure AD Pod identity. Azure Kubernetes Service (AKS) is a managed Kubernetes service that lets you quickly deploy and manage applications based on microservices.
Azure Active Directory pod-managed identities uses Kubernetes primitives to associate managed identities for Azure resources and identities in Azure Active Directory (AAD) with pods.
The following diagram shows the architecture of the above steps:
I will assume you already have an Azure Subscription setup.
Before going any further, we will need to register EnablePodIdentityPreview and install
aks-preview
Azure CLI extension.az feature register --name EnablePodIdentityPreview --namespace Microsoft.ContainerService
az extension update --name aks-preview
Let’s create a resource group and an AKS cluster with Azure CNI and pod-managed identity enabled.
export RESOURCE_GROUP=demo-k8s-rg
export CLUSTER_NAME=my-k8s-cluster
az group create --name=${RESOURCE_GROUP} --location eastus
az aks create -g ${RESOURCE_GROUP} -n ${CLUSTER_NAME} --enable-managed-identity --enable-pod-identity --network-plugin azure --enable-addons monitoring --node-count 1 --generate-ssh-keys
Then create an identity,
export IDENTITY_RESOURCE_GROUP="my-identity-rg"
export IDENTITY_NAME="sp-application-identity"
az group create --name ${IDENTITY_RESOURCE_GROUP} --location eastus
az identity create --resource-group ${IDENTITY_RESOURCE_GROUP} --name ${IDENTITY_NAME}
Then, assign required permissions for the created identity. The identity must have Reader permission in the resource group that contains the virtual machine scale set of our AKS cluster and
acrpull
permission in the resource group to access repositories to pull images from ACR.export IDENTITY_CLIENT_ID="$(az identity show -g ${IDENTITY_RESOURCE_GROUP} -n ${IDENTITY_NAME} --query clientId -otsv)"
export IDENTITY_RESOURCE_ID="$(az identity show -g ${IDENTITY_RESOURCE_GROUP} -n ${IDENTITY_NAME} --query id -otsv)"
export RG_RESOURCE_ID="$(az group show -g ${RESOURCE_GROUP} --query id -otsv)"
export NODE_GROUP=$(az aks show -g ${RESOURCE_GROUP} -n ${CLUSTER_NAME} --query nodeResourceGroup -o tsv)
export NODES_RESOURCE_ID=$(az group show -n $NODE_GROUP -o tsv --query "id")
az role assignment create --role "Reader" --assignee "$IDENTITY_CLIENT_ID" --scope $NODES_RESOURCE_ID
az role assignment create --role "acrpull" --assignee "$IDENTITY_CLIENT_ID" --scope $RG_RESOURCE_ID
Next, let’s create a pod identity for the cluster using the following command.
az aks pod-identity add --resource-group ${RESOURCE_GROUP} --cluster-name ${CLUSTER_NAME} --namespace dev-ns --name my-sp-pod-identity --identity-resource-id ${IDENTITY_RESOURCE_ID}
Now the first step is done, and we move on to the next step.
In this section, we will create an Azure SQL server and PostgreSQL database, grant database access to the identity we created in the first step.
First, create an Azure SQL server and database.
export DB_SERVER=my-sp-db-server
export DB_RG=my-db-rg
export DB_NAME=my-sp-db
export PGSSLMODE=require
az group create --name=my-database-rg --location eastus
az postgres server create --resource-group ${DB_RG} --name ${DB_SERVER} --location eastus --admin-user myadmin --admin-password P@ssword123 --sku-name B_Gen5_1
az postgres db create -g ${DB_RG} -s ${DB_SERVER} -n ${DB_NAME}
After the SQL server is ready, secure the server by setting the IP firewall rule.
az postgres server firewall-rule create --resource-group ${DB_RG} --server ${DB_SERVER} -name "AllowAllLinuxAzureIps" --start-ip-address YOUR_LOCAL_CLIENT_IP --end-ip-address YOUR_LOCAL_CLIENT_IP
Next, add the Azure AD Admin user to SQL Server; for more details about AD authentication, please refer to this link.
After the AD admin user has been set up, connect as the Azure AD administrator user to the PostgreSQL database using Azure AD authentication and run the following SQL statements:
SET aad_validate_oids_in_tenant = off;
CREATE ROLE myuser WITH LOGIN PASSWORD '<YOU_IDENTITY_CLIENT_ID>' IN ROLE azure_ad_user;
CREATE DATABASE my-sp-db;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO myuser;
Replace <YOUR_IDENTITY_CLIENT_ID> with your identity client id that we created in section 1.
The demo/sample application is a simple Spring Boot REST API; we will build the API in a docker image and then push it to Azure Container Registry (ACR).
The complete Java project is in my Github repo, clone the repo and run the following command in the project root directory:
mvn install dependency:copy-dependencies -DskipTests && cd target/dependency; jar -xf ../*.jar && cd ../..
Make sure you have JAVA SDK and maven installed on your computer.
Next, Create Azure Container Registry.
az acr create --resource-group ${RESOURCE_GROUP} --location eastus --name myspdemo --sku Basic
Then login to the ACR, build and push the Java container image to the registry.
az acr login --name myspdemo && mvn compile jib:build
Note that we have the jib plugin in the Spring Boot project; for more details visit this link.
To connect to the PostgreSQL database using managed identity, we have to acquire an OAuth access token from the MSI endpoint:
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fossrdbms-aad.database.windows.net&client_id=<YOUR_IDENTITY_CLIENT_ID>
Then, configure a DataSource programmatically in Spring Boot. The configuration script would look similar to this:
package com.example.awesomeprject;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.json.JSONTokener;
import org.json.JSONObject;
import java.net.*;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import org.apache.log4j.Logger;
@Configuration
public class DataSourceConfig {
public static Logger logger = Logger.getLogger("global");
@Value("${db.host}")
private String Host;
@Value("${db.user}")
private String User;
@Value("${db.name}")
private String Database;
@Value("${client_id}")
private String ClientId;
@Bean
@RefreshScope
public DataSource getDataSource() {
DataSourceBuilder dataSourceBuilder = DataSourceBuilder.create();
try {
URL url = new URL("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fossrdbms-aad.database.windows.net&client_id=" + ClientId);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
con.setRequestProperty("Metadata", "true");
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
JSONTokener tokener = new JSONTokener(in);
JSONObject json = new JSONObject(tokener);
String accessToken = json.getString("access_token");
logger.info("accessToken: " + accessToken);
dataSourceBuilder.url(this.Host);
dataSourceBuilder.username(this.User);
dataSourceBuilder.password(accessToken);
in.close();
con.disconnect();
} catch(Exception e) {
e.printStackTrace();
}
return dataSourceBuilder.build();
}
}
Now that we have all of the resources, it’s time to deploy our application pod.
With Kustomize, we can create multiple overlays and deploy the application with multi environments to Kubernetes.
Kustomize is a tool included with kubectl 1.14 that “lets you customize raw, template-free YAML files for multiple purposes, leaving the original YAML untouched and usable as is.”
Make a
.k8s/base
directory for all the default configuration templates:apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- service.yaml
- deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-deployment
labels:
app: demo
spec:
selector:
matchLabels:
app: demo
strategy:
type: Recreate
template:
metadata:
labels:
app: demo
aadpodidbinding: my-sp-pod-identity
spec:
containers:
- image: myspdemo2021.azurecr.io/awesomeprject:latest
name: demo
ports:
- containerPort: 8080
env:
- name: DB_SCHEMA
valueFrom:
configMapKeyRef:
name: sp-config
key: DB_SCHEMA
- name: DB_DATA
valueFrom:
configMapKeyRef:
name: sp-config
key: DB_DATA
- name: DS_INIT_MODE
valueFrom:
configMapKeyRef:
name: sp-config
key: DS_INIT_MODE
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: sp-config
key: DB_HOST
- name: DB_USER
valueFrom:
configMapKeyRef:
name: sp-config
key: DB_USER
- name: DB_NAME
valueFrom:
configMapKeyRef:
name: sp-config
key: DB_NAME
- name: CLIENT_ID
valueFrom:
secretKeyRef:
name: sp-secret
key: CLIENT_ID
volumeMounts:
- name: config
mountPath: 'app/resources/config'
readOnly: true
volumes:
- name: config
configMap:
name: sp-config
items:
- key: 'schema.sql'
path: 'schema.sql'
- key: 'data.sql'
path: 'data.sql'
apiVersion: v1
kind: Namespace
metadata:
name: ns
apiVersion: v1
kind: Service
metadata:
name: demo-service
labels:
app: demo
spec:
ports:
- protocol: TCP
port: 80
targetPort: 8080
selector:
app: demo
type: LoadBalancer
Then, make .k8s/dev for development environment configuration, Kustomize called this an overlay. Add new configMap.yml, configmap.yml, and kustomization.yaml files into the overlay directory
.k8s/dev
.apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namePrefix: dev-
namespace: dev-ns
commonLabels:
variant: dev
# patchesStrategicMerge:
resources:
- configmap.yaml
- secret.yaml
bases:
- ../base
apiVersion: v1
kind: ConfigMap
metadata:
name: sp-config
data:
DS_INIT_MODE: always
DB_USER: myuser@my-sp-db-server
DB_HOST: jdbc:postgresql://my-sp-db-server.postgres.database.azure.com:5432/my-sp-db?sslmode=require
DB_NAME: my-sp-db
DB_SCHEMA: config/schema.sql
DB_DATA: config/data.sql
data.sql: |
INSERT INTO "user" (firstName, lastName) SELECT 'William','Ferguson'
WHERE
NOT EXISTS (
SELECT id FROM "user" WHERE firstName = 'William' AND lastName = 'Ferguson'
);
schema.sql: |
DROP TABLE IF EXISTS "user";
CREATE TABLE "user"
(
id SERIAL PRIMARY KEY,
firstName VARCHAR(100) NOT NULL,
lastName VARCHAR(100) NOT NULL
);
apiVersion: v1
kind: Secret
data:
CLIENT_ID: CLIENT_ID_ENCODED_WITH_BASE64
metadata:
name: sp-secret
type: Opaque
In this demo, I store identity base 64 encoded client id into secret, for production, I suggest that we store the client id in Azure Key Vault and integrate Azure Key Vault with AKS.
We are almost there! Now deploy these configuration files to the Kubernetes cluster.
kustomize build .k8s/dev/. | kubectl apply -f -
Once the application has been deployed, use
kubectl
to check the status of our application pod:kubectl get pods -n dev-ns
We will eventually see our application Pod is in Running status and 1/1 containers in the READY column:
NAME READY STATUS RESTARTS AGE
dev-demo-deployment-6499974b5-2srzz 1/1 Running 0 23h
We can view Kubernetes logs, events, and pod metrics in real-time in the Azure Portal.
Container insights includes the Live Data feature, which is an advanced diagnostic feature allowing you direct access to your Azure Kubernetes Service (AKS) container logs (stdout/stderror), events, and pod metrics. It exposes direct access to kubectl logs -c, kubectl get events, and kubectl top pods.
For more details about Container Insight, refer to this link.
With these steps above, we now have a Java Spring Boot REST API running in Kubernetes and connects to an Azure PostgreSQL database using AAD Pod Identities. I also walked you through how to deploy applications to Kubernetes with Kustomize. For the complete code of this sample/demo, please refer to my GitHub repo.
Read behind a paywall at https://codeburst.io/deploying-a-spring-boot-rest-api-on-azure-kubernetes-service-with-azure-database-for-postgresql-4bf86a8059e0