Ronilson Alves

Implementando um micro serviço em Go Lang em um ambiente Spring Cloud

Eu diria que uma das características mais marcantes da arquitetura de micros serviços é a flexibilidade e a liberdade que ela proporciona no desenvolvimento de aplicações, você não precisa ficar "preso" à uma linguagem específica toda vez que você precisar solucionar um novo problema com código, você pode ter n micro serviços em uma linguagem X e o enésimo em uma linguagem Y . Essa característica para mim é fantástica, por que te permite frente a um novo problema escolher uma linguagem cuja as características atendam melhor aos requisitos para a solução, que desempenha melhor e com a qual seja mais fácil a implementação das regras de negócio.

A seguir, dentro de um ecossistema Spring Cloud simularemos a implementação de um micro serviço em Go Lang .

O problema Você desenvolveu uma API para um sistema interno de uma clínica odontológica, por intermédio dessa API os funcionários administrativos podem realizar o cadastro, leitura, atualizar algum dado ou até mesmo excluir pacientes, dentistas e consultas. A clínica já possuí um sistema para gerar faturas para os pacientes - interno também, entretanto não há integração e comunicação entre a API do serviço de agendamento com a do gerenciamento de faturas. Precisamos fazer essa integração e comunicação entre os micro serviços no ambiente Spring Cloud.

Desafios: Registrar o micro serviço Go Lang (agendamento) no Eureka. Acionar o micro serviço construído em Java (faturas) toda vez que um novo agendamento for realizado ou ocorrer alguma atualização no micro serviço de agendamento. Implementar autenticação de forma que seja possível o acesso autenticado ao micro serviço de agendamento por meio do API Gateway Vamos começar Para seguir a implementação demonstrada neste artigo, você deverá clonar meu repositório do GitHub com o código inicial do projeto (estou assumindo que você conheça bem Java + Spring e Go Lang). Você pode seguir esses passos para implementar do seu jeito com seu próprio código.

git clone -b starter https://github.com/ronilsonalves/GoLang-in-a-spring-cloud-architecture.git cd scheduling-service go mod download Registrando e desregistrando nosso micro serviço Go Lang no Spring Eureka Para implementar service discovery/service registery em nosso micro serviço Go Lang usaremos o GoKit, um conjunto de ferramentas que provém suporte a Autenticação, Log, Service Discovery, Trancing e mais outros recursos.

go get github.com/go-kit/kit Precisamos construir uma fargo instance contendo toda a informação sobre nosso micro serviço Go Lang antes de podermos registrá-la no Eureka

func buildFargoInstanceBody(appName, status string) *fargo.Instance { ipAddr, err := externalIP() if err != nil { fmt.Println(err) } port, err := strconv.Atoi(os.Getenv("PORT")) if err != nil { fmt.Println(err) port = 9000 } return &fargo.Instance{ InstanceId: ipAddr + ":" + appName + ":" + os.Getenv("PORT"), HostName: "localhost", App: strings.ToUpper(appName), IPAddr: ipAddr, VipAddress: appName, SecureVipAddress: appName, Status: fargo.StatusType(status), Overriddenstatus: "UNKNOWN", Port: port, PortEnabled: true, SecurePort: 8443, SecurePortEnabled: false, HomePageUrl: "http://localhost:" + os.Getenv("PORT") + os.Getenv("BASE_PATH"), StatusPageUrl: "http://localhost:" + os.Getenv("PORT") + "/status", HealthCheckUrl: "http://localhost:" + os.Getenv("PORT") + "/health", CountryId: 0, DataCenterInfo: fargo.DataCenterInfo{ Name: "MyOwn", Class: "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", }, LeaseInfo: fargo.LeaseInfo{}, Metadata: fargo.InstanceMetadata{}, UniqueID: nil, } }

// BuildFargoInstance build a Fargo Instance and return eureka.Registrar func BuildFargoInstance() eureka.Registrar { eurekaAddr := os.Getenv("EUREKA_SERVER_URL") if eurekaAddr == "" { fmt.Println("EUREKA_SERVER_URL is not set") }

logger := log.NewLogfmtLogger(os.Stderr)
logger = log.With(logger, "ts", log.DefaultTimestamp)

var fargoConfig fargo.Config
fargoConfig.Eureka.ServiceUrls = []string{eurekaAddr}
fargoConfig.Eureka.PollIntervalSeconds = 1

