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 se puede ejecutar de forma nativa en contenedores. .NET Core está disponible en GitHub y Microsoft y la comunidad de .NET lo mantienen. En este lab, se implementa una app de .NET Core 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 el entorno local de los desarrolladores y, luego, se implementan en producción. En la primera parte del lab, se valida una app de ejemplo de .NET Core con un contenedor que se ejecuta en Cloud Shell. Una vez validada, la app se implementa en Kubernetes con 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 app que muestra el nombre de host del contenedor que ejecuta esa instancia de la app. Luego, se valida la aplicación actualizada en Cloud Shell y se actualiza la implementación para que use 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 se indica, se aplicarán los costos normales de los siguientes servicios:

2. Configuración y requisitos

Requisitos previos

Para completar este lab, se requieren 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 creará 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 GPUs (aceleradores).

  • Enumera los tipos de máquinas con este comando gcloud compute machine-types list
  • Enumera las GPUs con este comando gcloud compute accelerator-types list
  • Enumera las zonas de procesamiento con este comando: gcloud compute zones list
  • Obtén ayuda sobre cualquier comando de gcloud gcloud container clusters --help
    • Por ejemplo, esto proporciona detalles sobre la creación de 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.

Preparación para crear el clúster de Kubernetes

En Cloud Shell, es necesario configurar algunas variables de entorno y 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

Dado que este lab implementa la app de .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 ajustar más adelante.
  • --node-locations es una lista de zonas separadas por comas. En este caso, se usan la zona que identificas en la variable de entorno anterior y us-central1-b.
    • NOTA: Esta lista no puede contener duplicados.
  • --workload-pool establece la identidad de carga de trabajo 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 principal forma 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. 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ías poder 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. Prueba 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 se esté ejecutando correctamente y que el contenedor de .NET funcione según lo esperado:

$ 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 muestra en Cloud Shell. El siguiente comando docker run crea un contenedor nuevo que expone el puerto 80 y lo asigna al puerto localhost 8080. Recuerda que localhost en este caso 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

Dado que 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 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.

Usa la “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 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 la siguiente:

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

Cómo ver la aplicación de ejemplo de .NET con la vista previa web

Ahora puedes ver la app de ejemplo que se inició en el último paso. Para ello, inicia la versión preliminar 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: una implementación y un servicio. 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 la nueva implementación 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. El Deployment anterior debería haber creado un ReplicaSet.

$ 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 agregó 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 de Service

Un recurso 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 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 controla ese Servicio.

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 ver más detalles sobre el servicio 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 de 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 servicio se balancearán en todas las instancias (Pods). El valor Endpoints anterior muestra las IPs de los Pods disponibles actualmente para este servicio. Compara estas direcciones IP con las de los Pods que se generaron 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 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 de 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 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, haremos 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 según lo previsto.

Obtén el código fuente

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

Actualiza la app para incluir 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 nueva aplicación 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 con la Vista previa en la Web. Esta vez, el parámetro Host debería estar 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 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 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

A continuación, etiqueta esa imagen y envíala a Google Container Registry. Con el ID de la 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 debe cambiarse 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 el -w, que supervisará todos los cambios a medida que se produzcan.

$ 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 se realiza. 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. Al igual que 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 de 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 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 servicio de Kubernetes esté distribuyendo la carga

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

Cómo escalar 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, se muestra que el tráfico ahora se balancea 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 con GKE. Luego, se modificó la app para que mostrara el nombre de host del contenedor en el que se ejecutaba. Luego, se actualizó la implementación de Kubernetes a la versión nueva y se escaló la app para demostrar cómo se distribuye la carga en instancias adicionales.

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

9. Limpia

Para evitar costos no deseados, usa los siguientes comandos para borrar el clúster y la imagen del 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