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

1. Visão geral

O Microsoft .NET Core é 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. Este laboratório implanta um aplicativo .NET Core conteinerizado no Google Kubernetes Engine (GKE).

Este laboratório segue um padrão típico de desenvolvimento. Nele, os aplicativos são desenvolvidos no ambiente local de um desenvolvedor e depois implantados na produção. Na primeira parte do laboratório, um exemplo de aplicativo .NET Core é validado usando um contêiner em execução no Cloud Shell. Depois da validação, 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 alteração é feita no aplicativo, que mostra o nome do host do contêiner que está executando essa instância do aplicativo. O aplicativo atualizado é validado no Cloud Shell, e a implantação é atualizada para usar a nova versão. A ilustração abaixo mostra a sequência das atividades deste laboratório:

Diagrama de sequência de demonstração

Custos

Se você executar este laboratório exatamente como foi escrito, os custos normais dos serviços a seguir serão aplicados

2. Configuração e requisitos

Pré-requisitos

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

Este laboratório usa o Docker em execução no Cloud Shell, disponível no console do Google Cloud e pré-configurado com várias ferramentas úteis, como gcloud e Docker. Confira abaixo como acessar o Cloud Shell. Clique no ícone do Cloud Shell no canto superior direito para exibi-lo no painel inferior da janela do console.

Cloud Shell

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

Este laboratório exige um cluster do Kubernetes. Na próxima seção, vamos criar um cluster do GKE com uma configuração simples. Nesta seção, mostramos alguns comandos gcloud que fornecem 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é mesmo GPUs (aceleradores).

  • Liste os tipos de máquina com este comando gcloud compute machine-types list.
  • Liste GPUs com este comando gcloud compute accelerator-types list
  • Liste as zonas do Compute com este comando gcloud compute zones list
  • Receba ajuda sobre qualquer comando gcloud gcloud container clusters --help
    • Por exemplo, isto fornece detalhes sobre a criação de um cluster do Kubernetes gcloud container clusters create --help

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

Prepare-se para criar o cluster do Kubernetes

No Cloud Shell, é necessário definir algumas variáveis de ambiente e configurar o cliente gcloud. Isso é feito com os comandos a seguir.

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}

Crie 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 novo 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 separadas por vírgulas. Neste caso, a zona que você identifica na variável de ambiente acima e us-central1-b são usadas
    • OBSERVAÇÃO: esta lista não pode conter cópias
  • O --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

Durante a criação do cluster, 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 usá-lo com o novo cluster recém-criado, ele precisa ser configurado para autenticar no cluster. Isso é feito com este 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 .NET oficial no hub do Docker.

Executar o contêiner localmente para verificar a funcionalidade

No Cloud Shell, verifique se o Docker está funcionando corretamente e se o contêiner .NET funciona conforme o esperado. Para isso, execute 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 app da Web

Um aplicativo da Web de exemplo também pode ser validado no Cloud Shell. O comando de execução do Docker abaixo cria um novo contêiner que expõe a porta 80 e o mapeia para a porta localhost 8080. Lembre-se de que localhost, neste 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 este é um app da Web, ele precisa ser visualizado e validado em um navegador. A próxima seção mostra como fazer isso no Cloud Shell usando a Visualização da Web.

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

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

Usar "Visualização da Web" para conferir os 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 de visualização da Web configurada para ser usada).

Cloud Shell

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

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

Usar a visualização da Web para ver o aplicativo de exemplo .NET

O app de exemplo que foi iniciado na última etapa agora pode ser acessado iniciando a Visualização da Web e carregando o URL fornecido. O código será semelhante a este:

Captura de tela do aplicativo .NET V1

5. Implantar no Kubernetes

Crie o arquivo YAML e aplique

A próxima etapa requer um arquivo YAML que descreva 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 que foram criados acima. Primeiro, confira os recursos de implantação e confirme se a nova implantação está lá.

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

Agora vamos conferir 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 deve haver três instâncias. O comando abaixo vai mostrar que há três instâncias. A opção -o wide foi adicionada para que os nós em que essas instâncias estão em execução 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>

Revisar o recurso de serviço

Um recurso de serviço 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 as solicitações processadas por esse serviço.

O comando a seguir deve mostrar 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 comando a seguir.

$ 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 carga balanceada em todas as instâncias (pods). O valor Endpoints acima mostra os IPs dos pods disponíveis atualmente para este serviço. Compare esses endereços com os IPs dos pods que foram gerados acima.

Verificar o app em execução

Neste ponto, o aplicativo estará ativo e pronto para solicitações dos usuários. Para acessá-lo, use um proxy. O comando a seguir cria um proxy local que aceita solicitações na porta 8080 e as transmite ao 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 código ao URL gerado pela visualização da Web: /api/v1/namespaces/default/services/dotnet-service:8080/proxy/. O resultado será parecido com este:

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

Parabéns pela implantação do app .NET Core no Google Kubernetes Engine. Em seguida, faremos uma alteração no app e reimplantá-lo.

6. Modificar o app

Nesta seção, o aplicativo será modificado para mostrar o host em que a instância está sendo executada. Assim, será possível 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 na Web. Desta vez, o parâmetro "Host" deve estar visível, conforme mostrado aqui:

Cloud Shell

Abra uma nova guia no Cloud Shell e execute docker ps para ver se o ID do contêiner corresponde ao valor "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

Marque e envie a imagem para que ela fique disponível para o Kubernetes

A imagem precisa ser marcada e enviada para que o Kubernetes seja capaz de extraí-la. Comece listando as imagens do 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, marque essa imagem e envie-a para o Google Container Registry. Usando o ID da IMAGEM acima, a aparência vai ficar assim:

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

7. Implante novamente o aplicativo atualizado

Editar o arquivo YAML

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

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

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

        image: gcr.io/PROJECT_ID/aspnetapp:alpine

Não se esqueça de modificá-la para usar seu PROJECT_ID. Ele vai ficar assim quando você terminar.

        image: gcr.io/myproject/aspnetapp:alpine

Aplique o arquivo YAML atualizado

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

Observe que o recurso de implantação aparece como atualizado e o de serviço mostra o estado inalterado. Os pods atualizados podem ser vistos como antes, com o comando kubectl get pod, mas, desta vez, adicionaremos o -w, que acompanhará todas as mudanças conforme 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 enquanto ela acontece. Primeiro, os contêineres novos são iniciados e, quando estão em execução, os contêineres antigos são encerrados.

Verificar o app em execução

Nesse ponto, o aplicativo está atualizado e pronto para solicitações dos usuários. Assim 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 código ao URL gerado pela visualização da Web: /api/v1/namespaces/default/services/dotnet-service:8080/proxy/. O resultado será parecido com este:

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

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

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

Escalonar instâncias verticalmente

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

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

Use este comando para conferir os novos pods e o estado atual deles

kubectl get pod -w

Observe que atualizar a mesma janela do navegador mostra que o tráfego está sendo balanceado em todos os novos pods.

8. Parabéns!

Neste laboratório, um aplicativo da Web de amostra .NET Core foi validado em um ambiente de desenvolvedor e, depois, implantado no Kubernetes usando o GKE. O aplicativo foi modificado para exibir 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 verticalmente para demonstrar como a carga é distribuída entre instâncias adicionais.

Para saber mais sobre o .NET e o Kubernetes, confira estes tutoriais. Eles se baseiam no que foi aprendido neste laboratório com a introdução do Istio Service Mesh para criar padrões mais sofisticados de roteamento e resiliência.

9. Limpar

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

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