Implementa y actualiza una aplicación de .NET Core en Google Kubernetes Engine

1. Descripción general

Microsoft .NET Core es una versión de código abierto y multiplataforma de .NET que puede ejecutarse de forma nativa en contenedores. .NET Core está disponible en GitHub y se mantiene mediante Microsoft y la comunidad de .NET. En este lab, se implementará una aplicación de .NET Core alojada en contenedores en Google Kubernetes Engine (GKE).

En este lab, se sigue un patrón de desarrollo típico en el que las aplicaciones se desarrollan en un entorno local de desarrollador y, luego, se implementan en producción. En la primera parte del lab, se valida una aplicación principal de ejemplo de .NET mediante un contenedor que se ejecuta en Cloud Shell. Una vez validada, la app se implementa en Kubernetes mediante GKE. El lab incluye los pasos para crear un clúster de GKE.

En la segunda parte del lab, se realiza un cambio menor en la aplicación que muestra el nombre de host del contenedor que ejecuta esa instancia de la aplicación. Luego, se valida la aplicación actualizada en Cloud Shell y se actualiza la implementación para usar la versión nueva. En la siguiente ilustración, se muestra la secuencia de actividades de este lab:

Diagrama de secuencia de demostración

Costos

Si ejecutas este lab exactamente como está escrito, se aplicarán los costos normales de los siguientes servicios

2. Configuración y requisitos

Requisitos previos

Para completar este lab, necesitas una cuenta y un proyecto de Google Cloud. Si quieres obtener instrucciones más detalladas para crear un proyecto nuevo, consulta este codelab.

En este lab, se utiliza la ejecución de Docker en Cloud Shell, que está disponible en la consola de Google Cloud y viene preconfigurado con muchas herramientas útiles, como gcloud y Docker. A continuación, se muestra cómo acceder a Cloud Shell. Haz clic en el ícono de Cloud Shell en la esquina superior derecha para que se muestre en el panel inferior de la ventana de la consola.

Cloud Shell

Opciones de configuración alternativas para el clúster de GKE (opcional)

Este lab requiere un clúster de Kubernetes. En la próxima sección, se creará un clúster de GKE con una configuración sencilla. En esta sección, se muestran algunos comandos de gcloud que proporcionan opciones de configuración alternativas para usar cuando se compila un clúster de Kubernetes con GKE. Por ejemplo, si usas los siguientes comandos, es posible identificar diferentes tipos de máquinas, zonas y hasta GPU (aceleradores).

  • Genera una lista de los tipos de máquina con este comando gcloud compute machine-types list
  • Genera una lista de las GPU con este comando gcloud compute accelerator-types list
  • Genera una lista de las zonas de procesamiento con este comando gcloud compute zones list
  • Obtén ayuda con cualquier comando de gcloud gcloud container clusters --help
    • Por ejemplo, aquí se proporcionan detalles para crear un clúster de Kubernetes gcloud container clusters create --help

Para obtener una lista completa de las opciones de configuración de GKE, consulta este documento.

Prepárate para crear el clúster de Kubernetes

En Cloud Shell, es necesario establecer algunas variables de entorno y configurar el cliente de gcloud. Esto se logra con los siguientes comandos.

export PROJECT_ID=YOUR_PROJECT_ID
export DEFAULT_ZONE=us-central1-c

gcloud config set project ${PROJECT_ID}
gcloud config set compute/zone ${DEFAULT_ZONE}

Crea un clúster de GKE

Debido a que este lab implementa la aplicación .NET Core en Kubernetes, es necesario crear un clúster. Usa el siguiente comando para crear un nuevo clúster de Kubernetes en Google Cloud con GKE.

gcloud container clusters create dotnet-cluster \
  --zone ${DEFAULT_ZONE} \
  --num-nodes=1 \
  --node-locations=${DEFAULT_ZONE},us-central1-b \
  --enable-stackdriver-kubernetes \
  --machine-type=n1-standard-1 \
  --workload-pool=${PROJECT_ID}.svc.id.goog \
  --enable-ip-alias
  • --num-nodes es la cantidad de nodos que se agregarán por zona y se puede escalar más adelante.
  • --node-locations es una lista de zonas separadas por comas. En este caso, la zona que identificas en la variable de entorno anterior y us-central1-b se usan
    • NOTA: Esta lista no puede contener duplicados
  • --workload-pool establece la identidad de las cargas de trabajo de GKE para que puedan acceder a los servicios de Google Cloud.

Mientras se compila el clúster, se muestra lo siguiente:

Creating cluster dotnet-cluster in us-central1-b... Cluster is being deployed...⠼

Configura kubectl

La CLI de kubectl es la forma principal de interactuar con un clúster de Kubernetes. Para usarlo con el clúster nuevo que se acaba de crear, se debe configurar de modo que se autentique en el clúster. Esto se hace con el siguiente comando.

$ gcloud container clusters get-credentials dotnet-cluster --zone ${DEFAULT_ZONE}
Fetching cluster endpoint and auth data.
kubeconfig entry generated for dotnet-cluster.

Ahora debería ser posible usar kubectl para interactuar con el clúster.

$ kubectl get nodes
NAME                                            STATUS   ROLES    AGE     VERSION
gke-dotnet-cluster-default-pool-02c9dcb9-fgxj   Ready    <none>   2m15s   v1.16.13-gke.401
gke-dotnet-cluster-default-pool-ed09d7b7-xdx9   Ready    <none>   2m24s   v1.16.13-gke.401

3. Realiza pruebas locales y confirma la funcionalidad deseada

En este lab, se utilizan las siguientes imágenes de contenedor del repositorio oficial de .NET en el concentrador de Docker.

Ejecuta el contenedor de forma local para verificar la funcionalidad

En Cloud Shell, ejecuta el siguiente comando de Docker para verificar que Docker esté funcionando correctamente y que el contenedor .NET funcione como se espera:

$ docker run --rm mcr.microsoft.com/dotnet/samples

      Hello from .NET!
      __________________
                        \
                        \
                            ....
                            ....'
                            ....
                          ..........
                      .............'..'..
                  ................'..'.....
                .......'..........'..'..'....
                ........'..........'..'..'.....
              .'....'..'..........'..'.......'.
              .'..................'...   ......
              .  ......'.........         .....
              .                           ......
              ..    .            ..        ......
            ....       .                 .......
            ......  .......          ............
              ................  ......................
              ........................'................
            ......................'..'......    .......
          .........................'..'.....       .......
      ........    ..'.............'..'....      ..........
    ..'..'...      ...............'.......      ..........
    ...'......     ...... ..........  ......         .......
  ...........   .......              ........        ......
  .......        '...'.'.              '.'.'.'         ....
  .......       .....'..               ..'.....
    ..       ..........               ..'........
            ............               ..............
          .............               '..............
          ...........'..              .'.'............
        ...............              .'.'.............
        .............'..               ..'..'...........
        ...............                 .'..............
        .........                        ..............
          .....
  
Environment:
.NET 5.0.1-servicing.20575.16
Linux 5.4.58-07649-ge120df5deade #1 SMP PREEMPT Wed Aug 26 04:56:33 PDT 2020

Confirma las funciones de la app web

Una aplicación web de muestra también se puede validar en Cloud Shell. El comando Docker run que aparece a continuación crea un contenedor nuevo que expone el puerto 80 y lo asigna al puerto 8080 de localhost. Recuerda que, en este caso, localhost está en Cloud Shell.

$ docker run -it --rm -p 8080:80 --name aspnetcore_sample mcr.microsoft.com/dotnet/samples:aspnetapp
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {64a3ed06-35f7-4d95-9554-8efd38f8b5d3} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app

Como se trata de una app web, debe visualizarse y validarse en un navegador web. En la siguiente sección, se muestra cómo hacerlo en Cloud Shell con la Vista previa en la Web.

4. Accede a los servicios desde Cloud Shell con “Vista previa en la Web”

Cloud Shell ofrece Vista previa en la Web, una función que permite usar un navegador para interactuar con los procesos que se ejecutan en la instancia de Cloud Shell.

Utiliza "Vista previa en la Web" para ver apps en Cloud Shell

En Cloud Shell, haz clic en el botón de vista previa en la Web y elige "Vista previa en el puerto 8080". (o en cualquier puerto que la Vista previa web esté configurada para usar).

Cloud Shell

Se abrirá una ventana del navegador con una dirección como la siguiente:

https://8080-cs-754738286554-default.us-central1.cloudshell.dev/?authuser=0

Visualiza la aplicación de muestra de .NET con la vista previa en la Web

Para ver la app de ejemplo que se inició en el último paso, ahora puedes iniciar la Vista previa en la Web y cargar la URL proporcionada. Debería verse algo similar a esto:

Captura de pantalla de la app de .NET V1

5. Implementar en Kubernetes

Compila el archivo YAML y aplica

