Migrer d’une classe de stockage dans l’arborescence vers des pilotes CSI sur Azure Kubernetes Service (AKS)
L’implémentation du pilote CSI (Container Storage Interface) a été introduite dans Azure Kubernetes Service (AKS) à partir de la version 1.21. En adoptant et en utilisant CSI comme standard, vos charges de travail avec état existantes utilisant des volumes persistants (PV) dans l’arborescence doivent être migrées ou mises à niveau pour utiliser le pilote CSI.
Pour simplifier au maximum ce processus et ne pas perdre de données, cet article fournit différentes options de migration. Ces options incluent des scripts pour garantir une migration fluide de l’arborescence vers des pilotes CSI de disques Azure et Azure Files.
Avant de commencer
- Azure CLI version 2.37.0 ou ultérieure. Exécutez
az --version
pour rechercher la version, puis exécutezaz upgrade
pour mettre à niveau la version. Si vous devez installer ou mettre à niveau, voir Installer Azure CLI. - Les administrateurs Kubectl et de cluster ont accès à la création, à l’obtention, à la création de liste et à la suppression d’un accès à un PVC ou à un PV, à un instantané de volume ou au contenu d’instantané de volume. Pour un cluster Microsoft Entra avec RBAC, vous êtes membre du rôle Administration cluster RBAC Azure Kubernetes Service.
Migrer des volumes de disque
Notes
Les étiquettes failure-domain.beta.kubernetes.io/zone
et failure-domain.beta.kubernetes.io/region
ont été dépréciées dans AKS 1.24 et supprimées dans la version 1.28. Si vos volumes persistants existants utilisent toujours nodeAffinity correspondant à ces deux étiquettes, vous devez les modifier en étiquettes topology.kubernetes.io/zone
et topology.kubernetes.io/region
dans le nouveau paramètre de volume persistant.
La migration de l’arborescence vers CSI est prise en charge à l’aide de deux options de migration :
- Créer un volume statique
- Créer un volume dynamique
Créer un volume statique
À l’aide de cette option, vous créez un PV en affectant claimRef
statiquement à un PVC que vous créerez ultérieurement, et spécifiez le volumeName
pour le PersistentVolumeClaim.
Les avantages de cette approche sont les suivants :
- Il est simple et peut être automatisé.
- Il n’est pas nécessaire de nettoyer la configuration d’origine à l’aide de la classe de stockage dans l’arborescence.
- Faible risque, car vous effectuez uniquement une suppression logique de PV/PVC Kubernetes, les données physiques ne sont pas supprimées.
- Aucun coût supplémentaire du fait de ne pas avoir à créer d’autres objets Azure tels que le disque, les instantanés, etc.
Les éléments suivants sont des considérations importantes à prendre en compte :
- La transition vers des volumes statiques à partir de volumes de style dynamique d’origine nécessite la construction et la gestion manuelle d’objets PV pour toutes les options.
- Temps d’arrêt potentiel de l’application lors du redéploiement de la nouvelle application en référence au nouvel objet PVC.
Migration
Mettez à jour le PV
ReclaimPolicy
existant de Supprimer à Conserver en exécutant la commande suivante :kubectl patch pv pvName -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
Remplacez pvName par le nom de votre sélection persistentVolume. Si vous souhaitez mettre à jour reclaimPolicy pour plusieurs PV, créez également un fichier nommé patchReclaimPVs.sh et copiez-le dans le code suivant.
#!/bin/bash # Patch the Persistent Volume in case ReclaimPolicy is Delete NAMESPACE=$1 i=1 for PVC in $(kubectl get pvc -n $NAMESPACE | awk '{ print $1}'); do # Ignore first record as it contains header if [ $i -eq 1 ]; then i=$((i + 1)) else PV="$(kubectl get pvc $PVC -n $NAMESPACE -o jsonpath='{.spec.volumeName}')" RECLAIMPOLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')" echo "Reclaim Policy for Persistent Volume $PV is $RECLAIMPOLICY" if [[ $RECLAIMPOLICY == "Delete" ]]; then echo "Updating ReclaimPolicy for $pv to Retain" kubectl patch pv $PV -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}' fi fi done
Exécutez le script avec le paramètre
namespace
pour spécifier l’espace de noms du cluster./PatchReclaimPolicy.sh <namespace>
.Obtenez la liste de tous les PVC de l’espace de noms triés par creationTimestamp en exécutant la commande suivante. Définissez l’espace de noms à l’aide de l’argument
--namespace
avec l’espace de noms de cluster.kubectl get pvc -n <namespace> --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,CreationTime:.metadata.creationTimestamp,StorageClass:.spec.storageClassName,Size:.spec.resources.requests.storage
Cette étape est utile si vous avez un grand nombre de PV qui doivent être migrés et que vous souhaitez en migrer quelques-uns à la fois. L’exécution de cette commande vous permet d’identifier les PVC qui ont été créés dans un délai d'exécution donné. Lorsque vous exécutez le script CreatePV.sh, l’heure de début et l’heure de fin sont deux des paramètres qui vous permettent de migrer uniquement les PVC pendant cette période.
Créez un fichier nommé CreatePV.sh et copiez-y le code suivant. Le script effectue les opérations suivantes :
- Crée un PersistentVolume avec le nom
existing-pv-csi
pour tous les persistentVolumes dans les espaces de noms pour la classe de stockagestorageClassName
. - Configurez
existing-pvc-csi
comment nouveau nom du PVC. - Crée un PVC avec le nom de PV que vous spécifiez.
#!/bin/bash #kubectl get pvc -n <namespace> --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,CreationTime:.metadata.creationTimestamp,StorageClass:.spec.storageClassName,Size:.spec.resources.requests.storage # TimeFormat 2022-04-20T13:19:56Z NAMESPACE=$1 FILENAME=$(date +%Y%m%d%H%M)-$NAMESPACE EXISTING_STORAGE_CLASS=$2 STORAGE_CLASS_NEW=$3 STARTTIMESTAMP=$4 ENDTIMESTAMP=$5 i=1 for PVC in $(kubectl get pvc -n $NAMESPACE | awk '{ print $1}'); do # Ignore first record as it contains header if [ $i -eq 1 ]; then i=$((i + 1)) else PVC_CREATION_TIME=$(kubectl get pvc $PVC -n $NAMESPACE -o jsonpath='{.metadata.creationTimestamp}') if [[ $PVC_CREATION_TIME >= $STARTTIMESTAMP ]]; then if [[ $ENDTIMESTAMP > $PVC_CREATION_TIME ]]; then PV="$(kubectl get pvc $PVC -n $NAMESPACE -o jsonpath='{.spec.volumeName}')" RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')" STORAGECLASS="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.storageClassName}')" echo $PVC RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')" if [[ $RECLAIM_POLICY == "Retain" ]]; then if [[ $STORAGECLASS == $EXISTING_STORAGE_CLASS ]]; then STORAGE_SIZE="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.capacity.storage}')" SKU_NAME="$(kubectl get storageClass $STORAGE_CLASS_NEW -o jsonpath='{.parameters.skuname}')" DISK_URI="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.azureDisk.diskURI}')" PERSISTENT_VOLUME_RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')" cat >$PVC-csi.yaml <<EOF apiVersion: v1 kind: PersistentVolume metadata: annotations: pv.kubernetes.io/provisioned-by: disk.csi.azure.com name: $PV-csi spec: accessModes: - ReadWriteOnce capacity: storage: $STORAGE_SIZE claimRef: apiVersion: v1 kind: PersistentVolumeClaim name: $PVC-csi namespace: $NAMESPACE csi: driver: disk.csi.azure.com volumeAttributes: csi.storage.k8s.io/pv/name: $PV-csi csi.storage.k8s.io/pvc/name: $PVC-csi csi.storage.k8s.io/pvc/namespace: $NAMESPACE requestedsizegib: "$STORAGE_SIZE" skuname: $SKU_NAME volumeHandle: $DISK_URI persistentVolumeReclaimPolicy: $PERSISTENT_VOLUME_RECLAIM_POLICY storageClassName: $STORAGE_CLASS_NEW --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: $PVC-csi namespace: $NAMESPACE spec: accessModes: - ReadWriteOnce storageClassName: $STORAGE_CLASS_NEW resources: requests: storage: $STORAGE_SIZE volumeName: $PV-csi EOF kubectl apply -f $PVC-csi.yaml LINE="PVC:$PVC,PV:$PV,StorageClassTarget:$STORAGE_CLASS_NEW" printf '%s\n' "$LINE" >>$FILENAME fi fi fi fi fi done
- Crée un PersistentVolume avec le nom
Pour créer un persistentVolume pour tous les persistentVolumes de l’espace de noms, exécutez le script CreatePV.sh avec les paramètres suivants :
namespace
– Espace de noms de clustersourceStorageClass
– StorageClass basé sur un pilote de stockage dans l’arborescencetargetCSIStorageClass
– StorageClass basé sur le pilote de stockage CSI, qui peut être l’une des classes de stockage par défaut dont le provisionneur est défini sur disk.csi.azure.com ou file.csi.azure.com. Vous pouvez également créer une classe de stockage personnalisée tant qu’elle est définie sur l’un de ces deux provisionneurs.startTimeStamp
– Indiquez une heure de début avant l’heure de création PVC au format aaaa-mm-ddthh :mm :sszendTimeStamp
– Indiquez une heure de fin au format aaaa-mm-jjthh:mm:ssz.
./CreatePV.sh <namespace> <sourceIntreeStorageClass> <targetCSIStorageClass> <startTimestamp> <endTimestamp>
Mettez à jour votre application pour utiliser le nouveau PVC.
Créer un volume dynamique
À l’aide de cette option, vous créez dynamiquement un volume persistant à partir d’une revendication de volume persistant.
Les avantages de cette approche sont les suivants :
Il est moins risqué, car tous les nouveaux objets sont créés tout en conservant d’autres copies avec des instantanés.
Il n’est pas nécessaire de construire des PV séparément et d’ajouter un nom de volume dans le manifeste PVC.
Les éléments suivants sont des considérations importantes à prendre en compte :
Bien que cette approche soit moins risquée, elle crée plusieurs objets qui augmenteront vos coûts de stockage.
Lors de la création des volumes, votre application n’est pas disponible.
Les étapes de suppression doivent être effectuées avec précaution. Des verrous de ressources temporaires peuvent être appliqués à votre groupe de ressources jusqu’à ce que la migration soit terminée et que votre application soit vérifiée.
Effectuez la validation/vérification des données lorsque des disques sont créés à partir d’instantanés.
Migration
Avant de continuer, vérifiez que :
Pour les charges de travail spécifiques où les données sont écrites en mémoire avant d’être écrites sur le disque, l’application doit être arrêtée et pour permettre le vidage des données en mémoire sur le disque.
La classe
VolumeSnapshot
doit exister comme indiqué dans l’exemple YAML suivant :apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: custom-disk-snapshot-sc driver: disk.csi.azure.com deletionPolicy: Delete parameters: incremental: "false"
Obtenez la liste de tous les PVC de l’espace de noms spécifié triés par creationTimestamp en exécutant la commande suivante. Définissez l’espace de noms à l’aide de l’argument
--namespace
avec l’espace de noms de cluster.kubectl get pvc --namespace <namespace> --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,CreationTime:.metadata.creationTimestamp,StorageClass:.spec.storageClassName,Size:.spec.resources.requests.storage
Cette étape est utile si vous avez un grand nombre de PV qui doivent être migrés et que vous souhaitez en migrer quelques-uns à la fois. L’exécution de cette commande vous permet d’identifier les PVC qui ont été créés dans un délai d'exécution donné. Lorsque vous exécutez le script MigrateCSI.sh, l’heure de début et l’heure de fin sont deux des paramètres qui vous permettent de migrer uniquement les PVC pendant cette période.
Créez un fichier nommé MigrateToCSI.sh et copiez-y le code suivant. Le script effectue les opérations suivantes :
- Crée un instantané de disque complet à l’aide de l’interface de ligne de commande Azure
- Crée
VolumesnapshotContent
- Crée
VolumeSnapshot
- Crée un PVC à partir de
VolumeSnapshot
- Crée un fichier avec le nom de fichier
<namespace>-timestamp
, qui contient la liste de toutes les anciennes ressources à nettoyer.
#!/bin/bash #kubectl get pvc -n <namespace> --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,CreationTime:.metadata.creationTimestamp,StorageClass:.spec.storageClassName,Size:.spec.resources.requests.storage # TimeFormat 2022-04-20T13:19:56Z NAMESPACE=$1 FILENAME=$NAMESPACE-$(date +%Y%m%d%H%M) EXISTING_STORAGE_CLASS=$2 STORAGE_CLASS_NEW=$3 VOLUME_STORAGE_CLASS=$4 START_TIME_STAMP=$5 END_TIME_STAMP=$6 i=1 for PVC in $(kubectl get pvc -n $NAMESPACE | awk '{ print $1}'); do # Ignore first record as it contains header if [ $i -eq 1 ]; then i=$((i + 1)) else PVC_CREATION_TIME=$(kubectl get pvc $PVC -n $NAMESPACE -o jsonpath='{.metadata.creationTimestamp}') if [[ $PVC_CREATION_TIME > $START_TIME_STAMP ]]; then if [[ $END_TIME_STAMP > $PVC_CREATION_TIME ]]; then PV="$(kubectl get pvc $PVC -n $NAMESPACE -o jsonpath='{.spec.volumeName}')" RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')" STORAGE_CLASS="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.storageClassName}')" echo $PVC RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')" if [[ $STORAGE_CLASS == $EXISTING_STORAGE_CLASS ]]; then STORAGE_SIZE="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.capacity.storage}')" SKU_NAME="$(kubectl get storageClass $STORAGE_CLASS_NEW -o jsonpath='{.parameters.skuname}')" DISK_URI="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.azureDisk.diskURI}')" TARGET_RESOURCE_GROUP="$(cut -d'/' -f5 <<<"$DISK_URI")" echo $DISK_URI SUBSCRIPTION_ID="$(echo $DISK_URI | grep -o 'subscriptions/[^/]*' | sed 's#subscriptions/##g')" echo $TARGET_RESOURCE_GROUP PERSISTENT_VOLUME_RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')" az snapshot create --resource-group $TARGET_RESOURCE_GROUP --name $PVC-$FILENAME --source "$DISK_URI" --subscription ${SUBSCRIPTION_ID} SNAPSHOT_PATH=$(az snapshot list --resource-group $TARGET_RESOURCE_GROUP --query "[?name == '$PVC-$FILENAME'].id | [0]" --subscription ${SUBSCRIPTION_ID}) SNAPSHOT_HANDLE=$(echo "$SNAPSHOT_PATH" | tr -d '"') echo $SNAPSHOT_HANDLE sleep 10 # Create Restore File cat <<EOF >$PVC-csi.yml apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotContent metadata: name: $PVC-$FILENAME spec: deletionPolicy: 'Delete' driver: 'disk.csi.azure.com' volumeSnapshotClassName: $VOLUME_STORAGE_CLASS source: snapshotHandle: $SNAPSHOT_HANDLE volumeSnapshotRef: apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot name: $PVC-$FILENAME namespace: $1 --- apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: $PVC-$FILENAME namespace: $1 spec: volumeSnapshotClassName: $VOLUME_STORAGE_CLASS source: volumeSnapshotContentName: $PVC-$FILENAME --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: csi-$PVC namespace: $1 spec: accessModes: - ReadWriteOnce storageClassName: $STORAGE_CLASS_NEW resources: requests: storage: $STORAGE_SIZE dataSource: name: $PVC-$FILENAME kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io EOF kubectl create -f $PVC-csi.yml LINE="OLDPVC:$PVC,OLDPV:$PV,VolumeSnapshotContent:volumeSnapshotContent-$FILENAME,VolumeSnapshot:volumesnapshot$FILENAME,OLDdisk:$DISK_URI" printf '%s\n' "$LINE" >>$FILENAME fi fi fi fi done
Pour migrer les volumes de disque, exécutez le script MigrateToCSI.sh avec les paramètres suivants :
namespace
– Espace de noms de clustersourceStorageClass
– StorageClass basé sur un pilote de stockage dans l’arborescencetargetCSIStorageClass
– StorageClass basé sur un pilote de stockage CSIvolumeSnapshotClass
– Nom de la classe d’instantané de volume. Par exemple :custom-disk-snapshot-sc
.startTimeStamp
– Indiquez une heure de début au format aaaa-mm-jjthh:mm:ssz.endTimeStamp
– Indiquez une heure de fin au format aaaa-mm-jjthh:mm:ssz.
./MigrateToCSI.sh <namespace> <sourceStorageClass> <TargetCSIstorageClass> <VolumeSnapshotClass> <startTimestamp> <endTimestamp>
Mettez à jour votre application pour utiliser le nouveau PVC.
Supprimez manuellement les anciennes ressources, notamment les PVC/PV dans l’arborescence, VolumeSnapshot et VolumeSnapshotContent. Sinon, la maintenance des objets PVC/PC et capture instantanée dans l’arborescence génère plus de coûts.
Migrer des volumes de partage de fichiers
La migration de l’arborescence vers CSI est prise en charge par la création d’un volume statique :
- Il n’est pas nécessaire de nettoyer la configuration d’origine à l’aide de la classe de stockage dans l’arborescence.
- Faible risque, car vous effectuez uniquement une suppression logique de PV/PVC Kubernetes, les données physiques ne sont pas supprimées.
- Aucun coût supplémentaire du fait de ne pas avoir à créer d’autres objets Azure tels que les partages de fichiers, etc.
Migration
Mettez à jour le PV
ReclaimPolicy
existant de Supprimer à Conserver en exécutant la commande suivante :kubectl patch pv pvName -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
Remplacez pvName par le nom de votre sélection persistentVolume. Si vous souhaitez mettre à jour reclaimPolicy pour plusieurs PV, créez également un fichier nommé patchReclaimPVs.sh et copiez-le dans le code suivant.
#!/bin/bash # Patch the Persistent Volume in case ReclaimPolicy is Delete namespace=$1 i=1 for pvc in $(kubectl get pvc -n $namespace | awk '{ print $1}'); do # Ignore first record as it contains header if [ $i -eq 1 ]; then i=$((i + 1)) else pv="$(kubectl get pvc $pvc -n $namespace -o jsonpath='{.spec.volumeName}')" reclaimPolicy="$(kubectl get pv $pv -n $namespace -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')" echo "Reclaim Policy for Persistent Volume $pv is $reclaimPolicy" if [[ $reclaimPolicy == "Delete" ]]; then echo "Updating ReclaimPolicy for $pv to Retain" kubectl patch pv $pv -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}' fi fi done
Exécutez le script avec le paramètre
namespace
pour spécifier l’espace de noms du cluster./PatchReclaimPolicy.sh <namespace>
.Créez une classe de stockage avec le provisionneur défini sur
file.csi.azure.com
, ou vous pouvez utiliser l’une des StorageClasses par défaut avec le provisionneur de fichiers CSI.Obtenez le
secretName
etshareName
à partir des PersistentVolumes existantes en exécutant la commande suivante :kubectl describe pv pvName
Créez un PV à l’aide de la nouvelle classe StorageClass et du
shareName
etsecretName
du PV dans l’arborescence. Créez un fichier nommé azurefile-mount-pv.yaml, et copiez-y le code suivant. Souscsi
, mettez à jourresourceGroup
,volumeHandle
etshareName
. Pour les options de montage, la valeur par défaut pour fileMode et dirMode est 0777.La valeur par défaut pour
fileMode
etdirMode
est 0777.apiVersion: v1 kind: PersistentVolume metadata: annotations: pv.kubernetes.io/provisioned-by: file.csi.azure.com name: azurefile spec: capacity: storage: 5Gi accessModes: - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: azurefile-csi csi: driver: file.csi.azure.com readOnly: false volumeHandle: unique-volumeid # make sure volumeid is unique for every identical share in the cluster volumeAttributes: resourceGroup: EXISTING_RESOURCE_GROUP_NAME # optional, only set this when storage account is not in the same resource group as the cluster nodes shareName: aksshare nodeStageSecretRef: name: azure-secret namespace: default mountOptions: - dir_mode=0777 - file_mode=0777 - uid=0 - gid=0 - mfsymlinks - cache=strict - nosharesock - nobrl # disable sending byte range lock requests to the server and for applications which have challenges with posix locks
Créez un fichier nommé azurefile-mount-pvc.yaml avec un PersistentVolumeClaim qui utilise PersistentVolume à l’aide du code suivant.
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: azurefile spec: accessModes: - ReadWriteMany storageClassName: azurefile-csi volumeName: azurefile resources: requests: storage: 5Gi
Utilisez la commande
kubectl
pour créer l’objet PersistentVolume.kubectl apply -f azurefile-mount-pv.yaml
Utilisez la commande
kubectl
pour créer l’objet PersistentVolumeClaim.kubectl apply -f azurefile-mount-pvc.yaml
Vérifiez que votre PersistentVolumeClaim est créé et lié au PersistentVolume en exécutant la commande suivante.
kubectl get pvc azurefile
La sortie se présente comme suit :
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE azurefile Bound azurefile 5Gi RWX azurefile 5s
Mettez à jour les spécifications de votre conteneur pour référencer votre PersistentVolumeClaim et mettre à jour votre pod. Par exemple, copiez le code suivant et créez un fichier nommé azure-files-pod.yaml.
... volumes: - name: azure persistentVolumeClaim: claimName: azurefile
La spécification du pod ne peut pas être mise à jour sur place. Utilisez les commandes
kubectl
suivantes pour supprimer, puis recréer le pod.kubectl delete pod mypod
kubectl apply -f azure-files-pod.yaml
Étapes suivantes
- Pour plus d’informations sur les meilleures pratiques relatives au stockage, consultez Meilleures pratiques relatives au stockage et aux sauvegardes dans Azure Kubernetes Service (AKS)
- Protégez les PV basés sur un pilote CSI que vous venez de migrer en procédant à leur sauvegarde à l’aide de Sauvegarde Azure pour AKS.
Azure Kubernetes Service