Back-End

18 jan, 2018

Implantação Ruby on Rails em nível de produção no Kubernetes Engine do Google Cloud – Parte 01

Publicidade

Eu tenho usado o Google Cloud com o Kubernetes Engine por dois meses e mudei, do zero para a produção. Na verdade, não levou um mês para que eu juntasse tudo, mas levou um mês extra para que eu descobrisse algumas arestas ásperas.

Muito longo; não leia: O Google está realmente fazendo um bom trabalho sendo um contrabalanceador para que o AWS não afrouxe. Se você já sabe tudo sobre o AWS, gostaria de encorajá-lo a testar o Google Cloud.

Possivelmente devido à memória muscular, eu ainda ficaria mais confortável com o AWS, mas agora que me forcei a sofrer o processo de aprendizagem, estou bastante confiante com o Google Cloud e Kubernetes para a maioria dos meus cenários.

Já confesso que não sou um especialista, então aceite o que eu digo com cautela. Este é um daqueles assuntos sobre os quais eu estou super ansioso para falar, mas também estou muito relutante com a escolha adequada de palavras para que você não tenha uma ideia errada sobre as soluções propostas.

O objetivo deste exercício é principalmente para que eu armazene alguns trechos e pensamentos para futuras referências. Portanto, lembre-se de que este também não é um artigo passo a passo. Minha primeira intenção foi seguir assim, mas percebi que seria quase como escrever um livro inteiro, então, não desta vez.

Para ter sucesso com algo como Google Cloud e Kubernetes, você deve ser testado em uma infraestrutura. Se você nunca instalou caixas de Linux de nível do servidor do zero, se você nunca fez otimizações do servidor, se você não estiver confortável com os componentes do lado do servidor, não tente uma implantação de uma produção real.

Sua aposta mais segura ainda é algo como Heroku.

Você tem que ser aquele tipo de pessoa que gosta de mexer com alguma coisa (como provavelmente você me viu fazendo em artigos anteriores do site).

Eu não sei tudo, mas sei o suficiente. Então, eu só tinha que descobrir quais das peças estariam de acordo com minhas necessidades. Você deve descrever suas necessidades antes de tentar escrever seu primeiro arquivo YAML. Planejar é crucial.

As coisas mais importantes em primeiro lugar, isso é o que eu queria/precisava:

  • Nível de aplicação web escalável, onde eu poderia fazer tanto as atualizações de rolagem (para zero atualizações de inatividade), quanto dimensionamento horizontal automático e manual dos servidores.
  • Armazenamento persistente montável com instantâneos/backups automáticos.
  • Banco de dados robusto gerenciado (Postgresql) com backups automáticos e replicação fácil para instâncias read-only.
  • Solução gerenciada para armazenar segredos (como o suporte ENV da Heroku). Nunca armazene a configuração de produção no código fonte.
  • Imagens do Docker suportam sem que eu tenha que criar infraestrutura personalizada para implantar.
  • Endereços IP externos estáticos para integrações que requerem um IP fixo.
  • Terminação SSL para que eu possa me conectar ao CloudFlare (CDN é obrigatório, mas não o suficiente. Em 2018 precisamos de algum nível de proteção DDoS).
  • Segurança suficiente por padrão, então tudo está – em teoria – isolado/bloqueado a menos que eu decida abrir.
  • Alta disponibilidade em diferentes regiões e zonas do centro de dados.

É fácil implantar uma aplicação web de demonstração simples. Mas eu não queria uma demonstração, eu queria uma solução em nível de produção para longo prazo. As melhorias para a minha implementação são muito bem-vindas, então fique à vontade para comentar abaixo.