El siguiente paso requiere un archivo YAML que describa dos recursos de Kubernetes: un Deployment y un Service. Crea un archivo llamado dotnet-app.yaml en Cloud Shell y agrégale el siguiente contenido.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dotnet-deployment
  labels:
    app: dotnetapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: dotnetapp
  template:
    metadata:
      labels:
        app: dotnetapp
    spec:
      containers:
      - name: dotnet
        image: mcr.microsoft.com/dotnet/samples:aspnetapp
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: dotnet-service
spec:
    selector:
      app: dotnetapp
    ports:
      - protocol: TCP
        port: 8080
        targetPort: 80

Ahora, usa kubectl para aplicar este archivo a Kubernetes.

$ kubectl apply -f dotnet-app.yaml
deployment.apps/dotnet-deployment created
service/dotnet-service created

Observa los mensajes que indican que se crearon los recursos deseados.

Explora los recursos resultantes

Podemos usar la CLI de kubectl para examinar los recursos que se crearon antes. Primero, veamos los recursos del objeto Deployment y confirmemos que la nueva implementación exista.

$ kubectl get deployment
NAME                READY   UP-TO-DATE   AVAILABLE   AGE
dotnet-deployment   3/3     3            3           80s

A continuación, observa los ReplicaSets. Debería haber un ReplicaSet creado con la implementación anterior.

$ kubectl get replicaset
NAME                           DESIRED   CURRENT   READY   AGE
dotnet-deployment-5c9d4cc4b9   3         3         3       111s

Por último, observa los Pods. El objeto Deployment indicó que debería haber tres instancias. El siguiente comando debería mostrar que hay tres instancias. Se agrega la opción -o wide para que se muestren los nodos en los que se ejecutan esas instancias.

$ kubectl get pod -o wide
NAME                                 READY   STATUS    RESTARTS   AGE     IP          NODE                                            NOMINATED NODE   READINESS GATES
dotnet-deployment-5c9d4cc4b9-cspqd   1/1     Running   0          2m25s   10.16.0.8   gke-dotnet-cluster-default-pool-ed09d7b7-xdx9   <none>           <none>
dotnet-deployment-5c9d4cc4b9-httw6   1/1     Running   0          2m25s   10.16.1.7   gke-dotnet-cluster-default-pool-02c9dcb9-fgxj   <none>           <none>
dotnet-deployment-5c9d4cc4b9-vvdln   1/1     Running   0          2m25s   10.16.0.7   gke-dotnet-cluster-default-pool-ed09d7b7-xdx9   <none>           <none>

Revisa el recurso Service

Un recurso de Servicio en Kubernetes es un balanceador de cargas. Los extremos están determinados por etiquetas en los Pods. De esta manera, apenas se agregan Pods nuevos a la implementación a través de la operación kubectl scale deployment anterior, los Pods resultantes están disponibles de inmediato para las solicitudes que maneja ese Service.

El siguiente comando debería mostrar el recurso Service.

$ kubectl get svc
NAME             TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
dotnet-service   ClusterIP   10.20.9.124   <none>        8080/TCP   2m50s
...

Puedes obtener más detalles sobre el Service con el siguiente comando.

$ kubectl describe svc dotnet-service
Name:              dotnet-service
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=dotnetapp
Type:              ClusterIP
IP:                10.20.9.124
Port:              <unset>  8080/TCP
TargetPort:        80/TCP
Endpoints:         10.16.0.7:80,10.16.0.8:80,10.16.1.7:80
Session Affinity:  None
Events:            <none>

Ten en cuenta que el Service es del tipo ClusterIP. Esto significa que cualquier Pod dentro del clúster puede resolver el nombre del Service, dotnet-service, en su dirección IP. Las cargas de las solicitudes enviadas al servicio se balancearán en todas las instancias (Pods). El valor Endpoints anterior muestra las IP de los Pods que están disponibles actualmente para este servicio. Compara estas direcciones con las IP de los Pods que se mostraron con anterioridad.

Verifica la app en ejecución

En este punto, la aplicación está activa y lista para recibir solicitudes de los usuarios. Para acceder a ella, usa un proxy. El siguiente comando crea un proxy local que acepta solicitudes en el puerto 8080 y las pasa al clúster de Kubernetes.

$ kubectl proxy --port 8080
Starting to serve on 127.0.0.1:8080

Ahora, usa la Vista previa en la Web en Cloud Shell para acceder a la aplicación web.

Agrega lo siguiente a la URL que genera la Vista previa en la Web: /api/v1/namespaces/default/services/dotnet-service:8080/proxy/. El resultado final será similar al siguiente:

https://8080-cs-473655782854-default.us-central1.cloudshell.dev/api/v1/namespaces/default/services/dotnet-service:8080/proxy/

Felicitaciones por implementar una app .NET Core en Google Kubernetes Engine. A continuación, realizaremos un cambio en la aplicación y volveremos a implementarla.

6. Modifica la app

En esta sección, la aplicación se modificará para mostrar el host en el que se ejecuta la instancia. Esto permitirá confirmar que el balanceo de cargas está funcionando y que los Pods disponibles responden como se espera.

Obtén el código fuente

git clone https://github.com/dotnet/dotnet-docker.git
cd dotnet-docker/samples/aspnetapp/

Actualiza la app para que incluya el nombre de host

vi aspnetapp/Pages/Index.cshtml
    <tr>
        <td>Host</td>
        <td>@Environment.MachineName</td>
    </tr>

Compila una nueva imagen de contenedor y pruébala de forma local

Compila la nueva imagen de contenedor con el código actualizado.

docker build --pull -t aspnetapp:alpine -f Dockerfile.alpine-x64 .

Como antes, prueba la aplicación nueva de forma local

$ docker run --rm -it -p 8080:80 aspnetapp:alpine
warn: Microsoft.AspNetCore.DataProtection.Repositories.FileSystemXmlRepository[60]
      Storing keys in a directory '/root/.aspnet/DataProtection-Keys' that may not be persisted outside of the container. Protected data will be unavailable when container is destroyed.
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
      No XML encryptor configured. Key {f71feb13-8eae-4552-b4f2-654435fff7f8} may be persisted to storage in unencrypted form.
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://[::]:80
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app

Al igual que antes, se puede acceder a la app mediante la Vista previa en la Web. Esta vez, el parámetro Host debería estar visible, como se muestra aquí:

Cloud Shell

Abre una pestaña nueva en Cloud Shell y ejecuta docker ps para ver que el ID del contenedor coincida con el valor del host que se muestra arriba.

$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS                  NAMES
ab85ce11aecd        aspnetapp:alpine    "./aspnetapp"       2 minutes ago       Up 2 minutes        0.0.0.0:8080->80/tcp   relaxed_northcutt

Etiqueta y envía la imagen a fin de que esté disponible para Kubernetes

La imagen se debe etiquetar y enviar para que Kubernetes pueda extraerla. Comienza por enumerar las imágenes de contenedor y, luego, identifica la imagen deseada.

$ docker image list
REPOSITORY                                         TAG                 IMAGE ID            CREATED             SIZE
aspnetapp                                          alpine              95b4267bb6d0        6 days ago          110MB

Luego, etiqueta esa imagen y envíala a Google Container Registry. Si usas el ID de IMAGEN anterior, se verá de la siguiente manera:

docker tag 95b4267bb6d0 gcr.io/${PROJECT_ID}/aspnetapp:alpine
docker push gcr.io/${PROJECT_ID}/aspnetapp:alpine

7. Vuelve a implementar la aplicación actualizada

Edita el archivo YAML

Vuelve al directorio donde se guarda el archivo dotnet-app.yaml. Busca la siguiente línea en el archivo YAML.

        image: mcr.microsoft.com/dotnet/core/samples:aspnetapp

Esto debe cambiarse para hacer referencia a la imagen de contenedor que se creó y envió a gcr.io anteriormente.

        image: gcr.io/PROJECT_ID/aspnetapp:alpine

No olvides modificarlo para usar tu PROJECT_ID. Cuando termines, debería verse de la siguiente manera:

        image: gcr.io/myproject/aspnetapp:alpine

Aplica el archivo YAML actualizado

$ kubectl apply -f dotnet-app.yaml
deployment.apps/dotnet-deployment configured
service/dotnet-service unchanged

Observa que el recurso Deployment se muestra actualizado y que el recurso Service se muestra sin cambios. Los Pods actualizados se pueden ver como antes con el comando kubectl get pod, pero esta vez agregaremos -w, que observará todos los cambios a medida que ocurren.