fargoConnection := fargo.NewConnFromConfig(fargoConfig)
fInstance := buildFargoInstanceBody("scheduling-service", "UP")
return *eureka.NewRegistrar(&fargoConnection, fInstance, log.With(logger, "component", "registrar"))

} Com eureka.Registrar retornado podemos usá-lo para registrar e desregistrar nosso micro serviço. Para isso, em nossa main precisaremos apenas atribuir o retorno de BuildFargoInstance() a uma variável e chamar o método Register().

eurekaRegister := sd.BuildFargoInstance() eurekaRegister.Register() Nesse ponto já registramos com sucesso nosso micro serviço Go Lang no Eureka Server, o módulo disponibilizado pelo GoKit envia heartbeats para a instância do nosso Eureka Server enquanto nosso micro serviço estiver rodando, mas para atualizarmos e desfazermos o registro do nosso micro serviço em caso de nossa instância ser terminada ou o processo ser encerrado precisando chamar o método Deregister(), para fazer isso vamos construir um channel para monitorar o status da aplicação em nossa função main.

c := make(chan os.Signal) signal.Notify(c, os.Interrupt) // contains filtered fields or functions in main function go func() { select { case signal := <-c: _ = signal time.Sleep(4 * time.Second) eurekaRegister.Deregister() os.Exit(1) } }() Integramos com sucesso nosso micro serviço ao Spring Eureka. O primeiro dos nossos desafios foi concluído.

Acionando nosso serviço de faturas através de uma fila do RabbitMQ. Usaremos uma fila RabbitMQ para acionarmos nosso micro serviço de gerenciamento de faturas (em Java) toda vez que um evento de agendamento for gerado em nosso micro serviço de agendamento (Go Lang).

Enviaremos uma mensagem para a fila e no micro serviço de faturas implementaremos um listener para observar e consumir as mensagens dessa fila.

Primeiro, vamos implementar um publisher em nosso micro serviço Go Lang, para isso vamos usar um cliente RabbitMQ para Go.

go get github.com/hadihammurabi/go-rabbitmq Então, precisamos conectar nosso micro serviço à instância do RabbitMQ e criar um canal onde publicaremos nossas mensagens:

// ConnectRabbitMQ connect and setup RabbitMQ channel and queue func ConnectRabbitMQ(urlConn, name string) (gorabbitmq.MQ, error) { mq, err := gorabbitmq.New(urlConn) failOnError(err, "Failed to create a MQ")

err = mq.Exchange().
	WithName(name).
	WithType(exchange.TypeDirect).
	Declare()
failOnError(err, "Failed to create a channel")

q, err := mq.Queue().
	WithName(name).
	Declare()
failOnError(err, "Failed to create a queue")

err = q.Binding().
	WithExchange(name).
	Bind()
failOnError(err, "Failed to bind queue")

return *mq, nil

} Em nosso serviço de agendamentos publicaremos uma mensagem em nossa fila toda vez que um novo evento de agendamento for criado.

// PublishMessage - send a msg to RabbitMQ queue when an appointment is made or updated func PublishMessage(a domain.AppointmentDTO) { mq, err := ConnectRabbitMQ(os.Getenv("RABBIT_MQ_URL_CONN"), "appointment-service") log.Println(err) body, _ := json.Marshal(a) err = mq.Publish(&gorabbitmq.MQConfigPublish{ RoutingKey: mq.Queue().Name, Message: amqpi.Publishing{ ContentType: "application/json", Body: body, }, }) defer mq.Close() } Agora o que precisamos fazer é chamar esse método em nosso service a cada novo agendamento salvo ou atualizado:

func (s *service) Create(a domain.Appointment)(domain.AppointmentDTO, error) { // contains filtered fields or functions if ok { amqp.PublishMessage(apSaved) return apSaved, nil } }

func (s *service) Update(id int, a domain.Appointment)(domain.AppointmentDTO, error) { // contains filtered fields or functions amqp.PublishMessage(response) return response, nil } A implementação do publisher no micro serviço Go Lang foi feita com sucesso, agora precisamos implementar no micro serviço de faturas (Java) um listener para podemos consumir essa fila e suas respectivas mensagens.

Como regra de negócio, precisamos gerar uma fatura para cada agendamento que for realizado e atualizar essa fatura caso sejam feitas atualizações nesse mesmo agendamento.

Primeiro, vamos configurar a conexão com o RabbitMQ e criar um canal para consumir nossas mensagens no serviço Java.

@Configuration public class RabbitMQSenderConfig { @Value("${queue.appointment-service.name}") private String appointmentServiceQueue;

@Bean
public Queue appointmentQueue() {
    return new Queue(this.appointmentServiceQueue,false);
}

} Também precisaremos configurar um conversos de mensagens em um objeto Java. Como nesse projeto estamos utilizando o pattern de Datetime brasileiro ("dd/MM/yyy HH:mm"), precisamos configurar um object mapper customizado para evitarmos exceptions enquanto tentamos converter campos do tipo Datetime. Para isso vamos criar uma classe:

@RequiredArgsConstructor public class CustomLocalDateTimeObjectMapper { private final ObjectMapper customObjectMapper;

public ObjectMapper getCustomLocalDateTimeObjectMapper() {
    JavaTimeModule module = new JavaTimeModule();
    this.customObjectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    this.customObjectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    LocalDateTimeDeserializer localDateTimeDeserializer =
            new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"));
    module.addDeserializer(LocalDateTime.class, localDateTimeDeserializer);
    this.customObjectMapper.registerModule(module);
    return this.customObjectMapper;
}

} Agora precisamos configurar o nosso template do RabbitMQ para usar nosso object mapper customizado:

@Configuration public class RabbitTemplateConfig {

@Bean
public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
    CustomLocalDateTimeObjectMapper customLocalDateTimeObjectMapper =
            new CustomLocalDateTimeObjectMapper(new ObjectMapper());
    return new Jackson2JsonMessageConverter(customLocalDateTimeObjectMapper.getCustomLocalDateTimeObjectMapper());
}

@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
    RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
    rabbitTemplate.setMessageConverter(producerJackson2MessageConverter());
    return rabbitTemplate;
}

} Criemos então nosso listener para observar e consumir nossas mensagens:

@RequiredArgsConstructor @Component public class AppointmentListener { private final Logger logger = Logger.getLogger(AppointmentListener.class.getName()); private final InvoiceServiceImpl invoiceService;

@RabbitListener(queues = {"${queue.appointment-service.name}"})
public void receiveMessage(Appointment appointment) {
    logger.log(Level.INFO,"Message received from RabbitMQ: "+appointment);
    List<Invoice> invoices =
            invoiceService.listAll().stream().filter(invoice -> Objects.equals(invoice.getAppointmentId(), appointment.id())).toList();
    if (invoices.isEmpty()) {
        invoiceService.save(appointment);
    } else {
        invoiceService.update(invoices.get(0),appointment);
    }
}

} Em nosso listener nós verificamos se já há alguma fatura para aquele agendamento e caso não haja criamos uma nova fatura, no caso de haver atualizamos.

Nesse ponto já temos nosso serviço de faturas escutando nosso serviço de agendamento (por intermédio da fila RabbitMQ) e criando e atualizando faturas quando necessário.

Protegendo nosso serviço de agendamento (Go Lang) usando o Keycloak server como Identity Provider Este é o terceiro item da nossa lista, precisamos proteger o nosso micro serviço Go Lang, aplicaremos as configurações de um resource server tendo o Keycloak server como provedor de identidade. Como estamos utilizando o Spring Gateway juntamente com o Spring Security, podemos configurar um token relay e ao invés de utilizar um token chave-valor, podemos utilizar o JWT gerado pelo Keycloak e validar suas claims e suas roles em nosso serviço de agendamento.

A configuração do reino do Keycloak está no repositório do projeto no GitHub.

Vamos ao que interessa. Em nosso Gateway vamos configurar o token relay para os micro serviços. Podemos fazer isso no application.yml do Gateway.

contains filtered fields

cloud: gateway: routes: - id: invoice-service uri: lb://invoice-service predicates: - Path=/invoices/** - id: scheduling-service uri: lb://scheduling-service predicates: - Path=/api/v1/** default-filters: - TokenRelay - LogFilter

contains filtered fields

No SecurityConfig.java do Gateway vamos especificar que qualquer requisição deve ser autenticada, se não, o usuário será direcionado para o Login do provedor de autenticação OAuth (nesse caso a página de login de usuários do Keycloak.

@Configuration public class SecurityConfig {

@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity httpSecurity) {
    httpSecurity
            .authorizeExchange()
            .pathMatchers("/actuator/**").permitAll()
            .anyExchange().authenticated()
            .and()
            .oauth2Login()
            .and().csrf().disable();
    return httpSecurity.build();
}

} No serviço Go Lang precisamos alterar a autenticação para usar o JWT gerado pelo Keycloak e enviado na request pelo Gateway. Para isso, criaremos um middleware para validar o token recebido e suas claims.