Alguns dos problemas para os recém-chegados:

  • A documentação é muito extensa, e você encontrará quase tudo – se você souber o que está procurando. Também tenha em mente que o Azure e o AWS também implementam Kubernetes com algumas diferenças, de modo que alguma documentação não se aplica ao Google Cloud e vice-versa.
  • Existem vários recursos nos estágios alfa, beta e estável. A documentação meio que nos mantém bem, mas a maioria dos tutoriais com apenas alguns meses de idade podem não mais funcionar como pretendido (incluindo este – eu estou assumindo Kubernetes 1.8.4-gke).
  • Existe todo um conjunto de palavras que se aplicam a conceitos que você já conhece, mas são chamados de diferentes. Acostumar-se ao vocabulário pode ser necessário no caminho logo de cara.
  • Parece que você está brincando com Lego. Muitas peças que você pode misturar e combinar. É fácil bagunçar. Isso significa que você pode criar uma configuração adaptada às suas necessidades. Mas se você apenas copiar e colar de tutoriais, você ficará preso.
  • Você pode fazer quase tudo através de arquivos YAML e da linha de comando, mas não é trivial reutilizar a configuração (por exemplo, para ambientes de produção e de teste). Existem ferramentas de terceiros que lidam com bits YAML parametrizáveis e reutilizáveis, mas eu faria tudo à mão primeiro. Nunca, nunca, tente modelos automatizados em infraestrutura sem saber exatamente o que eles estão fazendo.
  • Você tem duas ferramentas de linha de comando encorpadas: gcloud e kubectl, e a parte confusa é que elas nomeiam algumas coisas diferentemente, mesmo que sejam as mesmas “coisas”. Pelo menos, o kubectl está perto do docker, se você for familiarizado com isso.

Mais uma vez, este NÃO é um artigo passo a passo. Anotaremos alguns passos, mas não tudo.

Web-Tier Escalável (o próprio aplicativo da Web)

A primeira coisa que você deve ter é um aplicativo web totalmente compatível com 12 fatores.

Seja Ruby on Rails, Django, Laravel, Node.js ou o que for. Deve ser um aplicativo que nada compartilhe, que não dependa de escrever nada no sistema de arquivos local. Um que você possa facilmente desligar e iniciar instâncias de forma independente. Nenhuma sessão de estilo antigo na memória local ou em arquivos locais (prefiro evitar a afinidade da sessão). Nenhum upload para o sistema de arquivos local (se você precisar, você terá que montar um armazenamento persistente externo), sempre prefira enviar fluxos binários para serviços de armazenamento gerenciados.

Você deve ter um pipeline apropriado, que produza cache-busting através de recursos de impressões digitais (e goste disso ou não, a Rails ainda possui a melhor solução disponível no seu Pipeline de Recursos). Você não quer se preocupar com o fracasso manual de caches em CDNs.

Instrua seu aplicativo, adicione New Relic RPM, adicione Rollbar.

Mais uma vez, estamos em 2018, você não quer implantar código ingênuo com injeção de SQL (ou qualquer outra entrada), nenhuma eval não verificada próxima de seu código, nenhum espaço para CSRF ou XSS, etc. Vá em frente, compre a licença para o Brakeman Pro e adicione-o ao seu pipeline CI. Eu posso esperar.

Como isso não é um tutorial, assumirei que você está mais do que apto para se inscrever no Google Cloud e encontrar sua maneira de configurar um projeto, configurar sua região e zona.

Levei um tempo para entender a estrutura inicial no Google Cloud:

  • Você começa com um Projeto, que é o guarda-chuva para tudo o que seu aplicativo precisa.
  • Então você cria “clusters”. Você pode ter um cluster de produção ou de teste, por exemplo. Ou um cluster da web e um cluster de serviços separados para itens não-web, e assim por diante.
  • Um cluster possui um “cluster-master“, que é o controlador de todo o resto (os comandos gcloud e kubectl se comunicam com suas APIs).
  • Um cluster possui muitas “instâncias de node”, as “máquinas” apropriadas (ou, com mais precisão, instâncias VM).
  • Cada cluster também possui pelo menos um “node pool” (o “pool-padrão”), que é um conjunto de instâncias de node com a mesma configuração, o mesmo “tipo de máquina“.
  • Finalmente, cada instância de node executa uma ou mais “pods” que são contêineres leves como LXC. Aqui é onde o seu aplicativo realmente está.

Este é um exemplo de criação de um cluster:

gcloud container clusters create my-web-production \
--enable-cloud-logging \
--enable-cloud-monitoring \
--machine-type n1-standard-4 \
--enable-autoupgrade \
--enable-autoscaling --max-nodes=5 --min-nodes=2 \
--num-nodes 2

Como mencionei, ele também cria um default-pool com um tipo de máquina de n1-standard-4. Escolha qual a combinação de CPU/RAM que você precisará para seu aplicativo particular de antemão. O tipo que escolhi tem quatro vCPUs e 15 GB de RAM.