$ kubectl get pod -w
NAME                                 READY   STATUS              RESTARTS   AGE
dotnet-deployment-5c9d4cc4b9-cspqd   1/1     Running             0          34m
dotnet-deployment-5c9d4cc4b9-httw6   1/1     Running             0          34m
dotnet-deployment-5c9d4cc4b9-vvdln   1/1     Running             0          34m
dotnet-deployment-85f6446977-tmbdq   0/1     ContainerCreating   0          4s
dotnet-deployment-85f6446977-tmbdq   1/1     Running             0          5s
dotnet-deployment-5c9d4cc4b9-vvdln   1/1     Terminating         0          34m
dotnet-deployment-85f6446977-lcc58   0/1     Pending             0          0s
dotnet-deployment-85f6446977-lcc58   0/1     Pending             0          0s
dotnet-deployment-85f6446977-lcc58   0/1     ContainerCreating   0          0s
dotnet-deployment-5c9d4cc4b9-vvdln   0/1     Terminating         0          34m
dotnet-deployment-85f6446977-lcc58   1/1     Running             0          6s
dotnet-deployment-5c9d4cc4b9-cspqd   1/1     Terminating         0          34m
dotnet-deployment-85f6446977-hw24v   0/1     Pending             0          0s
dotnet-deployment-85f6446977-hw24v   0/1     Pending             0          0s
dotnet-deployment-5c9d4cc4b9-cspqd   0/1     Terminating         0          34m
dotnet-deployment-5c9d4cc4b9-vvdln   0/1     Terminating         0          34m
dotnet-deployment-5c9d4cc4b9-vvdln   0/1     Terminating         0          34m
dotnet-deployment-85f6446977-hw24v   0/1     Pending             0          2s
dotnet-deployment-85f6446977-hw24v   0/1     ContainerCreating   0          2s
dotnet-deployment-5c9d4cc4b9-cspqd   0/1     Terminating         0          34m
dotnet-deployment-5c9d4cc4b9-cspqd   0/1     Terminating         0          34m
dotnet-deployment-85f6446977-hw24v   1/1     Running             0          3s
dotnet-deployment-5c9d4cc4b9-httw6   1/1     Terminating         0          34m
dotnet-deployment-5c9d4cc4b9-httw6   0/1     Terminating         0          34m

El resultado anterior muestra la actualización progresiva a medida que ocurre. Primero, se inician los contenedores nuevos y, cuando se ejecutan, se finalizan los antiguos.

Verifica la app en ejecución

En este punto, la aplicación está actualizada y lista para recibir solicitudes de los usuarios. Como antes, se puede acceder a ella a través de un proxy.

$ kubectl proxy --port 8080
Starting to serve on 127.0.0.1:8080

Ahora, usa la Vista previa en la Web en Cloud Shell para acceder a la aplicación web.

Agrega lo siguiente a la URL que genera la Vista previa en la Web: /api/v1/namespaces/default/services/dotnet-service:8080/proxy/. El resultado final será similar al siguiente:

https://8080-cs-473655782854-default.us-central1.cloudshell.dev/api/v1/namespaces/default/services/dotnet-service:8080/proxy/

Confirma que el servicio de Kubernetes esté distribuyendo la carga

Actualice esta URL varias veces y observe que el Host cambia a medida que el Service balancea las cargas de las solicitudes en diferentes Pods. Compara los valores del host con la lista de Pods anterior para ver que todos los Pods estén recibiendo tráfico.

Escala verticalmente las instancias

Escalar apps en Kubernetes es fácil. El siguiente comando escalará la implementación hasta 6 instancias de la aplicación.

$ kubectl scale deployment dotnet-deployment --replicas 6
deployment.apps/dotnet-deployment scaled

Los nuevos Pods y su estado actual se pueden ver con este comando

kubectl get pod -w

Observa que, si actualizas la misma ventana del navegador, verás que el tráfico se está balanceando en todos los Pods nuevos.

8. ¡Felicitaciones!

En este lab, se validó una aplicación web de muestra de .NET Core en un entorno de desarrollador y, luego, se implementó en Kubernetes mediante GKE. Luego, la app se modificó para mostrar el nombre de host del contenedor en el que se ejecutaba. Luego, la implementación de Kubernetes se actualizó a la nueva versión y la aplicación se escaló verticalmente para demostrar cómo se distribuye la carga entre instancias adicionales.

Para obtener más información sobre .NET y Kubernetes, consulta estos instructivos. Estos conceptos se basan en lo aprendido en este lab, ya que presentan la malla de servicios de Istio para obtener patrones de enrutamiento y resiliencia más sofisticados.

9. Limpia

Para evitar costos no deseados, usa los siguientes comandos y borra el clúster y la imagen de contenedor que se crearon en este lab.

gcloud container clusters delete dotnet-cluster --zone ${DEFAULT_ZONE}
gcloud container images delete gcr.io/${PROJECT_ID}/aspnetapp:alpine