Continuando a primeira parte deste artigo, vou terminar minhas impressões sobre a implantação Ruby on Rails em nível de Produção no Kubernetes Engine do Google Cloud.
O Balanceador de Carga
Muitos artigos irão expor esses pods diretamente através de um serviço diferente, chamado Balanceador de Carga. Não tenho tanta certeza do quão bem isso se comporta sob pressão e se possui terminação SSL, etc. Por isso, eu decidi passar completamente com um Balanceador de Carga Ingress usando o NGINX Controller.
Antes de tudo, decidi criar um node-pool separado para ele. Por exemplo, como este:
gcloud container node-pools create web-load-balancer \ --cluster=my-web-production \ --node-labels=role=load-balancer \ --machine-type=g1-small \ --num-nodes 1 \ --max-nodes 3 --min-nodes=1 \ --enable-autoscaling
Assim como quando criamos o exemplo large-pool, aqui você deve cuidar da adição de –node-labels para que o controlador seja instalado aqui em vez do default-pool. Você precisará saber o nome da instância do node, podemos fazê-lo da seguinte forma:
$ gcloud compute instances list NAME ZONE MACHINE_TYPE PREEMPTIBLE INTERNAL_IP EXTERNAL_IP STATUS gke-my-web-production-default-pool-123-123 us-west1-a n1-standard-4 10.128.0.1 123.123.123.12 RUNNING gke-my-web-production-large-pool-123-123 us-west1-a n1-highcpu-8 10.128.0.2 50.50.50.50 RUNNING gke-my-web-production-web-load-balancer-123-123 us-west1-a g1-small 10.128.0.3 70.70.70.70 RUNNING
Vamos salvar assim por enquanto:
export LB_INSTANCE_NAME=gke-my-web-production-web-load-balancer-123-123
Você pode reservar manualmente um IP externo e dar-lhe um nome como este:
gcloud compute addresses create ip-web-production \ --ip-version=IPV4 \ --global
Para o bem do exemplo, vamos dizer que gerou um IP reservado “111.111.111.111”. Então, vamos buscá-lo e salvá-lo assim, por enquanto:
export LB_ADDRESS_IP=$(gcloud compute addresses list | grep "ip-web-production" | awk '{print $3}')
Finalmente, vamos ligar este endereço à instância do node do balanceador de carga:
export LB_INSTANCE_NAT=$(gcloud compute instances describe $LB_INSTANCE_NAME | grep -A3 networkInterfaces: | tail -n1 | awk -F': ' '{print $2}') gcloud compute instances delete-access-config $LB_INSTANCE_NAME \ --access-config-name "$LB_INSTANCE_NAT" gcloud compute instances add-access-config $LB_INSTANCE_NAME \ --access-config-name "$LB_INSTANCE_NAT" --address $LB_ADDRESS_IP
Uma vez que tivermos feito isso, poderemos adicionar o resto da configuração de Implantação do Ingress. Isso será meio longo, mas é, na maior parte, padrão. Vamos começar por definir outro aplicativo da web que chamaremos de default-http-backend, que será usado para responder a solicitações HTTP, caso nossos pods da web não estejam disponíveis por algum motivo. Vamos chamá-lo de deploy/default-web.yml:
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: default-http-backend spec: replicas: 1 template: metadata: labels: app: default-http-backend spec: terminationGracePeriodSeconds: 60 containers: - name: default-http-backend # Any image is permissable as long as: # 1. It serves a 404 page at / # 2. It serves 200 on a /healthz endpoint image: gcr.io/google_containers/defaultbackend:1.0 livenessProbe: httpGet: path: /healthz port: 8080 scheme: HTTP initialDelaySeconds: 30 timeoutSeconds: 5 ports: - containerPort: 8080 resources: limits: cpu: 10m memory: 20Mi requests: cpu: 10m memory: 20Mi
Não é necessário mudar nada aqui, e agora você já deve estar familiarizado com o modelo de Implantação. Mais uma vez, agora você sabe que precisa expô-lo através de um NodePort. Então, vamos adicionar um deploy/default-web-svc.yml:
kind: Service apiVersion: v1 metadata: name: default-http-backend spec: selector: app: default-http-backend ports: - protocol: TCP port: 80 targetPort: 8080 type: NodePort
Mais uma vez, não é necessário mudar nada. Os próximos três arquivos são as partes importantes. Primeiro, vamos criar um Balanceador de Carga NGINX e chamá-lo de deploy/nginx.yml:
apiVersion: extensions/v1beta1 kind: Deployment metadata: name: nginx-ingress-controller spec: replicas: 1 template: metadata: labels: k8s-app: nginx-ingress-lb spec: # hostNetwork makes it possible to use ipv6 and to preserve the source IP correctly regardless of docker configuration # however, it is not a hard dependency of the nginx-ingress-controller itself and it may cause issues if port 10254 already is taken on the host # that said, since hostPort is broken on CNI (https://github.com/kubernetes/kubernetes/issues/31307) we have to use hostNetwork where CNI is used hostNetwork: true terminationGracePeriodSeconds: 60 nodeSelector: role: load-balancer containers: - args: - /nginx-ingress-controller - "--default-backend-service=$(POD_NAMESPACE)/default-http-backend" - "--default-ssl-certificate=$(POD_NAMESPACE)/cloudflare-secret" env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace image: "gcr.io/google_containers/nginx-ingress-controller:0.9.0-beta.5" imagePullPolicy: Always livenessProbe: httpGet: path: /healthz port: 10254 scheme: HTTP initialDelaySeconds: 10 timeoutSeconds: 5 name: nginx-ingress-controller ports: - containerPort: 80 name: http protocol: TCP - containerPort: 443 name: https protocol: TCP volumeMounts: - mountPath: /etc/nginx-ssl/dhparam name: tls-dhparam-vol volumes: - name: tls-dhparam-vol secret: secretName: tls-dhparam
Observe o nodeSelector para tornar o rótulo do node que adicionamos quando criamos o novo node-pool.
Você pode querer mexer com os rótulos, o número de réplicas se você precisar. Mas aqui, você notará que ele monta um volume que eu nomeei como tls-dhparam-vol. Este é um Diffie Hellman Ephemeral Parameters. É assim que o geramos:
sudo openssl dhparam -out ~/documents/dhparam.pem 2048 kubectl create secret generic tls-dhparam --from-file=/home/myself/documents/dhparam.pem kubectl create secret generic tls-dhparam --from-file=/home/myself/documents/dhparam.pem
Além disso, observe que estou usando a versão “0.9.0-beta_5” para a imagem do controlador. Funciona bem, sem problemas até agora. Mas fique atento às notas de versão para versões mais recentes também e faça seus próprios testes.
Novamente, vamos expor esse controlador Ingress através do Serviço do Balanceador de Carga. Vamos chamá-lo de deploy/nginx-svc.yml:
apiVersion: v1 kind: Service metadata: name: nginx-ingress spec: type: LoadBalancer loadBalancerIP: 111.111.111.111 ports: - name: http port: 80 targetPort: http - name: https port: 443 targetPort: https selector: k8s-app: nginx-ingress-lb
Lembre-se do IP externo estático que reservamos acima e salvamos no LB_INGRESS_IP ENV var? Este é o que devemos colocar na seção spec:loadBalancerIP. Este é também o IP que você irá adicionar como “Um registro” no seu serviço DNS (digamos, mapeando seu “www.my-app.com.br” no CloudFlare).
Finalmente, podemos criar a configuração Ingress por si própria, vamos criar um deploy/ingress.yml como este:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: ingress annotations: kubernetes.io/ingress.class: "nginx" nginx.org/ssl-services: "web-svc" kubernetes.io/ingress.global-static-ip-name: ip-web-production ingress.kubernetes.io/ssl-redirect: "true" ingress.kubernetes.io/rewrite-target: / spec: tls: - hosts: - www.my-app.com.br secretName: cloudflare-secret rules: - host: www.my-app.com.br http: paths: - path: / backend: serviceName: web-svc servicePort: 80
Tenha cuidado com as anotações acima que conectam tudo. Elas vinculam o serviço NodePort que criamos para os pods da web com o controlador ingress nginx e adiciona a terminação SSL através dessa spec:tls:secretName.
Como você cria isso? Primeiro, você deve comprar um certificado SSL – novamente, usando CloudFlare como o exemplo.
Quando você terminar de comprar, o provedor deve lhe dar os arquivos secretos a serem baixados (mantenha-os seguros! Uma pasta pública de depósito não é segura!). Então, você deve adicioná-los à infraestrutura desse jeito:
kubectl create secret tls cloudflare-secret \ --key ~/downloads/private.pem \ --cert ~/downloads/fullchain.pem
Agora que editamos toda uma série de arquivos, podemos implantar toda a pilha do balanceador de carga:
kubectl create -f deploy/default-web.yml kubectl create -f deploy/default-web-svc.yml kubectl create -f deploy/nginx.yml kubectl create -f deploy/nginx-svc.yml kubectl create -f deploy/ingress.yml
Esta configuração NGINX Ingress baseia-se no artigo do site de Zihao Zhang. Também há exemplos no repositório incubador de kubernetes. Você pode querer verificar isso também.
Se você fez tudo certo até agora, https://www.my-app-com.br deve carregar seu aplicativo da Web. Você pode querer verificar o Tempo para o Primeiro-Byte (TTFB). Você pode fazer isso através do CloudFlare da seguinte maneira:
curl -vso /dev/null -w "Connect: %{time_connect} \n TTFB: %{time_starttransfer} \n Total time: %{time_total} \n" https://www.my-app.com.br
Ou, se você estiver tendo TTFB lento, pode ignorar o CloudFlare fazendo isso:
curl --resolve www.my-app.com.br:443:111.111.111.111 https://www.my-app.com.br -svo /dev/null -k -w "Connect: %{time_connect} \n TTFB: %{time_starttransfer} \n Total time: %{time_total} \n"
TTFB deve estar na proximidade de um segundo ou menos. Qualquer coisa muito além pode significar um problema em seu aplicativo. Você deve verificar os tipos de máquina da instância do node, o número de trabalhadores carregados por pod, a versão do proxy CloudSQL, a versão do controlador NGINX e assim por diante. Este é um procedimento de teste e erro, até onde eu sei. Inscreva-se em serviços como o Loader ou mesmo o Web Page Test para informações.
Atualizações Rolling
Agora que tudo está pronto e funcionando, como realizamos as Atualizações Rolling que mencionei no início? Primeiro, você dá um git push para o repositório do Registro de Contêiner e aguarda a criação da imagem do Docker.
Lembre-se que eu disse para deixar um gatilho marcar a imagem com um número de versão aleatório? Vamos usá-lo (você pode vê-lo na lista do Histórico de Compilação no Registro do Contêiner, no console do Google Cloud):
kubectl set image deployment web my-app=gcr.io/my-project/my-app:1238471234g123f534f543541gf5 --record
Você deve usar o mesmo nome e imagem que está declarado no deploy/web.yml de cima. Isso começará a lançar a atualização adicionando um novo pod, depois encerrando um pod e assim por diante, até que todos eles sejam atualizados, sem tempo de inatividade para seus usuários.
As Atualizações Rolling devem ser realizadas com cuidado. Por exemplo, se sua nova implantação requer uma migração de banco de dados, então você deve adicionar uma janela de manutenção (que significa: faça isso quando há, de pouco para nenhum tráfego, assim como no meio da noite). Então, você pode executar o comando migrar assim:
kubectl get pods # to get a pod name kubectl exec -it my-web-12324-121312 /app/bin/rails db:migrate # you can also bash to a pod like this, but remember that this is an ephemeral container, so file you edit and write there disappear on the next restart: kubectl exec -it my-web-12324-121312 bash
Para reimplantar tudo sem restaurar para uma Atualização Rolling, você deve fazer isso:
kubectl delete -f deploy/web.yml && kubectl apply -f deploy/web.yml
Você encontrará uma explicação mais completa no artigo de Ta-Ching.
Bônus: Instantâneos Automáticos
Um item que eu tinha na minha lista “Eu queria/precisava”, no início, é a capacidade de ter um armazenamento montável, persistente com backups/instantâneos automáticos. O Google Cloud fornece metade disso por enquanto. Você pode criar discos persistentes para montar em seus pods, mas não tem um recurso para fazer backup automaticamente. Pelo menos, ele possui APIs para inserir o instantâneo manualmente.
Para este exemplo, vamos criar um novo disco SSD e formata-lo primeiro:
gcloud compute disks create --size 500GB my-data --type pd-ssd gcloud compute instances list
O último comando é para que possamos copiar o nome de uma instância de node. Digamos que seja gke-my-web-app-default-pool-123-123. Vamos anexar o disco my-data a ele:
gcloud compute instances attach-disk gke-my-web-app-default-pool-123-123 --disk my-data --device-name my-data gcloud compute ssh gke-my-web-app-default-pool-123-123
O último comando ssh está na instância. Podemos listar os discos anexados com o sudo lsblk e você verá o disco de 500GB, provavelmente, como /dev/sdb, mas certifique-se de que está correto porque vamos formatá-lo!
sudo mkfs.ext4 -m 0 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/sdb
Agora podemos sair da sessão SSH e separar o disco:
gcloud compute instances detach-disk gke-my-web-app-default-pool-123-123 --disk my-data
Você pode montar este disco em seus pods, adicionando o seguinte ao seu yaml de implantação:
spec: containers: - image: ... name: my-app volumeMounts: - name: my-data mountPath: /data # readOnly: true # ... volumes: - name: my-data gcePersistentDisk: pdName: my-data fsType: ext4
Agora, vamos criar um arquivo de implantação CronJob como deploy/auto-snapshot.yml:
apiVersion: batch/v1beta1 kind: CronJob metadata: name: auto-snapshot spec: schedule: "0 4 * * *" concurrencyPolicy: Forbid jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: auto-snapshot image: grugnog/google-cloud-auto-snapshot command: ["/opt/entrypoint.sh"] env: - name: "GOOGLE_CLOUD_PROJECT" value: "my-project" - name: "GOOGLE_APPLICATION_CREDENTIALS" value: "/credential/credential.json" volumeMounts: - mountPath: /credential name: editor-credential volumes: - name: editor-credential secret: secretName: editor-credential
Como já fizemos antes, você precisará criar outra Conta de Serviço com permissões de editor na seção “IAM & admin” do console do Google Cloud, depois baixe a credencial JSON e, finalmente, carregue-a assim:
kubectl create secret generic editor-credential \ --from-file=credential.json=/home/myself/download/my-project-1212121.json
Observe também que, como um trabalho cron normal, há um parâmetro de cronograma que você pode querer mudar. No exemplo, “0 4 * * *”, significa que ele executará o instantâneo todos os dias às 4 da manhã.
Confira o repositório original desta solução para obter mais detalhes.
E isso deve ser tudo por enquanto!
Como eu disse no começo, este não é um procedimento completo, apenas destaques de algumas das partes importantes. Se você é novo no Kubernetes, você acabou de ler sobre Implantação, Service, Ingress, mas você tem ReplicaSet, DaemonSet e muito mais para brincar.
Eu acho que isso também já está extenso de mais para adicionar uma explicação de configuração de alta disponibilidade de várias regiões, então vamos parar por aqui.
Quaisquer correções ou sugestões são mais do que bem-vindas porque ainda estou no processo de aprendizagem, e há uma série de coisas que ainda não conheço.
***
Artigo traduzido com autorização do autor. Publicado originalmente em: http://www.akitaonrails.com/2018/01/09/my-notes-about-a-production-grade-ruby-on-rails-deployment-on-google-cloud-kubernetes-engine