Implantar e atualizar um app .NET Core no Google Kubernetes Engine

1. Visão geral

O .NET Core da Microsoft é uma versão de código aberto e multiplataforma do .NET que pode ser executada nativamente em contêineres. O .NET Core está disponível no GitHub e é mantido pela Microsoft e pela comunidade .NET. Neste laboratório, você vai implantar um app .NET Core em contêiner no Google Kubernetes Engine (GKE).

Este laboratório segue um padrão de desenvolvimento típico em que os aplicativos são desenvolvidos em um ambiente local para desenvolvedores e depois implantados na produção. Na primeira parte do laboratório, um app de exemplo do .NET Core é validado usando um contêiner em execução no Cloud Shell. Depois de validado, o app é implantado no Kubernetes usando o GKE. O laboratório inclui as etapas para criar um cluster do GKE.

Na segunda parte do laboratório, uma pequena mudança é feita no app que mostra o nome do host do contêiner que está executando essa instância do app. O aplicativo atualizado é validado no Cloud Shell, e a implantação é atualizada para usar a nova versão. A ilustração a seguir mostra a sequência de atividades neste laboratório:

Diagrama de sequência de demonstração

Custos

Se você executar este laboratório exatamente como está escrito, os custos normais dos seguintes serviços serão aplicados:

2. Configuração e requisitos

Pré-requisitos

Para concluir este laboratório, é necessário ter uma conta e um projeto do Google Cloud. Para instruções mais detalhadas sobre como criar um projeto, consulte este codelab.

Este laboratório usa o Docker em execução no Cloud Shell, que está disponível no console do Google Cloud e vem pré-configurado com muitas ferramentas úteis, como gcloud e Docker. Confira abaixo como acessar o Cloud Shell. Clique no ícone do Cloud Shell no canto superior direito para mostrá-lo no painel na parte de baixo da janela do console.

Cloud Shell

Opções alternativas de configuração para o cluster do GKE (opcional)

Este laboratório exige um cluster do Kubernetes. Na próxima seção, um cluster do GKE com uma configuração simples será criado. Nesta seção, mostramos alguns comandos gcloud que oferecem opções de configuração alternativas para usar ao criar um cluster do Kubernetes com o GKE. Por exemplo, usando os comandos abaixo, é possível identificar diferentes tipos de máquinas, zonas e até GPUs (aceleradores).

  • Liste os tipos de máquinas com este comando gcloud compute machine-types list
  • Liste as GPUs com este comando: gcloud compute accelerator-types list
  • Liste as zonas de computação com este comando: gcloud compute zones list
  • Receber ajuda com qualquer comando gcloud gcloud container clusters --help
    • Por exemplo, isso dá detalhes sobre como criar um cluster do Kubernetes gcloud container clusters create --help

Para uma lista completa de opções de configuração do GKE, consulte este documento.

Preparar a criação do cluster do Kubernetes

No Cloud Shell, é necessário definir algumas variáveis de ambiente e configurar o cliente gcloud. Isso é feito com os seguintes 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}

Criar um cluster do GKE

Como este laboratório implanta o app .NET Core no Kubernetes, é necessário criar um cluster. Use o comando a seguir para criar um cluster do Kubernetes no Google Cloud usando o 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 é o número de nós a serem adicionados por zona e pode ser escalonado posteriormente.
  • --node-locations é uma lista de zonas separada por vírgulas. Nesse caso, a zona identificada na variável de ambiente acima e us-central1-b são usadas
    • OBSERVAÇÃO: essa lista não pode conter duplicidades.
  • --workload-pool estabelece a identidade da carga de trabalho para que as cargas de trabalho do GKE possam acessar os serviços do Google Cloud.

Enquanto o cluster está sendo criado, o seguinte é exibido:

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

Configurar o kubectl

A CLI kubectl é a principal maneira de interagir com um cluster do Kubernetes. Para usar com o novo cluster que acabou de ser criado, ele precisa ser configurado para autenticar no cluster. Isso é feito com o seguinte comando.

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

Agora é possível usar kubectl para interagir com o cluster.

$ 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. Teste localmente e confirme a funcionalidade desejada