Por padrão, ele começa com três nodes, então escolhi dois no início, mas auto-escalável para 5 (você pode atualizar isto mais tarde se precisar, mas certifique-se de ter espaço para o crescimento inicial). E você pode continuar adicionando node-pools extras para instâncias de node de tamanho diferente, digamos, para que os trabalhadores do Sidekiq façam um processamento em segundo plano pesado. Em seguida, você deve criar um Node Pool separado com um tipo de máquina diferente para o seu conjunto de instâncias de node, por exemplo:

gcloud container node-pools create large-pool \
--cluster=my-web-production \
--node-labels=pool=large \
--machine-type=n1-highcpu-8 \
--num-nodes 1

Este outro pool controla um node do tipo n1-highcpu-8 que possui 8 vCPUs com 7,2 GB de RAM. Mais CPUs, menos memória. Você tem uma categoria de highmem que é menos CPUs com muito mais memória. Novamente, saiba o que você quer de antemão.

O bit importante aqui é o –node-labels, assim é como eu mapearei a implantação para escolher entre Node Pools (neste caso, entre o default pool e o large pool).

Depois de criar um cluster, você deve emitir o seguinte comando para obter suas credenciais:

gcloud container clusters get-credentials my-web-production

Isso também define o comando kubectl. Se você possui mais de um cluster (digamos, um my-web-production e my-web-staging), você deve ter muito cuidado para sempre levar um get-credentials para o cluster correto primeiro, caso contrário, você pode acabar executando um teste implantação no cluster de produção.

Por isso ser confuso, eu modifiquei o meu ZSH PROMPT para sempre mostrar com qual cluster estou lidando. Eu adaptei do zsh-kubectl-prompt:

Como você acabará tendo vários clusters em um aplicativo grande, eu recomendo que você adicione este prompt ao seu Shell.

Agora, como você implanta seu aplicativo pods nessas instâncias de node extravagantes?

Você deve ter um Dockerfile em seu repositório do projeto de aplicativo para gerar uma imagem Docker. Este é um exemplo para uma aplicação Ruby on Rails:

FROM ruby:2.4.3
ENV RAILS_ENV production
ENV SECRET_KEY_BASE xpto
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash -
RUN apt-get update && apt-get install -y nodejs postgresql-client cron htop vim
ADD Gemfile* /app/
WORKDIR /app
RUN gem update bundler --pre
RUN bundle install --without development test
RUN npm install
ADD . /app
RUN cp config/database.yml.prod.example config/database.yml && cp config/application.yml.example config/application.yml
RUN RAILS_GROUPS=assets bundle exec rake assets:precompile

Do Console da Web do Google Cloud, você encontrará um “Registro de Contêiner“, que é um Registro do Docker Privado.

Você deve adicionar o URL remoto à sua configuração local como esta:

git remote add gcloud https://source.developers.google.com/p/my-project/r/my-app

Agora você pode adicionar git push gcloud master. Eu recomendo que você também adicione triggers para marcar suas imagens. Eu adiciono dois triggers: um para marcá-lo com o latest e outro para marcá-lo com um número de versão aleatório. Você vai precisar deles mais tarde.

Depois de adicionar o repositório de registro como um remoto na sua configuração git (git remote add) e enviar para ele, ele deve começar a criar a imagem do Docker com as marcações apropriadas que você configurou com os disparadores.

Certifique-se de que o seu aplicativo Ruby on Rails não possua nada nos inicializadores que exijem uma conexão com o banco de dados, pois isso não está disponível. Isso é algo com o qual você pode ficar preso quando sua compilação do Docker falha devido à tarefa assets:precompile que carregou um inicializador que chama acidentalmente um Modelo – e que ativa o ActiveRecord::Base para tentar se conectar.

Além disso, certifique-se de que a versão Ruby no Dockerfile combina com a do Gemfile, caso contrário, também falhará.

Observou o config/application.yml estranho acima? Isto é de figaro. Também recomendo que você use algo para facilitar a configuração da variável ENV em seu sistema. Eu não gosto dos segredos do Rails, e ele não é exatamente amigável para a maioria dos sistemas de implantação depois que a Heroku fez o ENV vars onipresente. Fique atento aos ENV vars. O Kubernetes também lhe agradecerá por isso.

