Pontos Principais
- Baseado em diversas definições sobre o assunto, podemos dizer que cloud-native é um termo usado para descrever ambientes baseados em containers.
- Quais são as boas práticas a serem utilizadas no desenvolvimento de uma solução cloud-native?
- Confira o código de exemplo de uma aplicação cloud-native e saiba como rodá-la em uma ambiente PaaS.
No primeiro post dessa série, exploramos o funcionamento do Jakarta NoSQL, uma das grandes novidades da plataforma Jakarta EE . Neste segundo post, falaremos sobre o que é o cloud-native e como criar a aplicação baseada nesses princípios, utilizando o Jakarta EE e uma solução PaaS que facilita essa integração.
Mudando para o cloud-native
A computação em nuvem trouxe muitas metodologias e técnicas que revolucionaram o mundo técnico e comercial. Entre os novos termos cunhados, há o cloud-native. Para atender e estar a par das expectativas no universo Java, criou-se o Jakarta EE. O objetivo deste artigo é comentar sobre o conceito do cloud-native e executar uma aplicação usando-o juntamente com a versão mais recente do Jakarta EE NoSQL.
O que é o cloud-native?
Como qualquer conceito novo, existem diversas definições com o mesmo nome. Se lermos livros ou artigos sobre cloud-native poderemos não encontrar um consenso sobre o termo. Por exemplo:
Cloud-native é uma abordagem para criar e executar aplicações que explora as vantagens do modelo de computação em nuvem.
Cloud-native é uma modo diferente de pensar e raciocinar sobre sistemas de software. Incorpora os seguintes conceitos: Produzido por uma infraestrutura descartável, composta de limites, globalmente escalável, dotada de uma arquitetura descartável.
Em uso geral, "cloud-native" é uma abordagem para criar e executar aplicações que exploram as vantagens do modelo de entrega baseados na computação em nuvem. "Cloud-native" é sobre como, e não onde, as aplicações são criados e implantadas.
Em um entendimento baseado nas definições de diversos artigos, podemos dizer que cloud-native é um termo usado para descrever ambientes baseados em containers. Portanto, não está relacionado a linguagens ou estruturas de programação específicas, ou mesmo a uma empresa provedora de nuvem, mas a containers.
Quais são as melhores práticas quando falamos sobre cloud-native?
Quando começamos a aprender um novo conceito, normalmente buscamos ler sobre suas melhores práticas, a fim de evitar erros e qualquer code smell (problemas que podem ocorrer devido o não conhecimento das práticas e design). Com a Programação Orientada a Objetos (OOP), temos os padrões de design da Gang dos Quatro, em Java, temos o Java Efetivo e, ao falar sobre arquitetura, temos o Código Limpo e a Arquitetura Limpa. Portanto, a pergunta é: Quais são as melhores práticas para o cloud-native?
Não existem práticas recomendadas relacionadas especificamente ao nativo da nuvem, ao menos até onde sabemos. Mas como a nuvem está próxima da metodologia Agile, há várias práticas que podemos utilizar para termos uma aplicação saudável e indolor, além das citadas anteriormente:
- Manifesto for Agile Software Development
- Continuous integration
- Continuous delivery
- Domain-Driven Design
As práticas mais conhecidas relacionadas a qualquer aplicação que inclua computação em nuvem são inspiradas nos Patterns of Enterprise Application Architecture e Refactoring, de Martin Fowler.
A Aplicação Doze Fatores
- Codebase: Uma base de código com rastreamento utilizando controle de revisão, vários deploys;
- Dependencies: Declare e isole as dependências;
- Config: Armazene as configurações no ambiente;
- Backing services: Trate os serviços de apoio, como recursos interligados;
- Build, release, run: Separe estritamente os builds e execute-os em etapas;
- Processes: Execute a aplicação como um ou mais processos que são stateless;
- Port binding: Exporte serviços usando port binding;
- Concurrency: Dimensione usando um modelo de processo;
- Disposability: Maximize a robustez com inicialização e desligamento rápido;
- Dev/prod parity: Mantenha os ambientes de desenvolvimento, teste, produção o mais semelhante possível;
- Logs: Trate os logs como fluxo de eventos;
- Admin processes: Execute tarefas de administração/gerenciamento como processos pontuais;
Em resumo, ainda não existem práticas recomendadas mais específicas para o cloud-native, mas existem padrões do Agile, microservices e a aplicação de doze fatores que são importantes, e podem ser seguidas.
De volta ao código
Na introdução, explicamos com detalhes, o que significa cloud-native. Agora vamos retornar a nossa aplicação e convertê-la para uma aplicação cloud-native. No primeiro artigo, explicamos o modelo, a entidade e como o Jakarta NoSQL funciona. Portanto, usaremos a maneira mais fácil de lidar com as consultas com NoSQL e MongoDB com um Repository
.
import jakarta.nosql.mapping.Column;
import jakarta.nosql.mapping.Entity;
import jakarta.nosql.mapping.Id;
import javax.json.bind.annotation.JsonbVisibility;
import java.io.Serializable;
import java.util.Objects;
import java.util.Set;
@Entity
@JsonbVisibility(FieldPropertyVisibilityStrategy.class)
public class Hero implements Serializable {
@Id
private String name;
@Column
private Integer age;
@Column
private Set<String> powers;
}
import jakarta.nosql.mapping.Page;
import jakarta.nosql.mapping.Pagination;
import jakarta.nosql.mapping.Repository;
import java.util.stream.Stream;
public interface HeroRepository extends Repository<Hero, String> {
Stream<Hero> findAll();
Page<Hero> findAll(Pagination pagination);
Stream<Hero> findByPowersIn(String powers);
Stream<Hero> findByAgeGreaterThan(Integer age);
Stream<Hero> findByAgeLessThan(Integer age);
}
Para disponibilizar os serviços, criaremos uma aplicação REST com JAX-RS como sendo uma classe de recurso.
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.function.Supplier;
import static java.util.stream.Collectors.toList;
@ApplicationScoped
@Path("heroes")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class HeroResource {
private static final Supplier<WebApplicationException> NOT_FOUND =
() -> new WebApplicationException(Response.Status.NOT_FOUND);
@Inject
private HeroRepository repository;
@GET
public List<Hero> findAll() {
return repository.findAll()
.collect(toList());
}
@GET
@Path("/{id}")
public Hero findById(@PathParam("id") String id) {
return repository.findById(id).orElseThrow(NOT_FOUND);
}
@GET
@Path("seniors/{age}")
public List<Hero> findByOlder(@PathParam("age") Integer age) {
return repository.findByAgeGreaterThan(age)
.collect(toList());
}
@GET
@Path("youngs/{age}")
public List<Hero> findByYounger(@PathParam("age") Integer age) {
return repository.findByAgeLessThan(age)
.collect(toList());
}
@POST
public void save(Hero hero) {
repository.save(hero);
}
@PUT
@Path("/{id}")
public void update(@PathParam("id") String id, Hero hero) {
repository.save(hero);
}
@Path("/{id}")
@DELETE
public void delete(@PathParam("id") String name) {
repository.deleteById(name);
}
}
A aplicação está pronta. A última etapa, que criaremos é a classe de configuração que permite a conexão com o MongoDB. É bem simples. Usaremos o Eclipse MicroProfile Configuration que possui recursos com o Eclipse JNoSQL, a implementação de referência do Jakarta NoSQL. O Eclipse MicroProfile Config é uma solução para externalizar a configuração das aplicações Java e facilita o acompanhamento do terceiro fator.
import jakarta.nosql.document.DocumentCollectionManager;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Disposes;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
@ApplicationScoped
class DocumentManagerProducer {
@Inject
@ConfigProperty(name = "document")
private DocumentCollectionManager manager;
@Produces
public DocumentCollectionManager getManager() {
return manager;
}
public void destroy(@Disposes DocumentCollectionManager manager) {
manager.close();
}
}
A configuração atual de uma aplicação pode ser acessada via ConfigProvider#getConfig().
Um Config consiste nas informações coletadas dos org.eclipse.microprofile.config.spi.ConfigSources registrados. Esses ConfigSource são classificados de acordo com seu ordinal. Dessa forma, podemos substituir a configuração com menor importância externamente.
Por padrão, existem 3 ConfigSources:
- System.getProperties() (ordinal=400);
- System.getenv() (ordinal=300);
- Todos os arquivos META-INF/microprofile-config.properties no ClassPath. (ordinal padrão=100, configurável separadamente por meio de uma propriedade config_ordinal dentro de cada arquivo).
Portanto, os valores padrão podem ser especificados nos arquivos acima fornecidos com a aplicação e o valor pode ser substituído posteriormente para cada implantação. Um número ordinal mais alto tem preferência sobre um número mais baixo.
Isso implica que podemos ter a configuração do ambiente local como um arquivo, uma para teste, também como um arquivo e, podemos substituir todas essas informações quando as movermos para a nuvem.
document=document
document.database=conferences
document.settings.jakarta.nosql.host=localhost:27017
document.provider=org.eclipse.jnosql.diana.mongodb.document.MongoDBDocumentConfiguration
Agora temos uma configuração local, então vamos mudar nossa aplicação com o Jakarta EE com base na abordagem cloud-native. Para facilitar, usaremos uma PaaS (Plataforma como Serviço) porque podemos mover a aplicação baseada em container da aplicação da nuvem através da infraestrutura como código (IaC).
A infraestrutura como código, ou infraestrutura programável, nada mais é do que escrever um código, que pode ser feito usando uma linguagem de alto nível ou qualquer linguagem descritiva para gerenciar configurações e automatizar o provisionamento da infraestrutura, além das implantações.
Estrutura do Platform.sh
A aplicação Java está pronta para ser usada! A próxima etapa é definir os arquivos Platform.sh necessários para gerenciar e implantá-la. Em nosso primeiro artigo sobre o Java, analisamos detalhadamente cada um desses três arquivos:
- Um roteador (.platform / routes.yaml). O Platform.sh permite definir as rotas;
- Zero ou mais containers de serviço (.platform/services.yaml). O Platform.sh permite definir e configurar completamente a topologia e os serviços que desejamos utilizar no nosso projeto;
- Um ou mais containers de aplicações (.platform.app.yaml). Controlamos a aplicação e a maneira como será criada e implantada no Platform.sh por meio de um único arquivo de configuração.
O arquivo que será alterado nesta postagem é o arquivo de serviço, permitindo definir um banco de dados, mecanismo de pesquisa, cache e assim por diante. Para este projeto, vamos usar o MongoDB ao invés do MySQL.
mongodb:
type: mongodb:3.6
disk: 1024
Para ler a configuração do ambiente, o Platform.sh usa o leitor de configuração que permite fácil integração. O Platform.sh também suporta estruturas e linguagens de matriz, incluindo o Java. Neste artigo, substituiremos a configuração do MongoDB pelas propriedades do Java que irão adicionar transparência a aplicação, graças à configuração do MicroProfile do Eclipse. Com esses arquivos prontos e enviados, o Platform.sh criará um conjunto de containers em um cluster.
# Este arquivo descreve uma aplicação. Pode ter várias aplicações
# no mesmo projeto.
#
# Consulte https://docs.platform.sh/user_guide/reference/platform-app-yaml.html
# O nome da aplicação. Deve ser exclusivo dentro de um projeto.
name: app
# A versão que a aplicação utiliza.
type: "java:8"
disk: 800
# Os hooks executados por vários pontos no ciclo de vida da aplicação.
hooks:
build: |
wget https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64
mv jq-linux64 jq
chmod +x jq
mvn -U -DskipTests clean package payara-micro:bundle
# Os relacionamentos da aplicação com serviços ou demais aplicações.
#
# O lado esquerdo é o nome do relacionamento, pois será exposto
# para a aplicação na variável PLATFORM_RELATIONSHIPS. O lado direito
# está no formato ‘<nome do serviço>: <nome do terminal>’.
relationships:
mongodb: 'mongodb:mongodb'
# A configuração da aplicação quando é exposta na web.
web:
commands:
start: |
export MONGO_PORT=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|./jq -r ".mongodb[0].port"`
export MONGO_HOST=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|./jq -r ".mongodb[0].host"`
export MONGO_ADDRESS="${MONGO_HOST}:${MONGO_PORT}"
export MONGO_PASSWORD=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|./jq -r ".mongodb[0].password"`
export MONGO_USER=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|./jq -r ".mongodb[0].username"`
export MONGO_DATABASE=`echo $PLATFORM_RELATIONSHIPS|base64 -d|json_pp|./jq -r ".mongodb[0].path"`
java -jar -Xmx1024m -Ddocument.settings.jakarta.nosql.host=$MONGO_ADDRESS \
-Ddocument.database=$MONGO_DATABASE -Ddocument.settings.jakarta.nosql.user=$MONGO_USER \
-Ddocument.settings.jakarta.nosql.password=$MONGO_PASSWORD \
-Ddocument.settings.mongodb.authentication.source=$MONGO_DATABASE \
target/heroes-microbundle.jar --port $PORT
A aplicação está pronta. Agora vamos movê-la para a nuvem com o Platform.sh usando as seguintes etapas:
- Crie uma conta trial gratuita.
- Inscreva-se com um usuário e senha ou faça login usando sua conta atual do GitHub, Bitbucket ou Google. Se usarmos um login de terceiros, poderemos definir uma senha para a conta Platform.sh posteriormente;
- Selecione a região do mundo em que nosso site deve residir;
- Selecione o modelo em branco.
Após esse passo a passo, o Platform.sh fornecerá toda a infraestrutura para nós e oferecerá também ao projeto um repositório remoto do Git. A infraestrutura orientada a Platform.sh Git significa que gerenciará automaticamente tudo o que a aplicação precisa para enviá-lo ao repositório remoto principal. Depois de configurar as chaves SSH, só precisamos escrever o código, incluindo alguns arquivos YAML que especificam a infraestrutura desejada, depois enviá-lo para o Git e depois dar o push.
git remote add platform <platform.sh@gitrepository>
git commit -m "Initial project"
git push -u platform master
Nesta postagem, falamos sobre os princípios e as melhores práticas em torno do cloud-native, que ainda é uma área que precisa ser aprimorada quando falamos sobre novas técnicas de desenvolvimento de software. A nuvem facilita o desenvolvimento de software, e podemos ver uma aplicação sendo executada através do Platform.sh e integrada ao Jakarta EE. Tudo isso para mostrar que é um ótimo momento para mudarmos nosso projeto para um PaaS na nuvem, assim como o Platform.sh.