Como colocar uma aplicação Rails em produção no Google Kubernetes Engine
Kubernetes é a ferramenta mais popular para orquestrar containers. Neste guia, vamos ensinar como usar Kubernetes no Google Cloud Platform.
Vemos recentemente um aumento no uso de containers para fazer o deployment de aplicações na nuvem (tanto para ambientes de testes quanto para produção), e para isso Kubernetes surge como uma solução open-source (com uma das comunidades mais ativas do GitHub), muito adotada e simples para orquestração destes containers, que funciona juntamente com o Docker e tem suporte completo na AWS e no GCP.
Neste artigo será apresentado um passo a passo para realizar o deployment no Google Kubernetes Engine, usando o Kubernetes e uma aplicação simples em Rails, que contem um web service e uma Rake task que atualiza um banco de dados MySQL. O web service mostra o nome do host rodando a aplicação e o count (número de linhas) de uma tabela do banco de dados, enquanto a task gera uma nova linha a cada segundo na mesma tabela. A aplicação juntamente com os arquivos do Kubernetes está disponível no GitHub.
Aplicação
Com o simples comando abaixo, já podemos criar nossa aplicação (já quase finalizada, a propósito):
rails new assessing-kubernetes --api --database=mysql --skip-spring
Por praticidade, vamos criar uma action do Rails no próprio ApplicationController
da seguinte forma:
class ApplicationController < ActionController::API
HOSTNAME = `hostname`.strip
def index
render plain: "#{HOSTNAME} | Count: #{BackgroundJob.count}"
end
end# add to config/routes.rb:
# root to: 'application#index'
Em seguida devemos configurar o acesso ao banco de dados (config/database.yml
— irei pular o ambiente de teste e desenvolvimento, pois não é o que estamos tratando aqui) e assim podemos criar o nosso migration (uma tabela simples no banco):
default: &default
adapter: mysql2
encoding: utf8
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
password:production:
<<: *default
host: <%= ENV['ASSESSING-KUBERNETES_DATABASE_HOST'] %>
port: 3306
database: assessing-kubernetes_production
username: assessing-k8s
password: <%= ENV['ASSESSING-KUBERNETES_DATABASE_PASSWORD'] %>
Migration:
class CreateBackgroundJobs < ActiveRecord::Migration[5.2]
def change
create_table :background_jobs do |t|
t.string :hostname, index: true t.timestamps
end
end
end
Após rodar o comando rake db:migrate
seu schema deve ser atualizado, e assim já podemos criar uma Rake task que simula o comportamento de um worker executando em background (arquivo lib/tasks/worker.rake
):
namespace :worker do
desc 'Run the worker'
task :run => :environment do
hostname = `hostname`
while true
BackgroundJob.create(hostname: hostname)
puts "#{hostname}: created a new BackgroundJob"
sleep 1
end
end
end
Com isso, podemos começar o setup do ambiente. Todas as mudanças feitas na aplicação Rails até este ponto podem ser vistas neste diff.
Banco de Dados (CloudSQL)
Após criar seu projeto no Google Cloud, a primeira coisa a fazer é ativar a API Cloud SQL Administration (em gerenciamento de recurso — é uma etapa muito importante, não pule). Logo depois já podemos criar nossa instância de banco de dados (BD).
Você pode criar o BD usando o painel do GCP para selecionar as opções mais facilmente (mas também pode usar a ferramenta gcloud, disponível no próprio Cloud Shell).
Para este guia, uma instância db-f1-micro, com 10GB HDD (o mínimo) é suficiente. Selecione sua zona preferida (lembre-se de colocar tudo que usa na mesma zona) e defina as opções padrões e finalize a criação.
Para que seu cluster (mais precisamente os containers) do Kubernetes se conectem ao BD é necessário criar uma service account (conta de serviço — menu de IAM & admin). Esta conta deve possuir as seguintes características:
- Papel: Cliente do CloudSQL
- Dê um nome e um id
- Clique em
Fornecer uma nova chave privada
Criar
- Salve o arquivo(
<proxy_key.json>
)
Após criar esta conta precisamos criar o usuário no banco, isto pode ser feito pelo console, clique na instância do CloudSQL e vá na aba usuários, clique em criar conta de usuário — Em nome devemos colocar o especificado no arquivo de configuração do banco da aplicação, portanto, assessing-k8s. Crie uma senha que achar boa (<senha_db>
), e pode manter checado Allow any host (por praticidade para seguir o guia), ou então inserir na outra opção cloudsqlproxy~%, que irá permitir o acesso usando o Cloud Proxy.
O Cloud Proxy é uma aplicação do própio Google Cloud que permite a utilização de proxy e de uma conta de serviço para conectar-se ao banco de dados, portanto é a maneira mais simples de conectar as aplicações rodando em containers do Kubernetes ao banco do CloudSQL. Para isso será necessário incluir um container desta aplicação junto (na mesma pod) com os containers da nossa aplicação como veremos em breve.
Setup do kubernetes
Agora que já temos o restante do ambiente pronto podemos finalmente começar com o Kubernetes, e a primeira coisa a se fazer é criar o cluster em Kubernetes Engine — novamente o jeito mais simples é pelo console. Para facilitar a escolha das opções, lembre-se da zona escolhida, dê o nome que preferir (<nome_cluster>
), escolha a quantidade e o tipo de máquina (a configuração mais em conta atualmente são 3 maquinas f1-micro — mínimo permitido), as demais opções podem ser mantidas ou alteradas como preferir (eu desabilitei o stackdriver para este guia, por exemplo).
Após clicar em create, o próprio GCP irá cuidar de criar e inicializar as máquinas (os nós) que poderão ser vistos na aba Compute Engine.
O jeito mais simples de rodar os comandos do Kubernetes (kubectl) é usando o Cloud Shell (que inclusive já vem com o mesmo instalado), portanto, abra o Cloud Shell. A primeira coisa que temos que fazer agora é informar ao kubectl qual o contexto (Google Kubernetes Engine) e as credencias do nosso cluster, para que ele saiba onde efetuar seus comandos. Como estamos rodando no Cloud Shell esse processo se torna muito simples:
# Defina as credenciais de acesso ao cluster
gcloud container clusters get-credentials assessing-kube-cluster# Defina o contexto
kubectl config set-context gce
Isto deve ser suficiente para que o kubectl do Cloud Shell se comunique com seu cluster. Podemos agora iniciar com a criação dos secrets, objetos do Kubernetes que armazenam informações sensíveis como senhas e credenciais. Ou seja, iremos armazenar nossas credenciais da conta de serviço do Cloud SQL e as necessárias para aplicação, como <senha_db> e <rails_master_key>
para as credenciais da conta de serviço. Primeiramente crie um arquivo no Cloud Shell e copie o conteúdo do <proxy_key.json>
, salve e então crie o secret:
kubectl create secret generic cloudsql-instance-credentials --from-file=credential.json=<proxy_key.json>
Para o secret da aplicação, como estamos testando algo simples, podemos inserir diretamente usando literais:
kubectl create secret generic assessing-kubernetes-secrets --from-literal=db_pass='<pass-sql> --from-literal=rails_master_key=<key>
Agora que já temos o ambiente configurado com as informações sensíveis, podemos passar para o deploy.
Deploy
Para o deploy de uma aplicação Rails precisamos do código (não há executável por ser interpretado), portanto precisamos clonar todo o repositório no nosso Cloud Shell:
git clone https://github.com/infosimples/assessing-kubernetes.git
.
A primeira coisa que precisamos fazer agora que já temos todo o código é construir a imagem da aplicação e subi-la para o Google Container Registry, para que o Kubernetes possa ler e criar um container com esta imagem. Para isso foi criado um script em /kubernetes-prod/build.sh que recebe o número da versão como argumento e cria a imagem:
sh assessing-kubernetes/kubernetes-prod/build.sh 0.0.1
Este script cria uma imagem da aplicação com os requisitos da mesma, porém há um outro build que pega esta imagem e já realiza o bundle antes de enviar para o registry. Este passo pode demorar alguns minutos (até mesmo para iniciar).
Para realizar o deploy bastaria usar o script (na mesma pasta) deploy.sh
, porém podemos entender o que é feito neste script.
O Kubernetes é operado por meio de configurações, portanto há vários arquivos de configuração; nesta aplicação simples usaremos apenas 3 tipos de arquivos, sendo dois que descrevem deployments, que é a aplicação executando em si (um para o web service e um para o worker) e um que descreve um service, que representa basicamente um end-point para um deployment que necessita de acesso via rede externa ao cluster (no caso, o web service).
O serviço é bem simples, precisamos dar um nome à ele; colocar um selector, através do qual os deployments irão informar que usam este serviço; o tipo, cujos principais são ClusterIP (basicamente serviço interno do cluster) e LoadBalancer (expõe uma IP externo e faz load balance entre todas pods que o utilizarem) — no caso iremos utilizar o Loadbalancer para nosso web service; e finalmente as portas de rede que são mapeadas na aplicação, na qual deve ser informada a porta da pod e a porta do serviço (a que estará disponível para fora), no final temos:
kind: Service
apiVersion: v1
metadata:
name: web
spec:
selector:
app: web
ports:
- protocol: TCP
port: 80
targetPort: 3100
type: NodePort
Os deployments são um pouco mais complicados, mas seguem uma lógica parecida (e quase idêntica entre eles). Tem o nome também e o selector (no caso do deployment web — que é o nosso web service— deve ser o mesmo do serviço, no caso do deployment worker — que é nossa Rake task — não é sequer necessário), mas agora passar as informações sobre os containers em cada pod (pode e haverá mais de 1 por pod), cada container precisa de um nome; a imagem de onde cria o container; o comando a ser executado (é um array de strings, cada palavra do comando deve ser um item do array); se necessário as variáveis de ambiente (que podem ser mapeadas com literais ou para secrets — criados anteriormente); também se necessário volumes (armazenamento persistente ou dinâmico — não entraremos em detalhes aqui) e ainda portas de rede do container também caso necessário. Com isso temos nossos deploys:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
template:
metadata:
labels:
app: web
track: stable
spec:
containers:
- name: web
image: gcr.io/assessing--kubernetes/kube:0.0.1
args: ["bundle","exec","rails","server","--port","3100","--binding","0.0.0.0"]
env:
- name: RAILS_ENV
value: production
- name: ASSESSING-KUBERNETES_DATABASE_HOST
value: 127.0.0.1 # Local, because using cloudproxy
- name: RAILS_MASTER_KEY
valueFrom:
secretKeyRef:
name: assessing-kubernetes-secrets
key: rails_master_key
- name: ASSESSING-KUBERNETES_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: assessing-kubernetes-secrets
key: db_pass
ports:
- name: http
containerPort: 3100
- name: cloudsql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.11
command: ["/cloud_sql_proxy",
"-instances=assessing--kubernetes:us-central1:kube-mysql=tcp:3306",
"-credential_file=/secrets/cloudsql/credentials.json"]
volumeMounts:
- name: cloudsql-instance-credentials
mountPath: /secrets/cloudsql
readOnly: true
volumes:
- name: cloudsql-instance-credentials
secret:
secretName: cloudsql-instance-credentials
Como informado precisamos de 2 containers, um da nossa aplicação e um daquele cloudsql-proxy, descrito na parte do banco de dados — e este container no caso usa inclusive um volume persistente com aquele secret de credenciais.
O deployment do worker é muito parecido, porém não precisa de portas de rede, e o comando do container da aplicação é outro:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: worker
spec:
replicas: 1
template:
metadata:
labels:
app: worker
track: stable
spec:
containers:
- name: worker
image: gcr.io/assessing--kubernetes/kube:0.0.1
args: ["bundle","exec","rake","worker:run"]
env:
- name: RAILS_ENV
value: production
- name: ASSESSING-KUBERNETES_DATABASE_HOST
value: 127.0.0.1 # Local, because using cloudproxy
- name: RAILS_MASTER_KEY
valueFrom:
secretKeyRef:
name: assessing-kubernetes-secrets
key: rails_master_key
- name: ASSESSING-KUBERNETES_DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: assessing-kubernetes-secrets
key: db_pass
- name: cloudsql-proxy
image: gcr.io/cloudsql-docker/gce-proxy:1.11
command: ["/cloud_sql_proxy",
"-instances=assessing--kubernetes:us-central1:kube-mysql=tcp:3306",
"-credential_file=/secrets/cloudsql/credentials.json"]
volumeMounts:
- name: cloudsql-instance-credentials
mountPath: /secrets/cloudsql
readOnly: true
volumes:
- name: cloudsql-instance-credentials
secret:
secretName: cloudsql-instance-credentials
Agora que já temos todos nossos yamls (arquivos de configuração) podemos finalmente realizar o deploy, usando o script: sh assessing-kubernetes/kubernetes-prod/deploy.sh
; este script faz o deploy do serviço primeiro e depois dos deployments (esta é a prática recomendada do kubernetes). Mas são comandos simples também — estando na pasta assessing-kubernetes/kubernetes-prod basta:
kubectl apply -f services
kubectl apply -f deployments
Isto porque o apply -f
se aplica a pastas inteiras.
Para realizar uma atualização, caso necessário, é muito simples, após alterar o código, basta criar uma nova versãosh assessing-kubernetes/kubernetes-prod/build.sh <nova_versao>
; alterar os yamls de deployments para corresponder a nova versão (na tag image
alterar 0.0.1 para <nova_versao>
) e enviar:kubectl apply -f deployments
.
Com isso a aplicação já estará plenamente rodando, inclusive com um IP externo, este pode ser obtido detalhando o serviço: kubectl get services
, onde mostrará um External IP, que pode ser acessado por qualquer browser, lembre -sede indicar a porta 3100
Há uma série de comandos que podem ser úteis, como por exemplo para escalar um deployment (para que tenha mais de uma pod — pode estar especificado no yaml também), para usar o dashboard do Kubernetes, para dar rollback em um deployment, para pausar/resumir um deployment, selecionar mínimos e máximos de CPU/memória a serem usadas pelas pods (que também pode ser definido no yaml), dentre outros. Alguns destes comando estão descrito ao final do README do nosso repositório.
Servidor web (não esqueça como eu esqueci)
A aplicação rails roda em um servidor de aplicação, há diversas opções neste caso usamos o puma, porém há varias questões de vulnerabilidade e eficiência ao se expor o servidor de aplicações diretamente, precisa de um servidor web, que é melhor para estas questões especificas da comunicação (mais relacionados aos protocolos da comunicação por rede). Para isto iremos usar o Nginx, um dos servidores web mais utilizados (inclusive muito presente nos tutoriais de Kubernetes.
Se fossemos usar um Nginx puro, como precisaríamos há alguns meses atrás, deveríamos criar um deploy de configurações, além de configurar muitas outras coisas na mão, além de precisar de um container em cada pod que rodando o servidor de aplicação, porém recentemente a própria comunidade do Kubernetes lançou o ingress-nginx, que basicamente já vem com muitas das dificuldades de configuração.
Novamente se fossemos fazer este deploy também há poucos meses atrás precisaríamos escrever muitos arquivos yamls do Kubernetes, como descrito por akita, na época ainda estava em beta esse ingress-nginx. Atualmente é algo muito mais simples.
Primeiramente para esta aplicação resolver alguns problemas de configuração necessita roles específicas, e para podermos fornecer estas roles, precisamos também possuir uma role de admin (no nosso próprio usuário), isto pode ser feito com:
kubectl create clusterrolebinding cluster-admin-binding \
--clusterrole cluster-admin \
--user $(gcloud config get-value account)
Em seguida, seguindo o tutorial, podemos fazer o deploy direto dos yamls disponibilizados no github (do próprio ingress-nginx), se ainda precisarmos de configurações específicas precisaríamos copiar o conteúdo destes yamls e editar conforme necessário (por exemplo adicionar configurações no ConfigMap nginx-configuration):
# Deploy de arquivos essenciais
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml# Deploy de arquivos específicos ao GCP
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/cloud-generic.yaml
E finalmente para especificarmos qual serviço deve ser acessado pelo nginx, criamos um ingress (que gerencia os acessos externos aos serviços, portanto expondo nosso servidor de aplicação ao servidor web):
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: nginx-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- http:
paths:
- path: /
backend:
serviceName: web
servicePort: 80
No caso optamos por remover o ssl (https), isso porde ser facilmente obtido removendo a linha: nginx.ingress.kubernetes.io/ssl-redirect: "false"
— que então irá redirecionar todo trafego para https, para incluir nossos certificados ssl (caso tenhamos algum):
kubectl create secret tls <nome_certificado> --key <arquivo_chave_certificado> --cert <arquivo_certificado>
Conclusões
A aplicação em si é bem simples e o esforço para se fazer o primeiro deployment acabou se mostrando grande, como vimos, especialmente para criar cada um dos arquivos de configuração. Pressupondo que uma aplicação possa usar muito mais recursos do Kubernetes, com muitas configurações diferentes, isso pode gerar um esforço ainda maior para fazer o deployment.
Porém, como enfatizei anteriormente, este esforço se mostra apenas no primeiro deployment, quando é necessário fazer todo o setup do ambiente, já nos deploys seguintes (atualizações), o esforço é baixo e o processo é muito facilitado pelo uso do Kubernetes (o passo mais lento, porém simples, é o build das imagens, especialmente se feito pelo Cloud Shell).
Concluindo, Kubernetes é uma ferramenta extremamente útil para realizar deployment em ambientes de produção, fácil e ágil, e juntamente com outras ferramentas pode automatizar todo o processo de deployment (integração contínua). O esforço grande de configuração pode ser minimizado, ou pelo menos facilitado, com uso de ferramentas de abstração do Kubernetes como o OpenShift.