Agora, você pode substituir qualquer variável ENV do arquivo YAML de Implantação de Kubernetes. Agora é um bom momento para mostrar um exemplo disso. Você pode nomeá-lo deploy/web.yml ou o que for adequado para sua extravagância e – claro – verifique no seu repositório de código-fonte.

kind: Deployment
apiVersion: apps/v1beta1
metadata:
  name: web
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 1
  minReadySeconds: 10
  replicas: 2
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - image: gcr.io/my-project/my-app:latest
          name: my-app
          imagePullPolicy: Always
          ports:
          - containerPort: 4001
          command: ["passenger", "start", "-p", "4001", "-e", "production",
          "--max-pool-size", "2", "--min-instances", "2", "--no-friendly-error-pages"
          "--max-request-queue-time", "10", "--max-request-time", "10",
          "--pool-idle-time", "0", "--memory-limit", "300"]
          env:
            - name: "RAILS_LOG_TO_STDOUT"
              value: "true"
            - name: "RAILS_ENV"
              value: "production"
            # ... obviously reduced the many ENV vars for brevity
            - name: "REDIS_URL"
              valueFrom:
                secretKeyRef:
                  name: my-env
                  key: REDIS_URL
            - name: "SMTP_USERNAME"
              valueFrom:
                secretKeyRef:
                  name: my-env
                  key: SMTP_USERNAME
            - name: "SMTP_PASSWORD"
              valueFrom:
                secretKeyRef:
                  name: my-env
                  key: SMTP_PASSWORD
            # ... this part below is mandatory for Cloud SQL
            - name: DB_HOST
              value: 127.0.0.1
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: cloudsql-db-credentials
                  key: password
            - name: DB_USER
              valueFrom:
                secretKeyRef:
                  name: cloudsql-db-credentials
                  key: username

        - image: gcr.io/cloudsql-docker/gce-proxy:latest
          name: cloudsql-proxy
          command: ["/cloud_sql_proxy", "--dir=/cloudsql",
                    "-instances=my-project:us-west1:my-db=tcp:5432",
                    "-credential_file=/secrets/cloudsql/credentials.json"]
          volumeMounts:
            - name: cloudsql-instance-credentials
              mountPath: /secrets/cloudsql
              readOnly: true
            - name: ssl-certs
              mountPath: /etc/ssl/certs
            - name: cloudsql
              mountPath: /cloudsql
      volumes:
        - name: cloudsql-instance-credentials
          secret:
            secretName: cloudsql-instance-credentials
        - name: ssl-certs
          hostPath:
            path: /etc/ssl/certs
        - name: cloudsql
          emptyDir:

Há muita coisa acontecendo aqui. Então, deixe-me decompor isso um pouco:

  • O type e a apiVersion são importantes, você deve manter a atenção para a documentação se eles mudarem. Isso é o que é chamado de Implantação. Costumava haver um Controlador de Replicação (você encontrará desses em artigos antigos), mas não está mais em uso. A recomendação é usar um ReplicaSet.
  • Nomeie as coisas corretamente, aqui você possui metadatas:name com web. Também preste muita atenção a spec:template:metadata:labels onde eu rotulo cada pod com um rótulo do app: web, você precisará disso para poder selecionar esses pods mais tarde na seção Serviço abaixo.
  • Então eu tenho spec:strategy onde configuramos o Rolling Update. Portanto, se você tiver 10 pods, ele terminará um, iniciará o novo e continuará fazendo isso, sem nunca tirar tudo de uma só vez.
  • spec:replicas declara quantos Pods eu quero de uma só vez. Você terá que calcular manualmente o tipo de máquina do node-pool. Em seguida, dividir o número total de CPUs/RAM que você possui pelo quanto você precisa para cada instância do aplicativo.
  • Lembre-se da imagem Docker que geramos acima com a marcação ‘latest’? Você se refere a isso em spec:template:spec:containers:image
  • Estou usando Passenger com configuração de produção (verifique a documentação da Phusion, não basta copiar isso).
  • Na seção spec:template:spec:containers:env, posso substituir os ENV vars com os verdadeiros segredos de produção. E você notará que eu posso codificar valores pesadamente ou usar essa estranha engenhoca:
- name: "SMTP_USERNAME"
  valueFrom:
    secretKeyRef:
      name: my-env
      key: SMTP_USERNAME

Agora, está referenciando um armazenamento “Secreto” que eu nomeei “my-env”. E é assim que você cria o seu:

kubectl create secret generic my-env \
--from-literal=REDIS_URL=redis://foo.com:18821 \
--from-literal=SMTP_USERNAME=foobar

Leia a documentação, pois você pode carregar arquivos de texto em vez de declarar tudo da linha de comando.

Como eu disse antes, prefiro usar um serviço gerenciado para um banco de dados. Você pode definitivamente carregar sua própria imagem do Docker, mas eu realmente não recomendo. O mesmo vale para outros serviços semelhantes a bancos de dados, como Redis e Mongo. Se você é da AWS, o Google Cloud SQL é como o RDS.

Depois de criar sua instância PostgreSQL, você não pode acessá-la diretamente do aplicativo da Web. No final, você tem um padrão para uma segunda imagem Docker, um “CloudSQL Proxy“.

Para que isso funcione, você deve primeiro criar uma Conta de Serviço:

gcloud sql users create proxyuser host --instance=my-db --password=abcd1234

Depois de criar a instância do PostgreSQL, ela solicitará que você baixe uma credencial JSON, então tenha cuidado e guarde-a em algum lugar seguro. Eu também não tenho que dizer que você deve gerar uma senha forte e segura. Então, você deve criar segredos extras:

kubectl create secret generic cloudsql-instance-credentials \
--from-file=credentials.json=/home/myself/downloads/my-db-12345.json

kubectl create secret generic cloudsql-db-credentials \
--from-literal=username=proxyuser --from-literal=password=abcd1234

Estes são referenciados nesta parte da Implantação:

- image: gcr.io/cloudsql-docker/gce-proxy:latest
  name: cloudsql-proxy
  command: ["/cloud_sql_proxy", "--dir=/cloudsql",
            "-instances=my-project:us-west1:my-db=tcp:5432",
            "-credential_file=/secrets/cloudsql/credentials.json"]
  volumeMounts:
    - name: cloudsql-instance-credentials
      mountPath: /secrets/cloudsql
      readOnly: true
    - name: ssl-certs
      mountPath: /etc/ssl/certs
    - name: cloudsql
      mountPath: /cloudsql
volumes:
- name: cloudsql-instance-credentials
  secret:
    secretName: cloudsql-instance-credentials
- name: ssl-certs
  hostPath:
    path: /etc/ssl/certs
- name: cloudsql
  emptyDir:

Veja que você deve adicionar o nome do banco de dados (“my-db” neste exemplo), na cláusula -instance no comando.

E, a propósito, o gce-proxy:latest refere-se à versão 1.09 no momento em que esta postagem foi publicada. Mas já havia uma versão 1.11. Aquela me deu dores de cabeça, deixando cair conexões e adicionando um tempo de espera super longo. Então, eu voltei para a 1.09 (mais recente) e tudo funcionou como esperado. Portanto, fique atento!

Nem tudo o que é novo, é bom. Na infraestrutura, você deseja manter-se ao que é estável.

Você também pode querer a opção para carregar uma instância CloudSQL separada em vez de tê-la em cada pod, então os pods podem se conectar a apenas um proxy. Você pode querer ler este tópico sobre o assunto.

Parece que nada está exposto a nada, a menos que você diga isso. Então, precisamos expor esses pods através do chamado Porta de Serviço Node. Vamos também criar um arquivo deploy/web-svc.yaml:

apiVersion: v1
kind: Service
metadata:
  name: web-svc
spec:
  sessionAffinity: None
  ports:
  - port: 80
    targetPort: 4001
    protocol: TCP
  type: NodePort
  selector:
    app: web

É por isso que destaquei a importância da spec:template:metadata:labels, para que possamos usá-la aqui no spec:selector para selecionar os pods apropriados.

Agora podemos implantar esses dois assim:

kubectl create -f deploy/web.yml
kubectl create -f deploy/web-svc.yml

Bom, hoje acabamos por aqui. Caso tenham ficado com qualquer dúvida, deixem seus comentários abaixo e vamos conversando sobre. Será um prazer ajudar!

Na próxima parte do artigo, vamos já começar falando do balanceador de carga. Fique ligado!

***

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