
Ao construir um processo de CI/CD utilizando Kubernetes, às vezes surge o problema da incompatibilidade entre os requisitos da nova infraestrutura e o aplicativo a ser migrado para ela. Em particular, na fase de construção do aplicativo, é importante obter uma uma imagem que será utilizada em todas ambientes e clusters do projeto. Esse princípio é a base da correta (já falamos sobre isso e nosso diretor técnico).
No entanto, não é raro encontrar situações em que um framework pronto é utilizado no código do site, o que impõe restrições ao seu uso futuro. E se em um "ambiente comum" isso é fácil de lidar, no Kubernetes esse tipo de comportamento pode se tornar um problema, especialmente ao encontrá-lo pela primeira vez. Embora uma mente inventiva possa sugerir soluções de infraestrutura que parecem óbvias e até boas à primeira vista… é importante lembrar que a maioria das situações pode e deve ser resolvida de forma arquitetônica..
Vamos analisar soluções de workaround populares para o armazenamento de arquivos, que podem levar a consequências indesejadas na operação do cluster, além de indicar um caminho mais apropriado.
Armazenamento de estáticos.
Para ilustrar, consideremos um aplicativo web que utiliza um gerador de estáticos para produzir um conjunto de imagens, estilos e outros. Por exemplo, no framework PHP Yii, há um gerenciador de ativos embutido que gera nomes de diretórios exclusivos. Assim, é gerado um conjunto de caminhos que não se interceptam entre si para os estáticos do site (isso é feito por várias razões — por exemplo, para evitar duplicatas ao utilizar o mesmo recurso em múltiplos componentes). Assim, de forma padrão, ao acessar o módulo do recurso web pela primeira vez, ocorre a formação e distribuição dos estáticos (na verdade, muitas vezes são links simbólicos, mas falaremos sobre isso mais tarde) com um diretório raiz comum único para essa implantação:
-
webroot/assets/2072c2df/css/… -
webroot/assets/2072c2df/images/… -
webroot/assets/2072c2df/js/…
Quais as implicações disso no contexto do cluster?
Um exemplo simples.
Vamos pegar um caso bastante comum, onde o nginx está à frente do PHP para servir os estáticos e processar requisições simples. O método mais simples é Deployment com dois contêineres:
apiVersion: apps/v1
kind: Deployment
metadata:
name: site
spec:
selector:
matchLabels:
component: backend
template:
metadata:
labels:
component: backend
spec:
volumes:
- name: nginx-config
configMap:
name: nginx-configmap
containers:
- name: php
image: own-image-with-php-backend:v1.0
command: ["/usr/local/sbin/php-fpm","-F"]
workingDir: /var/www
- name: nginx
image: nginx:1.16.0
command: ["/usr/sbin/nginx", "-g", "daemon off;"]
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf
subPath: nginx.confEm termos simplificados, a configuração do nginx resume-se ao seguinte:
apiVersion: v1
kind: ConfigMap
metadata:
name: "nginx-configmap"
data:
nginx.conf: |
server {
listen 80;
server_name _;
charset utf-8;
root /var/www;
access_log /dev/stdout;
error_log /dev/stderr;
location / {
index index.php;
try_files $uri $uri/ /index.php?$args;
}
location ~ .php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
}
} Quando a página é acessada pela primeira vez no container PHP, os assets aparecem. Mas, no caso de dois containers em um único pod, o nginx não sabe sobre esses arquivos estáticos, que (de acordo com a configuração) deveriam ser entregues a ele. Como resultado, em todas as requisições de arquivos CSS e JS, o cliente verá erro 404. A solução mais simples seria organizar um diretório compartilhado entre os containers. Uma opção primitiva é um compartilhado. emptyDir:
apiVersion: apps/v1
kind: Deployment
metadata:
name: site
spec:
selector:
matchLabels:
component: backend
template:
metadata:
labels:
component: backend
spec:
volumes:
- name: assets
emptyDir: {}
- name: nginx-config
configMap:
name: nginx-configmap
containers:
- name: php
image: own-image-with-php-backend:v1.0
command: ["/usr/local/sbin/php-fpm","-F"]
workingDir: /var/www
volumeMounts:
- name: assets
mountPath: /var/www/assets
- name: nginx
image: nginx:1.16.0
command: ["/usr/sbin/nginx", "-g", "daemon off;"]
volumeMounts:
- name: assets
mountPath: /var/www/assets
- name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf
subPath: nginx.confAgora, os arquivos estáticos gerados no container são entregues corretamente pelo nginx. Mas lembro que esta é uma solução primitiva, o que significa que está longe do ideal e tem suas nuances e falhas, que serão discutidas a seguir.
Um armazenamento mais avançado
Agora, vamos imaginar uma situação em que o usuário acessou o site, carregou a página com os estilos disponíveis no contêiner e, enquanto ele estava lendo essa página, nós fizemos o redeployment do contêiner. No diretório de ativos, ficou vazio e é necessário fazer uma solicitação ao PHP para iniciar a geração de novos. No entanto, mesmo após isso, os links para a antiga estática estarão desatualizados, o que levará a erros de exibição da estática.
Além disso, provavelmente temos um projeto bastante carregado, o que significa que uma única cópia do aplicativo não será suficiente:
- Escalaremos Deployment para duas réplicas.
- Na primeira solicitação ao site, em uma réplica, os ativos foram criados.
- Em algum momento, o ingress decidiu (para balancear a carga) enviar a solicitação para a segunda réplica, e lá esses ativos ainda não estão. Ou pode ser que eles já não estejam lá, porque estamos usando
RollingUpdatee neste momento estamos fazendo o deploy.
No geral, o resultado é — novamente erros.
Para não perder os antigos ativos, é possível alterar emptyDir para hostPath, armazenando a estática fisicamente no nó do cluster. Essa abordagem é ruim porque, na prática, precisamos vincular o nosso aplicativo a um nó específico do cluster, porque — se mudarmos para outros nós — o diretório não conterá os arquivos necessários. Ou será necessário algum tipo de sincronização em segundo plano do diretório entre os nós.
Quais são as possíveis soluções?
- Se o hardware e os recursos permitirem, podemos usar para organizar um diretório de acesso igual para as necessidades da estática. recomenda discos SSD, pelo menos uma replicação tripla e uma conexão "grossa" estável entre os nós do cluster.
- Uma opção menos exigente seria a organização de um servidor NFS. No entanto, então precisamos considerar a possível elevação do tempo de resposta no processamento das solicitações pelo servidor web, e a resistência a falhas ficará a desejar. As consequências de uma falha são catastróficas: a perda do mount condena o cluster à morte sob a pressão da carga LA, que dispara para o céu.
Além de tudo, para todas as opções de criação de armazenamento permanente será necessária uma limpeza em segundo plano de conjuntos de arquivos obsoletos, acumulados ao longo de um certo período de tempo. Na frente dos contêineres com PHP, podemos colocar DaemonSet caches do nginx, que armazenarão cópias dos ativos por um tempo limitado. Esse comportamento é facilmente configurável com proxy_cache com profundidade de armazenamento em dias ou gigabytes de espaço em disco.
Combinar este método com os sistemas de arquivos distribuídos mencionados acima oferece um vasto campo para a fantasia, com a única limitação sendo o orçamento e o potencial técnico daqueles que irão implementá-lo e mantê-lo. De nossa experiência, podemos dizer que quanto mais simples a sistema, mais estável ele funciona. Ao adicionar tais camadas, manter a infraestrutura se torna muito mais complicado e, juntamente com isso, aumenta o tempo gasto na diagnóstica e na recuperação de qualquer falha.
Recomendação
Se a implementação das opções de armazenamento propostas também lhe parecer injustificada (difícil, cara…), vale a pena olhar a situação de outra perspectiva. Ou seja, aprofundar-se na arquitetura do projeto e erradicar o problema no código, vinculando-se a uma estrutura de dados estática na imagem, uma definição clara do conteúdo ou do procedimento de "aquecimento" e/ou pré-compilação de ativos na fase de construção da imagem. Assim, obtemos um comportamento absolutamente previsível e um conjunto idêntico de arquivos para todos os ambientes e réplicas da aplicação em execução.
Se voltarmos ao exemplo específico do framework Yii e não aprofundarmos em sua estrutura (o que não é o objetivo deste artigo), basta apontar duas abordagens populares:
- Alterar o processo de construção da imagem de modo a colocar os ativos em um local previsível. Isso é sugerido/implementado em extensões como .
- Definir hashes específicos para diretórios de ativos, como é descrito, por exemplo, em (começando do slide nº 35). Aliás, o autor da apresentação, eventualmente (e não sem razão!) recomenda que, após a construção dos ativos no servidor de build, eles sejam carregados em um repositório central (como S3), na frente do qual deve ser colocado um CDN.
Arquivos a serem carregados
Outro caso que certamente será relevante ao migrar a aplicação para um cluster Kubernetes é o armazenamento de arquivos do usuário no sistema de arquivos. Por exemplo, temos novamente uma aplicação PHP que aceita arquivos através de um formulário de upload, realiza alguma ação neles durante o processamento e os devolve.
O local onde esses arquivos devem ser armazenados, na realidade do Kubernetes, deve ser comum a todas as réplicas da aplicação. Dependendo da complexidade da aplicação e da necessidade de organização da persistência desses arquivos, esse local pode ser as opções de dispositivos compartilhados mencionadas acima, mas, como vemos, eles têm suas desvantagens.
Recomendação
Uma das opções para resolver isso é usar um armazenamento compatível com S3 (mesmo que seja algum tipo da categoria self-hosted como minio). A transição para o S3 exigirá mudanças no nível do código, e como o conteúdo será entregue no front-end, já discutimos .
Sessões do usuário
Vale destacar a organização do armazenamento de sessões de usuário. Muitas vezes, isso também envolve arquivos no disco, o que, no contexto do Kubernetes, levará a constantes solicitações de autorização do usuário se sua solicitação cair em outro contêiner.
Parte do problema é resolvido habilitando stickySessions no ingress (o recurso é suportado em todos os controladores de ingress populares - veja mais em ), para vincular o usuário a um pod específico com a aplicação:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: nginx-test
annotations:
nginx.ingress.kubernetes.io/affinity: "cookie"
nginx.ingress.kubernetes.io/session-cookie-name: "route"
nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"
spec:
rules:
- host: stickyingress.example.com
http:
paths:
- backend:
serviceName: http-svc
servicePort: 80
path: /Mas isso não eliminará os problemas em implantações repetidas.
Recomendação
Uma abordagem mais correta seria migrar a aplicação para armazenamento de sessões em memcached, Redis e soluções similares — em suma, eliminar completamente as opções baseadas em arquivos.
Conclusão
As soluções de infraestrutura discutidas no texto merecem ser aplicadas apenas como "gambiarras" temporárias (o que soa bem mais bonito em inglês como workaround). Elas podem ser relevantes nas primeiras etapas da migração da aplicação para o Kubernetes, mas não devem "enraizar-se".
O caminho recomendado consiste em se livrar delas em favor de uma reestruturação arquitetônica da aplicação de acordo com o já amplamente conhecido . No entanto, isso - transformar a aplicação em um formato stateless - inevitavelmente significa que serão necessárias mudanças no código, e aqui é importante encontrar o equilíbrio entre as possibilidades/requisitos do negócio e as perspectivas de implementação e manutenção do caminho escolhido.
P.S.
Confira também em nosso blog:
- «»;
- «»;
- «» (da Red Hat);
- «».
Fonte: habr.com
