zmoog / public-notes

Apache License 2.0
0 stars 1 forks source link

Figure out how Workload Identity works #72

Open zmoog opened 5 months ago

zmoog commented 5 months ago

I have an AKS cluster on Azure. I want to write a Go application that lists the VM on a subscription without using credentials.

Azure offers an authentication mechanism called workload identity that seems a good fit.

Let's see how it works.

zmoog commented 5 months ago

After some reading and a few failed attempts, the document Deploy and configure workload identity on an Azure Kubernetes Service (AKS) cluster seems the best resource I was able to get.

zmoog commented 5 months ago

Export environment variables

For this walkthrough, I'm using an existing cluster named mbranca-sdh-4160:

export RESOURCE_GROUP="mbranca-sdh-4160"
export CLUSTER_NAME="mbranca-sdh-4160"
export LOCATION="eastus2"
export SERVICE_ACCOUNT_NAMESPACE="default"
export SERVICE_ACCOUNT_NAME="workload-identity-sa"
export SUBSCRIPTION="$(az account show --query id --output tsv)"
export USER_ASSIGNED_IDENTITY_NAME="myIdentity"
export FEDERATED_IDENTITY_CREDENTIAL_NAME="myFedIdentity"
zmoog commented 5 months ago

Update an existing AKS cluster

Since I already have a cluster, I'll set it up enabling OIDC and workload identity:

az aks update \
    -g "${RESOURCE_GROUP}" \
    -n "${CLUSTER_NAME}" \
    --enable-oidc-issuer \
    --enable-workload-identity
zmoog commented 5 months ago

Retrieve the OIDC Issuer URL

Now that OIDC is enabled, we can collect the OIDC Issuer URL and store it as environment variable:

export AKS_OIDC_ISSUER="$(az aks show --resource-group "${RESOURCE_GROUP}" --name "${CLUSTER_NAME}" --query "oidcIssuerProfile.issuerUrl" -otsv)"
zmoog commented 5 months ago

Create a managed identity

From the source document:

Use the Azure CLI az account set command to set a specific subscription to be the current active subscription. Then use the az identity create command to create a managed identity.

# set the default subscription
az account set --subscription "${SUBSCRIPTION}"

az identity create --name "${USER_ASSIGNED_IDENTITY_NAME}" --resource-group "${RESOURCE_GROUP}" --location "${LOCATION}" --subscription "${SUBSCRIPTION}"

# (optional) if you need to delete one 
az identity delete --name "${USER_ASSIGNED_IDENTITY_NAME}" --resource-group "${RESOURCE_GROUP}" --subscription "${SUBSCRIPTION}"
zmoog commented 5 months ago

Create Kubernetes service account

Create a Kubernetes service account and annotate it with the client ID of the managed identity created in the previous step. Use the az aks get-credentials command and replace the values for the cluster name and the resource group name.

az aks get-credentials -n "${CLUSTER_NAME}" -g "${RESOURCE_GROUP}"

Copy and paste the following multi-line input in the Azure CLI.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  annotations:
    azure.workload.identity/client-id: "${USER_ASSIGNED_CLIENT_ID}"
  name: "${SERVICE_ACCOUNT_NAME}"
  namespace: "${SERVICE_ACCOUNT_NAMESPACE}"
EOF

Use the az identity federated-credential create command to create the federated identity credential between the managed identity, the service account issuer, and the subject.

az identity federated-credential create \
  --name ${FEDERATED_IDENTITY_CREDENTIAL_NAME} \
  --identity-name "${USER_ASSIGNED_IDENTITY_NAME}" \
  --resource-group "${RESOURCE_GROUP}" \
  --issuer "${AKS_OIDC_ISSUER}" \
  --subject system:serviceaccount:"${SERVICE_ACCOUNT_NAMESPACE}":"${SERVICE_ACCOUNT_NAME}" \
  --audience api://AzureADTokenExchange
zmoog commented 5 months ago

Build your application

For this test, I created a trivial "application" that count the VMs available on a subscription.

Here's the source code:

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"

    "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
    "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute"
    "k8s.io/klog/v2"
)

func main() {
    subscriptionId := os.Getenv("SUBSCRIPTION_ID")

    credential, err := azidentity.NewDefaultAzureCredential(nil)
    if err != nil {
        klog.Fatal(err)
    }

    client, err := armcompute.NewVirtualMachinesClient(subscriptionId, credential, nil)
    if err != nil {
        klog.Fatal(err)
    }

    pager := client.NewListAllPager(nil)

    for pager.More() {
        page, err := pager.NextPage(context.Background())
        if err != nil {
            klog.Fatal(err)
        }

        fmt.Printf("Found %d VMs\n", len(page.Value))
    }

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "Hello, Kubernetes!")
    })

    err = http.ListenAndServe(":8080", nil)
    if err != nil {
        klog.Fatal(err)
    }
}

Here is the Dockerfile to build an image:

FROM golang:1.21 as builder

WORKDIR /app
COPY . .

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o myapp

FROM alpine:3.14

WORKDIR /app
COPY --from=builder /app/myapp /app/

EXPOSE 8080
CMD ["/app/myapp"]

And finally, the K8s deployment file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      name: myapp
      namespace: default
      labels:
        azure.workload.identity/use: "true"
        app: myapp
    spec:
      serviceAccountName: workload-identity-sa
      containers:
        - name: myapp
          image: zmoog/myapp:latest
          env:
          - name: SUBSCRIPTION_ID
            value: "<put your subscription ID here>"

I build and pushed the image to Docker Hub:

docker build -t zmoog/myapp:latest .  

docker push zmoog/myapp:latest  
zmoog commented 5 months ago

Permit myIdentity to list the VMs

The workload identity infrastructure is ready, but we must give myIdentity permission to read the VM information.

I'm using Azure Portal for this step, but I want to come back later and replace this section with a CLI command.

On Azure Portal:

  1. Visit Subscriptions > [select your subscription] > Access Control (IAM)
  2. Click on Add > Add role assignment
  3. In the Role tab, select Reader
  4. In the Members tab, click Select member
  5. Type "myIdentity" in the search box
  6. Select "myIdentity" and click on the Select button to confirm
  7. Click on Review + assign to complete the operation

CleanShot 2024-01-17 at 00 12 53@2x

Now, the identity "myIdentity" is authorized to perform the action the app needs.

zmoog commented 5 months ago

Run your application

As a final step, I will deploy and run the app on k8s.

$ k apply -f deployment.yaml
deployment.apps/myapp-deployment created

$ k get pods                                                                                                                                                                          
NAME                                READY   STATUS    RESTARTS   AGE
myapp-deployment-785ff98547-tmxn9   1/1     Running   0          14s

$ k logs myapp-deployment-785ff98547-tmxn9                                                                                                                                            
Found 7 VMs