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

1. Descripción general

.NET Core de Microsoft es una versión de código abierto y multiplataforma de .NET que se puede ejecutar de forma nativa en contenedores. .NET Core está disponible en GitHub y lo mantienen Microsoft y la comunidad de .NET. En este lab, se implementa una app 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 los desarrolladores y, luego, se implementan en producción. En la primera parte del lab, se valida una app de .NET Core de ejemplo con un contenedor que se ejecuta en Cloud Shell. Una vez validada, la app se implementa en Kubernetes con GKE. En el lab, se incluyen los pasos para crear un clúster de GKE.

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

Diagrama de secuencia de la demostración

Costos

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

2. Configuración y requisitos

Requisitos previos

Para completar este lab, se requiere una cuenta y un proyecto de Google Cloud. Para obtener instrucciones más detalladas sobre cómo crear un proyecto nuevo, consulta este codelab.

En este lab, se usa Docker, que se ejecuta en Cloud Shell, que está disponible a través de 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 aparezca 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 siguiente sección, se crea un clúster de GKE con una configuración simple. 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, con los siguientes comandos, es posible identificar diferentes tipos de máquinas, zonas y hasta GPU (aceleradores).

  • Enumera los tipos de máquinas con este comando: gcloud compute machine-types list
  • Enumera las GPU con este comando: gcloud compute accelerator-types list
  • Enumera las zonas de procesamiento con este comando: gcloud compute zones list
  • Obtener ayuda sobre 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

Como en este lab se implementa la app de .NET Core en Kubernetes, es necesario crear un clúster. Usa el siguiente comando para crear un clúster de Kubernetes nuevo 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, se usan la zona que identificaste en la variable de entorno anterior y us-central1-b
    • NOTA: Esta lista no puede contener duplicados
  • --workload-pool establece Workload Identity para que las cargas de trabajo de GKE 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 para que se autentique en el clúster. Para ello, ejecuta 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 de forma local y confirma la funcionalidad deseada

En este lab, se usan las siguientes imágenes de contenedor del repositorio oficial de .NET en Docker Hub.

Ejecuta el contenedor de forma local para verificar la funcionalidad

En Cloud Shell, ejecuta el siguiente comando de Docker para verificar que Docker esté activo y funcionando correctamente, y que el contenedor de .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 la funcionalidad de la app web

También se puede validar una aplicación web de ejemplo en Cloud Shell. El siguiente comando de ejecución de Docker crea un contenedor nuevo que expone el puerto 80 y lo asigna al puerto 8080 de localhost. Recuerda que, en este caso, localhost se encuentra 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, se debe ver y validar 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 la "Vista previa en la Web"

Cloud Shell ofrece la 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.

Usa la "Vista previa en la Web" para ver las 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 el puerto que esté configurado para usar la vista previa en la Web).

Cloud Shell

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

https://8080-cs-754738286554-default.us-central1.cloudshell.dev/

Visualiza la aplicación de ejemplo de .NET con la vista previa web

Para ver la app de ejemplo que se inició en el último paso, inicia la vista previa en la Web y carga 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 aplícalo

En el siguiente paso, se 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 anteriormente. Primero, veamos los recursos de Deployment y confirmemos que el nuevo Deployment esté allí.

$ 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 por el Deployment anterior.

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

Por último, observa los Pods. El 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 Service en Kubernetes es un balanceador de cargas. Los extremos se determinan según las etiquetas de los Pods. De esta manera, en cuanto se agregan Pods nuevos al Deployment a través de la operación kubectl scale deployment anterior, los Pods resultantes están disponibles de inmediato para las solicitudes que controla 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
...

Es posible ver 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>

Observa 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 solicitudes enviadas al Service se balancearán entre todas las instancias (Pods). El valor Endpoints anterior muestra las IP de los Pods disponibles actualmente para este servicio. Compáralas con las IPs de los Pods que se mostraron anteriormente.

Verifica la app en ejecución

En este punto, la aplicación está activa y lista para las solicitudes de los usuarios. Para acceder a él, 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/. Se verá de la siguiente manera:

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

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

6. Modifica la app

En esta sección, se modificará la aplicación para mostrar el host en el que se ejecuta la instancia. Esto permitirá confirmar que el balanceo de cargas funciona 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

Como antes, se puede acceder a la app con la vista previa en la Web. Esta vez, el parámetro Host debería ser visible, como se muestra aquí:

Cloud Shell

Abre una nueva pestaña en Cloud Shell y ejecuta docker ps para ver que el ID del contenedor coincida con el valor de Host que se muestra más 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

Etiquetar y enviar la imagen para que esté disponible en Kubernetes

La imagen debe etiquetarse y enviarse 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. Con 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 en el que se guardó el archivo dotnet-app.yaml. Busca la siguiente línea en el archivo YAML:

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

Esto se debe cambiar para hacer referencia a la imagen de contenedor que se creó y se 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 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 supervisará todos los cambios a medida que ocurran.

$ 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

En el resultado anterior, se muestra la actualización progresiva a medida que se produce. Primero, se inician los contenedores nuevos y, cuando se están ejecutando, se finalizan los contenedores antiguos.

Verifica la app en ejecución

En este punto, la aplicación se actualiza y está lista para las solicitudes de los usuarios. Como antes, se puede acceder a él con 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/. Se verá de la siguiente manera:

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

Confirma que el Service de Kubernetes esté distribuyendo la carga

Actualiza esta URL varias veces y observa que el host cambia a medida que el Service balancea las cargas de las solicitudes entre diferentes Pods. Compara los valores de Host con la lista de Pods de arriba para ver que todos los Pods reciben tráfico.

Escala verticalmente las instancias

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

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

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

kubectl get pod -w

Observa que, cuando actualizas la misma ventana del navegador, se muestra que el tráfico ahora se balancea entre todos los Pods nuevos.

8. ¡Felicitaciones!

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

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

9. Limpia

Para evitar costos no deseados, usa los siguientes comandos para borrar 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