Este laboratório usa as seguintes imagens de contêiner do repositório oficial do .NET no Docker Hub.

Executar o contêiner localmente para verificar a funcionalidade

No Cloud Shell, verifique se o Docker está funcionando corretamente e se o contêiner do .NET funciona como esperado executando o seguinte comando do Docker:

$ 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

Confirmar a funcionalidade do web app

Um exemplo de aplicativo da Web também pode ser validado no Cloud Shell. O comando "docker run" abaixo cria um contêiner que expõe a porta 80 e a mapeia para a porta localhost 8080. Lembre-se de que localhost, nesse caso, está no 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 é um web app, ele precisa ser visualizado e validado em um navegador da Web. A próxima seção mostra como fazer isso no Cloud Shell usando a visualização na Web.

4. Acessar serviços do Cloud Shell usando a "Visualização na Web"

O Cloud Shell oferece a Visualização da Web, um recurso que permite usar um navegador para interagir com processos em execução na instância do Cloud Shell.

Usar a "Visualização da Web" para ver apps no Cloud Shell

No Cloud Shell, clique no botão de visualização da Web e escolha Visualizar na porta 8080 (ou qualquer porta que a visualização da Web esteja configurada para usar).

Cloud Shell

Isso vai abrir uma janela do navegador com um endereço como este:

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

Visualizar o aplicativo de exemplo do .NET usando a visualização da Web

O app de exemplo iniciado na última etapa pode ser visualizado ao iniciar a prévia da Web e carregar o URL fornecido. O código será semelhante a este:

Captura de tela do app .NET V1

5. Implantar no Kubernetes

Criar e aplicar o arquivo YAML

A próxima etapa exige um arquivo YAML que descreve dois recursos do Kubernetes: uma implantação e um serviço. Crie um arquivo chamado dotnet-app.yaml no Cloud Shell e adicione o seguinte conteúdo a ele.

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

Agora use kubectl para aplicar esse arquivo ao Kubernetes.

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

Observe as mensagens que indicam que os recursos desejados foram criados.

Conheça os recursos resultantes

Podemos usar a CLI kubectl para examinar os recursos criados acima. Primeiro, vamos analisar os recursos de implantação e confirmar se a nova implantação está lá.

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

Em seguida, confira os ReplicaSets. Deve haver um ReplicaSet criado pela implantação acima.

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

Por fim, confira os Pods. A implantação indicou que deveria haver três instâncias. O comando abaixo mostra que há três instâncias. A opção -o wide é adicionada para que os nós em que essas instâncias estão sendo executadas sejam mostrados.

$ 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>

Analisar o recurso "Service"

Um recurso Service no Kubernetes é um balanceador de carga. Os endpoints são determinados por rótulos nos pods. Dessa forma, assim que novos pods são adicionados à implantação pela operação kubectl scale deployment acima, os pods resultantes ficam imediatamente disponíveis para solicitações processadas por esse serviço.

O comando a seguir mostra o 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
...

É possível ver mais detalhes sobre o serviço com o seguinte 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>

Observe que o serviço é do tipo ClusterIP. Isso significa que qualquer pod no cluster pode resolver o nome do serviço, dotnet-service, para o endereço IP dele. As solicitações enviadas ao serviço terão balanceamento de carga em todas as instâncias (pods). O valor Endpoints acima mostra os IPs dos pods disponíveis no momento para esse serviço. Compare esses IPs com os dos pods que foram gerados acima.

Verificar o app em execução

Neste ponto, o aplicativo está ativo e pronto para solicitações do usuário. Para acessar, use um proxy. O comando a seguir cria um proxy local que aceita solicitações na porta 8080 e as transmite para o cluster do Kubernetes.

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

Agora use a visualização da Web no Cloud Shell para acessar o aplicativo da Web.

Adicione o seguinte ao URL gerado pela Visualização na Web: /api/v1/namespaces/default/services/dotnet-service:8080/proxy/. O resultado será algo parecido com isto:

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

Parabéns por implantar um app .NET Core no Google Kubernetes Engine. Em seguida, vamos fazer uma mudança no app e reimplantar.

6. Modificar o app

