One place for hosting & domains

      Archives hassan latif

      How To Do Canary Deployments With Istio and Kubernetes


      Introduction

      When introducing new versions of a service, it is often desirable to shift a controlled percentage of user traffic to a newer version of the service in the process of phasing out the older version. This technique is called a canary deployment.

      Kubernetes cluster operators can orchestrate canary deployments natively using labels and Deployments. This technique has certain limitations, however: traffic distribution and replica counts are coupled, which in practice means replica ratios must be controlled manually in order to limit traffic to the canary release. In other words, to direct 10% of traffic to a canary deployment, you would need to have a pool of ten pods, with one pod receiving 10% of user traffic, and the other nine receiving the rest.

      Deploying with an Istio service mesh can address this issue by enabling a clear separation between replica counts and traffic management. The Istio mesh allows fine-grained traffic control that decouples traffic distribution and management from replica scaling. Instead of manually controlling replica ratios, you can define traffic percentages and targets, and Istio will manage the rest.

      In this tutorial, you will create a canary deployment using Istio and Kubernetes. You will deploy two versions of a demo Node.js application, and use Virtual Service and Destination Rule resources to configure traffic routing to both the newer and older versions. This will be a good starting point to build out future canary deployments with Istio.

      Prerequisites

      Note: We highly recommend a cluster with at least 8GB of available memory and 4vCPUs for this setup. This tutorial will use three of DigitalOcean’s standard 4GB/2vCPU Droplets as nodes.

      Step 1 — Packaging the Application

      In the prerequisite tutorial, How To Install and Use Istio With Kubernetes, you created a node-demo Docker image to run a shark information application and pushed this image to Docker Hub. In this step, you will create another image: a newer version of the application that you will use for your canary deployment.

      Our original demo application emphasized some friendly facts about sharks on its Shark Info page:

      Shark Info Page

      But we have decided in our new canary version to emphasize some scarier facts:

      Scary Shark Info Page

      Our first step will be to clone the code for this second version of our application into a directory called node_image. Using the following command, clone the nodejs-canary-app repository from the DigitalOcean Community GitHub account. This repository contains the code for the second, scarier version of our application:

      • git clone https://github.com/do-community/nodejs-canary-app.git node_image

      Navigate to the node_image directory:

      This directory contains files and folders for the newer version of our shark information application, which offers users information about sharks, like the original application, but with an emphasis on scarier facts. In addition to the application files, the directory contains a Dockerfile with instructions for building a Docker image with the application code. For more information about the instructions in the Dockerfile, see Step 3 of How To Build a Node.js Application with Docker.

      To test that the application code and Dockerfile work as expected, you can build and tag the image using the docker build command, and then use the image to run a demo container. Using the -t flag with docker build will allow you to tag the image with your Docker Hub username so that you can push it to Docker Hub once you've tested it.

      Build the image with the following command:

      • docker build -t your_dockerhub_username/node-demo-v2 .

      The . in the command specifies that the build context is the current directory. We've named the image node-demo-v2, to reference the node-demo image we created in How To Install and Use Istio With Kubernetes.

      Once the build process is complete, you can list your images with docker images:

      You will see the following output confirming the image build:

      Output

      REPOSITORY TAG IMAGE ID CREATED SIZE your_dockerhub_username/node-demo-v2 latest 37f1c2939dbf 5 seconds ago 77.6MB node 10-alpine 9dfa73010b19 2 days ago 75.3MB

      Next, you'll use docker run to create a container based on this image. We will include three flags with this command:

      • -p: This publishes the port on the container and maps it to a port on our host. We will use port 80 on the host, but you should feel free to modify this as necessary if you have another process running on that port. For more information about how this works, see this discussion in the Docker docs on port binding.
      • -d: This runs the container in the background.
      • --name: This allows us to give the container a customized name.

      Run the following command to build the container:

      • docker run --name node-demo-v2 -p 80:8080 -d your_dockerhub_username/node-demo-v2

      Inspect your running containers with docker ps:

      You will see output confirming that your application container is running:

      Output

      CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 49a67bafc325 your_dockerhub_username/node-demo-v2 "docker-entrypoint.s…" 8 seconds ago Up 6 seconds 0.0.0.0:80->8080/tcp node-demo-v2

      You can now visit your server IP in your browser to test your setup: http://your_server_ip. Your application will display the following landing page:

      Application Landing Page

      Click on the Get Shark Info button to get to the scarier shark information:

      Scary Shark Info Page

      Now that you have tested the application, you can stop the running container. Use docker ps again to get your CONTAINER ID:

      Output

      CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 49a67bafc325 your_dockerhub_username/node-demo-v2 "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:80->8080/tcp node-demo-v2

      Stop the container with docker stop. Be sure to replace the CONTAINER ID listed here with your own application CONTAINER ID:

      Now that you have tested the image, you can push it to Docker Hub. First, log in to the Docker Hub account you created in the prerequisites:

      • docker login -u your_dockerhub_username

      When prompted, enter your Docker Hub account password. Logging in this way will create a ~/.docker/config.json file in your non-root user's home directory with your Docker Hub credentials.

      Push the application image to Docker Hub with the docker push command. Remember to replace your_dockerhub_username with your own Docker Hub username:

      • docker push your_dockerhub_username/node-demo-v2

      You now have two application images saved to Docker Hub: the node-demo image, and node-demo-v2. We will now modify the manifests you created in the prerequisite tutorial How To Install and Use Istio With Kubernetes to direct traffic to the canary version of your application.

      Step 2 — Modifying the Application Deployment

      In How To Install and Use Istio With Kubernetes, you created an application manifest with specifications for your application Service and Deployment objects. These specifications describe each object's desired state. In this step, you will add a Deployment for the second version of your application to this manifest, along with version labels that will enable Istio to manage these resources.

      When you followed the setup instructions in the prerequisite tutorial, you created a directory called istio_project and two yaml manifests: node-app.yaml, which contains the specifications for your Service and Deployment objects, and node-istio.yaml, which contains specifications for your Istio Virtual Service and Gateway resources.

      Navigate to the istio_project directory now:

      Open node-app.yaml with nano or your favorite editor to make changes to your application manifest:

      Currently, the file looks like this:

      ~/node-istio.yaml

      apiVersion: v1
      kind: Service
      metadata:
        name: nodejs
        labels: 
          app: nodejs
      spec:
        selector:
          app: nodejs
        ports:
        - name: http
          port: 8080 
      ---
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: nodejs
        labels:
          version: v1
      spec:
        replicas: 1
        selector:
          matchLabels:
            app: nodejs
        template:
          metadata:
            labels:
              app: nodejs
              version: v1
          spec:
            containers:
            - name: nodejs
              image: your_dockerhub_username/node-demo
              ports:
              - containerPort: 8080
      

      For a full explanation of this file's contents, see Step 3 of How To Install and Use Istio With Kubernetes.

      We have already included version labels in our Deployment metadata and template fields, following Istio's recommendations for Pods and Services. Now we can add specifications for a second Deployment object, which will represent the second version of our application, and make a quick modification to the name of our first Deployment object.

      First, change the name of your existing Deployment object to nodejs-v1:

      ~/node-istio.yaml

      ...
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: nodejs-v1
        labels:
          version: v1
      ...
      

      Next, below the specifications for this Deployment, add the specifications for your second Deployment. Remember to add the name of your own image to the image field:

      ~/node-istio.yaml

      ...
      ---
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: nodejs-v2
        labels:
          version: v2
      spec:
        replicas: 1
        selector:
          matchLabels:
            app: nodejs
        template:
          metadata:
            labels:
              app: nodejs
              version: v2
          spec:
            containers:
            - name: nodejs
              image: your_dockerhub_username/node-demo-v2
              ports:
              - containerPort: 8080
      

      Like the first Deployment, this Deployment uses a version label to specify the version of the application that corresponds to this Deployment. In this case, v2 will distinguish the application version associated with this Deployment from v1, which corresponds to our first Deployment.

      We've also ensured that the Pods managed by the v2 Deployment will run the node-demo-v2 canary image, which we built in the previous Step.

      Save and close the file when you are finished editing.

      With your application manifest modified, you can move on to making changes to your node-istio.yaml file.

      Step 3 — Weighting Traffic with Virtual Services and Adding Destination Rules

      In How To Install and Use Istio With Kubernetes, you created Gateway and Virtual Service objects to allow external traffic into the Istio mesh and route it to your application Service. Here, you will modify your Virtual Service configuration to include routing to your application Service subsets — v1 and v2. You will also add a Destination Rule to define additional, version-based policies to the routing rules you are applying to your nodejs application Service.

      Open the node-istio.yaml file:

      Currently, the file looks like this:

      ~/istio_project/node-istio.yaml

      apiVersion: networking.istio.io/v1alpha3
      kind: Gateway
      metadata:
        name: nodejs-gateway
      spec:
        selector:
          istio: ingressgateway 
        servers:
        - port:
            number: 80
            name: http
            protocol: HTTP
          hosts:
          - "*"
      ---
      apiVersion: networking.istio.io/v1alpha3
      kind: VirtualService
      metadata:
        name: nodejs
      spec:
        hosts:
        - "*"
        gateways:
        - nodejs-gateway
        http:
        - route:
          - destination:
              host: nodejs
      

      For a complete explanation of the specifications in this manifest, see Step 4 of How To Install and Use Istio With Kubernetes.

      Our first modification will be to the Virtual Service. Currently, this resource routes traffic entering the mesh through our nodejs-gateway to our nodejs application Service. What we would like to do is configure a routing rule that will send 80% of traffic to our original application, and 20% to the newer version. Once we are satisfied with the canary's performance, we can reconfigure our traffic rules to gradually send all traffic to the newer application version.

      Instead of routing to a single destination, as we did in the original manifest, we will add destination fields for both of our application subsets: the original version (v1) and the canary (v2).

      Make the following additions to the Virtual Service to create this routing rule:

      ~/istio_project/node-istio.yaml

      ...
      apiVersion: networking.istio.io/v1alpha3
      kind: VirtualService
      metadata:
        name: nodejs
      spec:
        hosts:
        - "*"
        gateways:
        - nodejs-gateway
        http:
        - route:
          - destination:
              host: nodejs
              subset: v1
            weight: 80
          - destination:
              host: nodejs
              subset: v2
            weight: 20
      

      The policy that we have added includes two destinations: the subset of our nodejs Service that is running the original version of our application, v1, and the subset that is running the canary, v2. Subset one will receive 80% of incoming traffic, while the canary will receive 20%.

      Next, we will add a Destination Rule that will apply rules to incoming traffic after that traffic has been routed to the appropriate Service. In our case, we will configure subset fields to send traffic to Pods with the appropriate version labels.

      Add the following code below your Virtual Service definition:

      ~/istio_project/node-istio.yaml

      ...
      ---
      apiVersion: networking.istio.io/v1alpha3
      kind: DestinationRule
      metadata:
        name: nodejs
      spec:
        host: nodejs
        subsets:
        - name: v1
          labels:
            version: v1
        - name: v2
          labels:
            version: v2
      

      Our Rule ensures that traffic to our Service subsets, v1 and v2, reaches Pods with the appropriate labels: version: v1 and version: v2. These are the labels that we included in our application Deployment specs.

      If we wanted, however, we could also apply specific traffic policies at the subset level, enabling further specificity in our canary deployments. For additional information about defining traffic policies at this level, see the official Istio documentation.

      Save and close the file when you have finished editing.

      With your application manifests revised, you are ready to apply your configuration changes and examine your application traffic data using the Grafana telemetry addon.

      Step 4 — Applying Configuration Changes and Accessing Traffic Data

      The application manifests are updated, but we still need to apply these changes to our Kubernetes cluster. We'll use the kubectl apply command to apply our changes without completely overwriting the existing configuration. After doing this, you will be able to generate some requests to your application and look at the associated data in your Istio Grafana dashboards.

      Apply your configuration to your application Service and Deployment objects:

      • kubectl apply -f node-app.yaml

      You will see the following output:

      Output

      service/nodejs unchanged deployment.apps/nodejs-v1 created deployment.apps/nodejs-v2 created

      Next, apply the configuration updates you've made to node-istio.yaml, which include the changes to the Virtual Service and the new Destination Rule:

      • kubectl apply -f node-istio.yaml

      You will see the following output:

      Output

      gateway.networking.istio.io/nodejs-gateway unchanged virtualservice.networking.istio.io/nodejs configured destinationrule.networking.istio.io/nodejs created

      You are now ready to generate traffic to your application. Before doing that, however, first check to be sure that you have the grafana Service running:

      • kubectl get svc -n istio-system | grep grafana

      Output

      grafana ClusterIP 10.245.233.51 <none> 3000/TCP 4d2h

      Also check for the associated Pods:

      • kubectl get svc -n istio-system | grep grafana

      Output

      grafana-67c69bb567-jpf6h 1/1 Running 0 4d2h

      Finally, check for the grafana-gateway Gateway and grafana-vs Virtual Service:

      • kubectl get gateway -n istio-system | grep grafana

      Output

      grafana-gateway 3d5h
      • kubectl get virtualservice -n istio-system | grep grafana

      Output

      grafana-vs [grafana-gateway] [*] 4d2h

      If you don't see output from these commands, check Steps 2 and 5 of How To Install and Use Istio With Kubernetes, which discuss how to enable the Grafana telemetry addon when installing Istio and how to enable HTTP access to the Grafana Service.

      You can now access your application in the browser. To do this, you will need the external IP associated with your istio-ingressgateway Service, which is a LoadBalancer Service type. We matched our nodejs-gateway Gateway with this controller when writing our Gateway manifest in How To Install and Use Istio With Kubernetes. For more detail on the Gateway manifest, see Step 4 of that tutorial.

      Get the external IP for the istio-ingressgateway Service with the following command:

      • kubectl get svc -n istio-system

      You will see output like the following:

      Output

      NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE grafana ClusterIP 10.245.85.162 <none> 3000/TCP 42m istio-citadel ClusterIP 10.245.135.45 <none> 8060/TCP,15014/TCP 42m istio-galley ClusterIP 10.245.46.245 <none> 443/TCP,15014/TCP,9901/TCP 42m istio-ingressgateway LoadBalancer 10.245.171.39 ingressgateway_ip 15020:30707/TCP,80:31380/TCP,443:31390/TCP,31400:31400/TCP,15029:30285/TCP,15030:31668/TCP,15031:32297/TCP,15032:30853/TCP,15443:30406/TCP 42m istio-pilot ClusterIP 10.245.56.97 <none> 15010/TCP,15011/TCP,8080/TCP,15014/TCP 42m istio-policy ClusterIP 10.245.206.189 <none> 9091/TCP,15004/TCP,15014/TCP 42m istio-sidecar-injector ClusterIP 10.245.223.99 <none> 443/TCP 42m istio-telemetry ClusterIP 10.245.5.215 <none> 9091/TCP,15004/TCP,15014/TCP,42422/TCP 42m prometheus ClusterIP 10.245.100.132 <none> 9090/TCP 42m

      The istio-ingressgateway should be the only Service with the TYPE LoadBalancer, and the only Service with an external IP.

      Navigate to this external IP in your browser: http://ingressgateway_ip.

      You should see the following landing page:

      Application Landing Page

      Click on Get Shark Info button. You will see one of two shark information pages:

      Shark Info Page

      Scary Shark Info Page

      Click refresh on this page a few times. You should see the friendlier shark information page more often than the scarier version.

      Once you have generated some load by refreshing five or six times, you can head over to your Grafana dashboards.

      In your browser, navigate to the following address, again using your istio-ingressgateway external IP and the port that's defined in the Grafana Gateway manifest: http://ingressgateway_ip:15031.

      You will see the following landing page:

      Grafana Home Dash

      Clicking on Home at the top of the page will bring you to a page with an istio folder. To get a list of dropdown options, click on the istio folder icon:

      Istio Dash Options Dropdown Menu

      From this list of options, click on Istio Service Dashboard.

      This will bring you to a landing page with another dropdown menu:

      Service Dropdown in Istio Service Dash

      Select nodejs.default.svc.cluster.local from the list of available options.

      If you navigate down to the Service Workloads section of the page, you will be able to look at Incoming Requests by Destination And Response Code:

      Service Workloads Dashboards

      Here, you will see a combination of 200 and 304 HTTP response codes, indicating successful OK and Not Modified responses. The responses labeled nodejs-v1 should outnumber the responses labeled nodejs-v2, indicating that incoming traffic is being routed to our application subsets following the parameters we defined in our manifests.

      Conclusion

      In this tutorial, you deployed a canary version of a demo Node.js application using Istio and Kubernetes. You created Virtual Service and Destination Rule resources that together allowed you to send 80% of your traffic to your original application service, and 20% to the newer version. Once you are satisfied with the performance of the newer application version, you can update your configuration settings as desired.

      For more information about traffic management in Istio, see the related high-level overview in the documentation, as well as specific examples that use Istio's bookinfo and helloworld sample applications.



      Source link

      Criando Containers Otimizados para o Kubernetes


      Introdução

      Imagens de container são o formato de empacotamento principal para a definição de aplicações no Kubernetes. Usadas como base para pods e outros objetos, as imagens desempenham um papel importante ao aproveitar os recursos do Kubernetes para executar aplicações com eficiência na plataforma. Imagens bem projetadas são seguras, altamente eficientes e focadas. Elas são capazes de reagir a dados de configuração ou instruções fornecidas pelo Kubernetes e também implementar endpoints que o sistema de orquestração usa para entender o estado interno da aplicação.

      Neste artigo, vamos apresentar algumas estratégias para criar imagens de alta qualidade e discutir algumas metas gerais para ajudar a orientar suas decisões ao containerizar aplicações. Vamos nos concentrar na criação de imagens destinadas a serem executadas no Kubernetes, mas muitas das sugestões se aplicam igualmente à execução de containers em outras plataformas de orquestração ou em outros contextos.

      Características de Imagens de Container Eficientes

      Antes de passarmos por ações específicas a serem tomadas ao criar imagens de container, falaremos sobre o que torna boa uma imagem de container. Quais devem ser seus objetivos ao projetar novas imagens? Quais características e quais comportamentos são mais importantes?

      Algumas qualidades que podem ser indicadas são:

      Um propósito único e bem definido

      Imagens de container devem ter um único foco discreto. Evite pensar em imagens de container como máquinas virtuais, onde pode fazer sentido agrupar funcionalidades relacionadas. Em vez disso, trate suas imagens de container como utilitários Unix, mantendo um foco estrito em fazer bem uma pequena coisa. As aplicações podem ser coordenadas fora do escopo do container para compor funcionalidades complexas.

      Design genérico com a capacidade de injetar configuração em tempo de execução

      Imagens de container devem ser projetadas com a reutilização em mente quando possível. Por exemplo, a capacidade de ajustar a configuração em tempo de execução geralmente é necessária para atender aos requisitos básicos, como testar suas imagens antes de fazer o deploy em produção. Imagens pequenas e genéricas podem ser combinadas em diferentes configurações para modificar o comportamento sem criar novas imagens.

      Tamanho pequeno da imagem

      Imagens menores têm vários benefícios em ambientes em cluster, como o Kubernetes. Elas baixam rapidamente para novos nodes e geralmente têm um conjunto menor de pacotes instalados, o que pode melhorar a segurança. As imagens de container reduzidas simplificam o debug de problemas, minimizando a quantidade de software envolvida.

      Estado gerenciado externamente

      Containers em ambientes clusterizados experimentam um ciclo de vida muito volátil, incluindo desligamentos planejados e não planejados devido à escassez de recursos, dimensionamento ou falhas de node. Para manter a consistência, auxiliar na recuperação e na disponibilidade de seus serviços e evitar a perda de dados, é essencial armazenar o estado da aplicação em um local estável fora do container.

      Fácil de entender

      É importante tentar manter as imagens de container tão simples e fáceis de entender quanto possível. Ao solucionar problemas, a capacidade de raciocinar facilmente sobre o problema exibindo a configuração da imagem do container ou testando o comportamento dele pode ajudá-lo a alcançar uma resolução mais rapidamente. Pensar em imagens de container como um formato de empacotamento para sua aplicação, em vez de uma configuração de máquina, pode ajudá-lo a encontrar o equilíbrio certo.

      Siga as práticas recomendadas do software em container

      As imagens devem ter como objetivo trabalhar dentro do modelo de container, em vez de agir contra ele. Evite implementar práticas convencionais de administração de sistema, como incluir sistemas init completos e aplicações como daemon. Faça o log para a saída padrão, para que o Kubernetes possa expor os dados aos administradores, em vez de usar um daemon de log interno. Cada um desses itens difere das melhores práticas para sistemas operacionais completos.

      Aproveite totalmente os recursos do Kubernetes

      Além de estar em conformidade com o modelo de container, é importante entender e reconciliar o ambiente e as ferramentas que o Kubernetes fornece. Por exemplo, fornecer endpoints para verificações de prontidão e disponibilidade ou ajustar a operação com base nas alterações na configuração ou no ambiente pode ajudar suas aplicações a usar o ambiente de deploy dinâmico do Kubernetes a seu favor.

      Agora que estabelecemos algumas das qualidades que definem imagens de container altamente funcionais, podemos mergulhar mais fundo em estratégias que ajudam você a atingir essas metas.

      Reutilizar Camadas de Base Compartilhadas Mínimas

      Podemos começar examinando os recursos a partir dos quais as imagens de container são criadas: imagens de base. Cada imagem de container é construída ou a partir de uma imagem pai, uma imagem usada como ponto de partida ou da camada abstrata scratch, uma camada de imagem vazia sem sistema de arquivos. Uma imagem de base é uma imagem de container que serve como fundação para futuras imagens, através da definição do sistema operacional básico e do fornecimento da funcionalidade principal. As imagens são compostas por uma ou mais camadas de imagem construídas umas sobre as outras para formar uma imagem final.

      Nenhum utilitário padrão ou sistema de arquivos está disponível ao trabalhar diretamente a partir do scratch, o que significa que você só tem acesso a funcionalidades extremamente limitadas. Embora as imagens criadas diretamente a partir do scratch possam ser muito simples e minimalistas, seu objetivo principal é definir imagens de base. Normalmente, você deseja construir suas imagens de container sobre uma imagem pai que configura um ambiente básico no qual suas aplicações são executadas, para que você não precise construir um sistema completo para cada imagem.

      Embora existam imagens base para uma variedade de distribuições Linux, é melhor ser deliberado sobre quais sistemas você escolhe. Cada nova máquina terá que baixar a imagem principal e as camadas complementares que você adicionou. Para imagens grandes, isso pode consumir uma quantidade significativa de largura de banda e aumentar significativamente o tempo de inicialização de seus containers em sua primeira execução. Não há como reduzir uma imagem pai usada como downstream no processo de criação de containers, portanto, começar com uma imagem pai mínima é uma boa ideia.

      Ambientes ricos em recursos, como o Ubuntu, permitem que sua aplicação seja executada em um ambiente com o qual você esteja familiarizado, mas há algumas desvantagens a serem consideradas. As imagens do Ubuntu (e imagens de distribuição convencionais semelhantes) tendem a ser relativamente grandes (acima de 100 MB), o que significa que quaisquer imagens de container construídas a partir delas herdarão esse peso.

      O Alpine Linux é uma alternativa popular para imagens de base porque ele compacta com sucesso muitas funcionalidades em uma imagem de base muito pequena (~ 5MB). Ele inclui um gerenciador de pacotes com repositórios consideráveis e possui a maioria dos utilitários padrão que você esperaria de um ambiente Linux mínimo.

      Ao projetar suas aplicações, é uma boa ideia tentar reutilizar o mesmo pai para cada imagem. Quando suas imagens compartilham um pai, as máquinas que executam seus containers baixam a camada pai apenas uma vez. Depois disso, elas só precisarão baixar as camadas que diferem entre suas imagens. Isso significa que, se você tiver recursos ou funcionalidades comuns que gostaria de incorporar em cada imagem, criar uma imagem pai comum para herdar talvez seja uma boa ideia. Imagens que compartilham uma linhagem ajudam a minimizar a quantidade de dados extras que você precisa baixar em novos servidores.

      Gerenciando Camadas de Container

      Depois que você selecionou uma imagem pai, você pode definir sua imagem de container acrescentando software adicional, copiando arquivos, expondo portas e escolhendo processos para serem executados. Certas instruções no arquivo de configuração da imagem (um Dockerfile se você estiver usando o Docker) adicionarão camadas complementares à sua imagem.

      Por muitas das mesmas razões mencionadas na seção anterior, é importante estar ciente de como você adiciona camadas às suas imagens devido ao tamanho resultante, à herança e à complexidade do runtime. Para evitar a criação de imagens grandes e de difícil controle é importante desenvolver um bom entendimento de como as camadas de container interagem, como o mecanismo de criação faz o cache das camadas e como diferenças sutis em instruções semelhantes podem ter um grande impacto nas imagens que você cria.

      Entendendo as Camadas de Imagem e Construindo o Cache

      O Docker cria uma nova camada de imagem toda vez que executa as instruções RUN, COPY ou ADD. Se você construir a imagem novamente, o mecanismo de construção verificará cada instrução para ver se ela possui uma camada de imagem armazenada em cache para a operação. Se ele encontrar uma correspondência no cache, ele usará a camada de imagem existente em vez de executar a instrução novamente e reconstruir a camada.

      Esse processo pode reduzir significativamente os tempos de criação, mas é importante entender o mecanismo usado para evitar possíveis problemas. Para instruções de cópia de arquivos como COPY e ADD, o Docker compara os checksums dos arquivos para ver se a operação precisa ser executada novamente. Para instruções RUN, o Docker verifica se possui uma camada de imagem existente armazenada em cache para aquela sequência de comandos específica.

      Embora não seja imediatamente óbvio, esse comportamento pode causar resultados inesperados se você não for cuidadoso. Um exemplo comum disso é a atualização do índice de pacotes local e a instalação de pacotes em duas etapas separadas. Estaremos usando o Ubuntu para este exemplo, mas a premissa básica se aplica igualmente bem às imagens de base para outras distribuições:

      Dockerfile de exemplo de instalação de pacotes

      FROM ubuntu:18.04
      RUN apt -y update
      RUN apt -y install nginx
      . . .
      

      Aqui, o índice de pacotes local é atualizado em uma instrução RUN (apt -y update) e o Nginx é instalado em outra operação. Isso funciona sem problemas quando é usado pela primeira vez. No entanto, se o Dockerfile for atualizado posteriormente para instalar um pacote adicional, pode haver problemas:

      Dockerfile de exemplo de instalação de pacotes

      FROM ubuntu:18.04
      RUN apt -y update
      RUN apt -y install nginx php-fpm
      . . .
      

      Nós adicionamos um segundo pacote ao comando de instalação executado pela segunda instrução. Se uma quantidade significativa de tempo tiver passado desde a criação da imagem anterior, a nova compilação poderá falhar. Isso ocorre porque a instrução de atualização de índice de pacotes (RUN apt -y update) não foi alterada, portanto, o Docker reutiliza a camada de imagem associada a essa instrução. Como estamos usando um índice de pacotes antigo, a versão do pacote php-fpm que temos em nossos registros locais pode não estar mais nos repositórios, resultando em um erro quando a segunda instrução é executada.

      Para evitar esse cenário, certifique-se de consolidar quaisquer etapas que sejam interdependentes em uma única instrução RUN para que o Docker reexecute todos os comandos necessários quando ocorrer uma mudança:

      Dockerfile de exemplo de instalação de pacotes

      FROM ubuntu:18.04
      RUN apt -y update && apt -y install nginx php-fpm
      . . .
      

      A instrução agora atualiza o cache do pacotes local sempre que a lista de pacotes é alterada.

      Reduzindo o Tamanho da Camada de Imagem Ajustando Instruções RUN

      O exemplo anterior demonstra como o comportamento do cache do Docker pode subverter as expectativas, mas há algumas outras coisas que devem ser lembradas com relação à maneira como as instruções RUN interagem com o sistema de camadas do Docker. Como mencionado anteriormente, no final de cada instrução RUN, o Docker faz o commit das alterações como uma camada de imagem adicional. A fim de exercer controle sobre o escopo das camadas de imagens produzidas, você pode limpar arquivos desnecessários no ambiente final que serão comitados prestando atenção aos artefatos introduzidos pelos comandos que você executa.

      Em geral, o encadeamento de comandos em uma única instrução RUN oferece um grande controle sobre a camada que será gravada. Para cada comando, você pode configurar o estado da camada (apt -y update), executar o comando principal (apt install -y nginx php-fpm) e remover quaisquer artefatos desnecessários para limpar o ambiente antes de ser comitado. Por exemplo, muitos Dockerfiles encadeiam rm -rf /var/lib/apt/lists/* ao final dos comandos apt, removendo os índices de pacotes baixados, para reduzir o tamanho final da camada:

      Dockerfile de exemplo de instalação de pacotes

      FROM ubuntu:18.04
      RUN apt -y update && apt -y install nginx php-fpm && rm -rf /var/lib/apt/lists/*
      . . .
      

      Para reduzir ainda mais o tamanho das camadas de imagem que você está criando, tentar limitar outros efeitos colaterais não intencionais dos comandos que você está executando pode ser útil. Por exemplo, além dos pacotes explicitamente declarados, o apt também instala pacotes “recomendados” por padrão. Você pode incluir --no-install-recommends aos seus comandos apt para remover esse comportamento. Você pode ter que experimentar para descobrir se você confia em qualquer uma das funcionalidades fornecidas pelos pacotes recomendados.

      Usamos os comandos de gerenciamento de pacotes nesta seção como exemplo, mas esses mesmos princípios se aplicam a outros cenários. A idéia geral é construir as condições de pré-requisito, executar o comando mínimo viável e, em seguida, limpar quaisquer artefatos desnecessários em um único comando RUN para reduzir a sobrecarga da camada que você estará produzindo.

      Usando Multi-stage Builds

      Multi-stage builds foram introduzidos no Docker 17.05, permitindo aos desenvolvedores controlar mais rigidamente as imagens finais de runtime que eles produzem. Multi-stage builds ou Compilações em Vários Estágios permitem que você divida seu Dockerfile em várias seções representando estágios distintos, cada um com uma instrução FROM para especificar imagens pai separadas.

      Seções anteriores definem imagens que podem ser usadas para criar sua aplicação e preparar ativos. Elas geralmente contêm ferramentas de compilação e arquivos de desenvolvimento necessários para produzir a aplicação, mas não são necessários para executá-la. Cada estágio subsequente definido no arquivo terá acesso aos artefatos produzidos pelos estágios anteriores.

      A última declaração FROM define a imagem que será usada para executar a aplicação. Normalmente, essa é uma imagem reduzida que instala apenas os requisitos de runtime necessários e, em seguida, copia os artefatos da aplicação produzidos pelos estágios anteriores.

      Este sistema permite que você se preocupe menos com a otimização das instruções RUN nos estágios de construção, já que essas camadas de container não estarão presentes na imagem de runtime final. Você ainda deve prestar atenção em como as instruções interagem com o cache de camadas nos estágios de construção, mas seus esforços podem ser direcionados para minimizar o tempo de construção em vez do tamanho final da imagem. Prestar atenção às instruções no estágio final ainda é importante para reduzir o tamanho da imagem, mas ao separar os diferentes estágios da construção do container, é mais fácil obter imagens simplificadas sem tanta complexidade no Dockerfile.

      Escopo de Funcionalidade ao Nível de Container e de Pod

      Embora as escolhas que você faz em relação às instruções de criação de containers sejam importantes, decisões mais amplas sobre como containerizar seus serviços geralmente têm um impacto mais direto em seu sucesso. Nesta seção, falaremos um pouco mais sobre como fazer uma melhor transição de suas aplicações de um ambiente mais convencional para uma plataforma de container.

      Containerizando por Função

      Geralmente, é uma boa prática empacotar cada parte de uma funcionalidade independente em uma imagem de container separada.

      Isso difere das estratégias comuns empregadas nos ambientes de máquina virtual, em que os aplicativos são frequentemente agrupados na mesma imagem para reduzir o tamanho e minimizar os recursos necessários para executar a VM. Como os containers são abstrações leves que não virtualizam toda a pilha do sistema operacional, essa abordagem é menos atraente no Kubernetes. Assim, enquanto uma máquina virtual de stack web pode empacotar um servidor web Nginx com um servidor de aplicações Gunicorn em uma única máquina para servir uma aplicação Django, no Kubernetes eles podem ser divididos em containeres separados.

      Projetar containers que implementam uma parte discreta de funcionalidade para seus serviços oferece várias vantagens. Cada container pode ser desenvolvido independentemente se as interfaces padrão entre os serviços forem estabelecidas. Por exemplo, o container Nginx poderia ser usado para fazer proxy para vários back-ends diferentes ou poderia ser usado como um balanceador de carga se tivesse uma configuração diferente.

      Depois de fazer o deploy, cada imagem de container pode ser escalonada independentemente para lidar com várias restrições de recursos e de carga. Ao dividir suas aplicações em várias imagens de container, você ganha flexibilidade no desenvolvimento, na organização e no deployment.

      Combinando Imagens de Container em Pods

      No Kubernetes, pods são a menor unidade que pode ser gerenciada diretamente pelo painel de controle. Os pods consistem em um ou mais containers juntamente com dados de configuração adicionais para informar à plataforma como esses componentes devem ser executados. Os containers em um pod são sempre lançados no mesmo worker node no cluster e o sistema reinicia automaticamente containers com falha. A abstração do pod é muito útil, mas introduz outra camada de decisões sobre como agrupar os componentes de suas aplicações.

      Assim como as imagens de container, os pods também se tornam menos flexíveis quando muita funcionalidade é agrupada em uma única entidade. Os próprios pods podem ser escalados usando outras abstrações, mas os containers dentro deles não podem ser gerenciados ou redimensionados independentemente. Portanto, para continuar usando nosso exemplo anterior, os containers Nginx e Gunicorn separados provavelmente não devem ser empacotados juntos em um único pod, para que possam ser controlados e deployados separadamente.

      No entanto, há cenários em que faz sentido combinar containers funcionalmente diferentes como uma unidade. Em geral, eles podem ser categorizadas como situações em que um container adicional suporta ou aprimora a funcionalidade central do container principal ou ajuda-o a adaptar-se ao seu ambiente de deployment. Alguns padrões comuns são:

      • Sidecar: O container secundário estende a funcionalidade central do container principal, agindo em uma função de utilitário de suporte. Por exemplo, o container sidecar pode encaminhar logs ou atualizar o sistema de arquivos quando um repositório remoto é alterado. O container principal permanece focado em sua responsabilidade central, mas é aprimorado pelos recursos fornecidos pelo sidecar.
      • Ambassador: Um container Ambassador é responsável por descobrir e conectar-se a recursos externos (geralmente complexos). O container principal pode se conectar a um container Ambassador em interfaces conhecidas usando o ambiente interno do pod. O Ambassador abstrai os recursos de back-end e o tráfego de proxies entre o container principal e o pool de recursos.
      • Adaptor: Um container Adaptor é responsável por normalizar as interfaces primárias de container, os dados e os protocolos para alinhar com as propriedades esperadas por outros componentes. O container principal pode operar usando formatos nativos e o container Adaptor traduz e normaliza os dados para se comunicar com o mundo externo.

      Como você deve ter notado, cada um desses padrões suporta a estratégia de criar imagens genéricas e padronizadas de container principais que podem ser implantadas em contextos e configurações variados. Os containers secundários ajudam a preencher a lacuna entre o container principal e o ambiente de deployment específico que está sendo usado. Alguns containers Sidecar também podem ser reutilizados para adaptar vários containers primários às mesmas condições ambientais. Esses padrões se beneficiam do sistema de arquivos compartilhado e do namespace de rede fornecidos pela abstração do pod, ao mesmo tempo em que permitem o desenvolvimento independente e o deploy flexível de containers padronizados.

      Projetando para Configuração de Runtime

      Existe alguma tensão entre o desejo de construir componentes reutilizáveis e padronizados e os requisitos envolvidos na adaptação de aplicações ao seu ambiente de runtime. A configuração de runtime é um dos melhores métodos para preencher a lacuna entre essas preocupações. Componentes são criados para serem genéricos e flexíveis e o comportamento necessário é descrito no runtime, fornecendo ao software informações adicionais sobre a configuração. Essa abordagem padrão funciona para containers, assim como para aplicações.

      Construir com a configuração de runtime em mente requer que você pense à frente durante as etapas de desenvolvimento de aplicação e de containerização. As aplicações devem ser projetadas para ler valores de parâmetros da linha de comando, arquivos de configuração ou variáveis de ambiente quando forem iniciados ou reiniciados. Essa lógica de análise e injeção de configuração deve ser implementada no código antes da containerização.

      Ao escrever um Dockerfile, o container também deve ser projetado com a configuração de runtime em mente. Os containers possuem vários mecanismos para fornecer dados em tempo de execução. Os usuários podem montar arquivos ou diretórios do host como volumes dentro do container para ativar a configuração baseada em arquivo. Da mesma forma, as variáveis de ambiente podem ser passadas para o runtime interno do container quando o msmo é iniciado. As instruções de Dockerfile CMD e ENTRYPOINT também podem ser definidas de uma forma que permita que as informações de configuração de runtime sejam passadas como parâmetros de comando.

      Como o Kubernetes manipula objetos de nível superior, como pods, em vez de gerenciar containeres diretamente, há mecanismos disponíveis para definir a configuração e injetá-la no ambiente de container em runtime. Kubernetes ConfigMaps e Secrets permitem que você defina os dados de configuração separadamente e projete os valores no ambiente de container como variáveis de ambiente ou arquivos em runtime. ConfigMaps são objetos de finalidade geral destinados a armazenar dados de configuração que podem variar de acordo com o ambiente, o estágio de teste etc. Secrets oferecem uma interface semelhante, mas são projetados especificamente para dados confidenciais, como senhas de contas ou credenciais de API.

      Ao entender e utilizar corretamente as opções de configuração de runtime disponíveis em todas as camadas de abstração, você pode criar componentes flexíveis que retiram suas entradas dos valores fornecidos pelo ambiente. Isso possibilita reutilizar as mesmas imagens de container em cenários muito diferentes, reduzindo a sobrecarga de desenvolvimento, melhorando a flexibilidade da aplicação.

      Ao fazer a transição para ambientes baseados em container, os usuários geralmente iniciam movendo as cargas de trabalho existentes, com poucas ou nenhuma alteração, para o novo sistema. Eles empacotam aplicações em containers agrupando as ferramentas que já estão usando na nova abstração. Embora seja útil utilizar seus padrões usuais para colocar as aplicações migradas em funcionamento, cair em implementações anteriores em containers pode, às vezes, levar a um design ineficaz.

      Tratando Containers como Aplicações, não como Serviços

      Frequentemente surgem problemas quando os desenvolvedores implementam uma funcionalidade significativa de gerenciamento de serviços nos containers. Por exemplo, a execução de serviços systemd no container ou tornar daemons os servidores web pode ser considerada uma prática recomendada em um ambiente de computação normal, mas elas geralmente entram em conflito com as suposições inerentes ao modelo de container.

      Os hosts gerenciam os eventos do ciclo de vida do container enviando sinais para o processo que opera como PID (ID do processo) 1 dentro do container. O PID 1 é o primeiro processo iniciado, que seria o sistema init em ambientes de computação tradicionais. No entanto, como o host só pode gerenciar o PID 1, usar um sistema init convencional para gerenciar processos dentro do container às vezes significa que não há como controlar a aplicação principal. O host pode iniciar, parar ou matar o sistema init interno, mas não pode gerenciar diretamente a aplicação principal. Os sinais às vezes propagam o comportamento pretendido para a aplicação em execução, mas isso adiciona complexidade e nem sempre é necessário.

      Na maioria das vezes, é melhor simplificar o ambiente de execução dentro do container para que o PID 1 esteja executando a aplicação principal em primeiro plano. Nos casos em que vários processos devem ser executados, o PID 1 é responsável por gerenciar o ciclo de vida de processos subsequentes. Certas aplicações, como o Apache, lidam com isso nativamente gerando e gerenciando workers que lidam com conexões. Para outras aplicações, um script wrapper ou um sistema init muito simples como o dumb-init ou o sistema init incluído tini podem ser usados em alguns casos. Independentemente da implementação escolhida, o processo que está sendo executado como PID 1 no container deve responder adequadamente aos sinais TERM enviados pelo Kubernetes para se comportar como esperado.

      Gerenciando a Integridade do Container no Kubernetes

      Os deployments e serviços do Kubernetes oferecem gerenciamento de ciclo de vida para processos de longa duração e acesso confiável e persistente a aplicações, mesmo quando os containers subjacentes precisam ser reiniciados ou as próprias implementações são alteradas. Ao retirar a responsabilidade de monitorar e manter a integridade do serviço do container, você pode aproveitar as ferramentas da plataforma para gerenciar cargas de trabalho saudáveis.

      Para que o Kubernetes gerencie os containers adequadamente, ele precisa entender se as aplicações em execução nos containers são saudáveis e capazes de executar o trabalho. Para ativar isso, os containers podem implementar análises de integridade: endpoints de rede ou comandos que podem ser usados para relatar a integridade da aplicação. O Kubernetes verificará periodicamente as sondas de integridade definidas para determinar se o container está operando conforme o esperado. Se o container não responder adequadamente, o Kubernetes reinicia o container na tentativa de restabelecer a funcionalidade.

      O Kubernetes também fornece sondas de prontidão, uma construção similar. Em vez de indicar se a aplicação em um container está íntegra, as sondas de prontidão determinam se a aplicação está pronta para receber tráfego. Isso pode ser útil quando uma aplicação em container tiver uma rotina de inicialização que deve ser concluída antes de estar pronta para receber conexões. O Kubernetes usa sondas ou testes de prontidão para determinar se deve adicionar um pod ou remover um pod de um serviço.

      A definição de endpoints para esses dois tipos de sondagem pode ajudar o Kubernetes a gerenciar seus containers com eficiência e pode evitar que problemas no ciclo de vida do container afetem a disponibilidade do serviço. Os mecanismos para responder a esses tipos de solicitações de integridade devem ser incorporados à própria aplicação e devem ser expostos na configuração da imagem do Docker.

      Conclusão

      Neste guia, abordamos algumas considerações importantes para se ter em mente ao executar aplicações em container no Kubernetes. Para reiterar, algumas das sugestões que examinamos foram:

      • Use imagens pai mínimas e compartilháveis para criar imagens com o mínimo de inchaço e reduzir o tempo de inicialização
      • Use multi-stage builds para separar os ambientes de criação e de runtime do container
      • Combine as instruções do Dockerfile para criar camadas de imagem limpas e evitar erros de cache de imagem
      • Containerize isolando a funcionalidade discreta para permitir a escalabilidade e o gerenciamento flexíveis
      • Crie pods para ter uma responsabilidade única e focada
      • Empacote os containers auxiliares para melhorar a funcionalidade do container principal ou para adaptá-lo ao ambiente de deployment
      • Crie aplicações e containers para responder à configuração de runtime para permitir maior flexibilidade ao fazer deploy
      • Execute aplicações como processos principais em containers, para que o Kubernetes possa gerenciar eventos do ciclo de vida
      • Desenvolva endpoints de integridade e atividade dentro da aplicação ou container para que o Kubernetes possa monitorar a integridade do container

      Durante todo o processo de desenvolvimento e deployment, você precisará tomar decisões que podem afetar a robustez e a eficácia do seu serviço. Compreender as maneiras pelas quais as aplicações conteinerizadas diferem das aplicações convencionais e aprender como elas operam em um ambiente de cluster gerenciado pode ajudá-lo a evitar algumas armadilhas comuns e permitir que você aproveite todos os recursos oferecidos pelo Kubernetes.



      Source link

      How to Use a Remote Docker Server to Speed Up Your Workflow


      Introduction

      Building CPU-intensive images and binaries is a very slow and time-consuming process that can turn your laptop into a space heater at times. Pushing Docker images on a slow connection takes a long time, too. Luckily, there’s an easy fix for these issues. Docker lets you offload all those tasks to a remote server so your local machine doesn’t have to do that hard work.

      This feature was introduced in Docker 18.09. It brings support for connecting to a Docker host remotely via SSH. It requires very little configuration on the client, and only needs a regular Docker server without any special config running on a remote machine. Prior to Docker 18.09, you had to use Docker Machine to create a remote Docker server and then configure the local Docker environment to use it. This new method removes that additional complexity.

      In this tutorial, you’ll create a Droplet to host the remote Docker server and configure the docker command on your local machine to use it.

      Prerequisites

      To follow this tutorial, you’ll need:

      • A DigitalOcean account. You can create an account if you don’t have one already.
      • Docker installed on your local machine or development server. If you are working with Ubuntu 18.04, follow Steps 1 and 2 of How To Install and Use Docker on Ubuntu 18.04; otherwise, follow the official documentation for information about installing on other operating systems. Be sure to add your non-root user to the docker group, as described in Step 2 of the linked tutorial.

      Step 1 – Creating the Docker Host

      To get started, spin up a Droplet with a decent amount of processing power. The CPU Optimized plans are perfect for this purpose, but Standard ones work just as well. If you will be compiling resource-intensive programs, the CPU Optimized plans provide dedicated CPU cores which allow for faster builds. Otherwise, the Standard plans offer a more balanced CPU to RAM ratio.

      The Docker One-click image takes care of all of the setup for us. Follow this link to create a 16GB/8vCPU CPU-Optimized Droplet with Docker from the control panel.

      Alternatively, you can use doctl to create the Droplet from your local command line. To install it, follow the instructions in the doctl README file on GitHub.

      The following command creates a new 16GB/8vCPU CPU-Optimized Droplet in the FRA1 region based on the Docker One-click image:

      • doctl compute droplet create docker-host
      • --image docker-18-04
      • --region fra1
      • --size c-8
      • --wait
      • --ssh-keys $(doctl compute ssh-key list --format ID --no-header | sed 's/$/,/' | tr -d 'n' | sed 's/,$//')

      The doctl command uses the ssh-keys value to specify which SSH keys it should apply to your new Droplet. We use a subshell to call doctl compute ssh-key-list to retrieve the SSH keys associated with your DigitalOcean account, and then parse the results using the sed and tr commands to format the data in the correct format. This command includes all of your account’s SSH keys, but you can replace the highlighted subcommand with the fingerprint of any key you have in your account.

      Once the Droplet is created you’ll see its IP address among other details:

      Output

      ID Name Public IPv4 Private IPv4 Public IPv6 Memory VCPUs Disk Region Image Status Tags Features Volumes 148681562 docker-host your_server_ip 16384 8 100 fra1 Ubuntu Docker 5:18.09.6~3 on 18.04 active

      You can learn more about using the doctl command in the tutorial How To Use doctl, the Official DigitalOcean Command-Line Client.

      When the Droplet is created, you’ll have a ready to use Docker server. For security purposes, create a Linux user to use instead of root.

      First, connect to the Droplet with SSH as the root user:

      Once connected, add a new user. This command adds one named sammy:

      Then add the user to the docker group to give it permission to run commands on the Docker host.

      • sudo usermod -aG docker sammy

      Finally, exit from the remote server by typing exit.

      Now that the server is ready, let's configure the local docker command to use it.

      Step 2 – Configuring Docker to Use the Remote Host

      To use the remote host as your Docker host instead of your local machine, set the DOCKER_HOST environment variable to point to the remote host. This variable will instruct the Docker CLI client to connect to the remote server.

      • export DOCKER_HOST=ssh://sammy@your_server_ip

      Now any Docker command you run will be run on the Droplet. For example, if you start a web server container and expose a port, it will be run on the Droplet and will be accessible through the port you exposed on the Droplet's IP address.

      To verify that you're accessing the Droplet as the Docker host, run docker info.

      You will see your Droplet's hostname listed in the Name field like so:

      Output

      … Name: docker-host

      One thing to keep in mind is that when you run a docker build command, the build context (all files and folders accessible from the Dockerfile) will be sent to the host and then the build process will run. Depending on the size of the build context and the amount of files, it may take a longer time compared to building the image on a local machine. One solution would be to create a new directory dedicated to the Docker image and copy or link only the files that will be used in the image so that no unneeded files will be uploaded inadvertently.

      Conclusion

      You've created a remote Docker host and connected to it locally. The next time your laptop's battery is running low or you need to build a heavy Docker image, use your shiny remote Docker server instead of your local machine.

      You might also be interested in learning how to optimize Docker images for production, or how to optimize them specifically for Kubernetes.



      Source link