type Claims struct { RealmAccess roles json:"realm_access,omitempty" JTI string json:"jti,omitempty" }

type roles struct { Roles []string json:"roles,omitempty" }

var RealmConfigURL = os.Getenv("REALM_CONFIG_URL") var clientID = os.Getenv("CLIENT_ID") var authorizedRole = "ADMIN"

func IsAuthorizedJWT() gin.HandlerFunc { return func(c *gin.Context) { rawAccessToken := strings.Replace(c.GetHeader("Authorization"), "Bearer", "", 1)

	trans := &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	}
	client := &http.Client{
		Timeout:   time.Duration(6000) * time.Second,
		Transport: trans,
	}

	ctx := oidc.ClientContext(context.Background(), client)
	provider, err := oidc.NewProvider(ctx, RealmConfigURL)
	if err != nil {
		authorizationFailed("an authorization error occurred while getting the provider: "+err.Error(), c)
		return
	}

	oidcConfig := &oidc.Config{
		ClientID: clientID,
	}

	verifier := provider.Verifier(oidcConfig)
	idToken, err := verifier.Verify(ctx, rawAccessToken)
	if err != nil {
		authorizationFailed("an authorization error occurred while verifying the token: "+err.Error(), c)
		return
	}

	var IDTokenClaims Claims
	if err := idToken.Claims(&IDTokenClaims); err != nil {
		authorizationFailed("An error occurred while extracting claims: "+err.Error(), c)
		return
	}

	userAccessRoles := IDTokenClaims.RealmAccess.Roles
	for _, userRole := range userAccessRoles {
		if userRole == authorizedRole {
			c.Next()
			return
		}
	}

	authorizationFailed("The user has no permission to access this API", c)
}

}

func authorizationFailed(message string, c *gin.Context) { web.BadResponse(c, http.StatusUnauthorized, "ERROR", message) return } Então em nosso main.go tudo o que precisaremos fazer é atualizar o middleware de autenticação para usar o que acabamos de criar em nossas rotas.

func main() { // contains filtered fields or functions r.Use(IsAuthorizedJWT()) // contains filtered fields or functions } Finalmente o acesso ao micro serviço Go Lang se dará de forma segura por intermédio do Gateway. O terceiro item de nossa lista está quase pronto.

Se um usuário tentar criar uma fatura manualmente, ele precisa fornecer o preço e o ID do agendamento no corpo da requisição, com o ID informado nosso serviço de faturas irá requisitar ao serviço de agendamento (agora permitindo apenas usuários autenticados e com a role de ADMIN) os dados do agendamento e validar se ele realmente existe e por fim com os dados retornados gerar uma fatura, para realizar essa requisição usaremos o Feign client e implementaremos nele um RequestInterceptor para "pegarmos" a autenticação do usuário e enviá-la ao micro serviço de agendamento.

@Configuration @RequiredArgsConstructor public class FeignConfiguration {

private static final String KEYCLOAK_REGISTRATION_ID = "keycloak-registration";

private final OAuth2AuthorizedClientService clientService;
private final ClientRegistrationRepository registrationRepository;

@Bean
public RequestInterceptor requestInterceptor() {
    ClientRegistration clientRegistration = registrationRepository.findByRegistrationId(KEYCLOAK_REGISTRATION_ID);
    OAAuth2ClientCredentialsFeignManager credentialsFeignMananger =
            new OAAuth2ClientCredentialsFeignManager(authorizedClientManager(),clientRegistration);
    return requestInterceptor -> {
        requestInterceptor.header("Authorization", "Bearer " + credentialsFeignMananger.getAccessToken());
    };
}

@Bean
public OAuth2AuthorizedClientManager authorizedClientManager () {
    OAuth2AuthorizedClientProvider authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
            .builder()
            .clientCredentials()
            .build();

    AuthorizedClientServiceOAuth2AuthorizedClientManager authorizedClientManager =
            new AuthorizedClientServiceOAuth2AuthorizedClientManager(registrationRepository, clientService);

    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    return authorizedClientManager;
}

} Pronto, nosso serviço Go Lang está implementado em nosso ambiente Spring Cloud com sucesso. Lembrando que no repositório do projeto você encontra um export do realm para testar a autenticação.

Eu espero que este tutorial tenha sido util para você e caso tenha perguntas ou sugestões não hesite em me contatar. Obrigado por ler.