Nesta seção, o aplicativo será modificado para mostrar o host em que a instância está sendo executada. Isso vai permitir confirmar se o balanceamento de carga está funcionando e se os pods disponíveis estão respondendo conforme o esperado.

Conseguir o código-fonte

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

Atualizar o app para incluir o nome do host

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

Criar uma nova imagem de contêiner e testar localmente

Crie a nova imagem do contêiner com o código atualizado.

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

Como antes, teste o novo aplicativo localmente.

$ 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, o app pode ser acessado usando a visualização da Web. Desta vez, o parâmetro "Host" vai aparecer, como mostrado aqui:

Cloud Shell

Abra uma nova guia no Cloud Shell e execute docker ps para conferir se o ID do contêiner corresponde ao valor do host mostrado acima.

$ 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

Marcar e enviar a imagem para que ela fique disponível para o Kubernetes

A imagem precisa ser marcada e enviada para que o Kubernetes possa extraí-la. Comece listando as imagens de contêiner e identifique a imagem desejada.

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

Em seguida, adicione uma tag à imagem e envie-a para o Google Container Registry. Usando o ID da imagem acima, isso vai ficar assim:

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

7. Reimplante o aplicativo atualizado

Edite o arquivo YAML

Volte para o diretório em que o arquivo dotnet-app.yaml está salvo. Encontre a seguinte linha no arquivo YAML:

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

Isso precisa ser alterado para fazer referência à imagem do contêiner criada e enviada para gcr.io acima.

        image: gcr.io/PROJECT_ID/aspnetapp:alpine

Não se esqueça de modificar para usar seu PROJECT_ID. O resultado vai ficar assim:

        image: gcr.io/myproject/aspnetapp:alpine

Aplicar o arquivo YAML atualizado

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

O recurso "Implantação" mostra que foi atualizado, e o recurso "Serviço" mostra que não foi alterado. Os pods atualizados podem ser vistos como antes com o comando kubectl get pod, mas desta vez vamos adicionar o -w, que vai monitorar todas as mudanças à medida que elas acontecem.

$ 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

A saída acima mostra a atualização gradual à medida que ela acontece. Primeiro, os novos contêineres são iniciados e, quando estão em execução, os antigos são encerrados.

Verificar o app em execução

Nesse ponto, o aplicativo é atualizado e fica pronto para solicitações do usuário. Como antes, ele pode ser acessado usando um proxy.

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

Agora use a visualização da Web no Cloud Shell para acessar o aplicativo da Web.

Adicione o seguinte ao URL gerado pela Visualização na Web: /api/v1/namespaces/default/services/dotnet-service:8080/proxy/. O resultado será algo parecido com isto:

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

Confirmar se o serviço do Kubernetes está distribuindo a carga

Atualize esse URL várias vezes e observe que o host muda à medida que as solicitações são balanceadas por carga em diferentes pods pelo serviço. Compare os valores de host com a lista de pods acima para ver se todos os pods estão recebendo tráfego.

Escalonar verticalmente instâncias

Escalonar apps no Kubernetes é fácil. O comando a seguir vai escalonar a implantação para até seis instâncias do aplicativo.

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

Os novos pods e o estado atual deles podem ser vistos com este comando:

kubectl get pod -w

Ao atualizar a mesma janela do navegador, você vai notar que o tráfego agora está sendo balanceado em todos os novos pods.

8. Parabéns!

Neste laboratório, um aplicativo da Web de amostra do .NET Core foi validado em um ambiente de desenvolvimento e implantado no Kubernetes usando o GKE. Em seguida, o app foi modificado para mostrar o nome do host do contêiner em que estava sendo executado. A implantação do Kubernetes foi atualizada para a nova versão, e o app foi escalonado para demonstrar como a carga é distribuída em instâncias adicionais.

Para saber mais sobre .NET e Kubernetes, confira estes tutoriais. Eles se baseiam no que foi aprendido neste laboratório ao apresentar a malha de serviço do Istio para padrões de roteamento e resiliência mais sofisticados.

9. Limpar

Para evitar custos não intencionais, use os comandos a seguir para excluir o cluster e a imagem do contêiner criados neste laboratório.

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