<![CDATA[ Lucas Santos ]]> https://blog.lsantos.dev https://blog.lsantos.dev/favicon.png Lucas Santos https://blog.lsantos.dev Ter, 26 Out 2021 09:49:20 -0300 60 <![CDATA[ 10 coisas que você não sabia que o NPM era capaz de fazer ]]> https://blog.lsantos.dev/npm-10-comandos/ 61702af5dfaa6b5e446d559c Qui, 21 Out 2021 10:55:00 -0300

Bora pra mais um conteúdo de vídeo lá no meu canal, nesse vídeo eu comento um pouco sobre o NPM e como ele pode ser muito mais do que um gerenciador de pacotes.

No vídeo eu listo alguns comandos e também listo alguns repositórios, esses são:

  • NPM Expansions é um repositório e um pacote open source pra você dizer qual é o significado de NPM!

E aqui vão os links dos comandos que eu mostrei no vídeo!

]]>
<![CDATA[ 5 coisas pra você saber antes de palestrar: Call For Papers ]]> https://blog.lsantos.dev/5-dicas-de-cfp/ 61672814ea858f6bbc0e9a2b Qui, 14 Out 2021 10:40:00 -0300 Bora discutir um pouco sobre eventos de tecnologia e como eles podem ajudar na sua carreira?

Nesse vídeo vamos iniciar uma série onde vamos falar só sobre eventos de tecnologia de vários aspectos! Começando com um dos mais importantes: O Call For Papers!

]]>
<![CDATA[ Temporal: a nova forma de se trabalhar com datas em JavaScript ]]> https://blog.lsantos.dev/temporal-api/ 614cd0a94ce56c73e07cdc02 Qui, 23 Set 2021 21:49:00 -0300 Não é novidade que a API de datas do JavaScript precisa de uma alteração urgente. Desde muito tempo, muitos devs reclamam que ela não é muito intuitiva e também não é muito confiável, além disso, a API de datas tem algumas convenções que, digamos, são pouco ortodoxas como, por exemplo, começar os meses do 0 ao invés de 1.

Vamos entender todos os problemas do Date e vamos entender também como a nova API Temporal promete resolvê-los. Além disso vamos entender o porquê de termos uma nova API para isso ao invés de modificar o que já temos funcionando.

Os problemas do Date

Como Maggie Pint aponta em seu blog, hoje já é de senso comum que Brendan Eich teve 10 dias para escrever o que seria conhecido como JavaScript e inclui-lo no hoje falecido Netscape browser.

Manipulação de datas é uma parte muito importante de qualquer linguagem de programação, nenhuma pode ser lançada (nem mesmo ser considerada completa) sem ter algo para tratar o que temos de mais comum no dia-a-dia, o tempo. Só que implementar todo o domínio de manipulação de datas não é algo trivial – se hoje não é trivial para a gente, que só usa, imagina para quem implementa – então Eich se baseou na instrução "Deve se parecer com Java", que foi dada a ele para construir a linguagem, e copiou a API java.Util.Date, que já era ruim, e foi praticamente toda reescrita no Java 1.1, isso 24 anos atrás.

Baseado nisso, Maggie, Matt e Brian, os principais commiters do nosso querido Moment.js, compilaram uma lista de coisas que o Date do JavaScript deixava a desejar:

  1. O Date não suporta timezones além do UTC e o horário local do usuário: Não temos como, nativamente, exibir a data de forma prática em múltiplos fusos, o que podemos fazer é calcular manualmente um offset para adicionar ao UTC e assim modificar a data.
  2. O parser de data é bastante confuso por si só
  3. O objeto Date é mutável, então alguns métodos modificam a referência do objeto original, fazendo uma implementação global falhar
  4. A implementação do DST (Daylight Saving Time, o horário de verão) é algo que até hoje é meio esotérico na maioria das linguagens, no JS não é diferente
  5. Tudo que você precisa fazer para fazer contas com datas vai ter fazer chorar por dentro eventualmente. Isto porque a API não possui métodos simples para adicionar dias, ou então para calcular intervalos, você precisa transformar tudo para um timestamp unix e fazer as contas na mão
  6. Esquecemos que o mundo é um lugar grande, e não temos só um tipo de calendário. O calendário Gregoriano é o mais comum para o ocidente, no entanto, temos outros calendários que devemos também suportar.

Um pouco mais abaixo neste mesmo post, ela comenta sobre como algumas dessas coisas são "consertáveis" com a adição de métodos ou parâmetros extras. Porém existe um outro fator que temos que levar em consideração quando estamos tratando com JavaScript que provavelmente não temos que pensar em outros casos.

A compatibilidade.

Web Compatibility

A web é um lugar grande e, por consequencia, o JavaScript se tornou absurdamente grande. Existe uma frase muito famosa que diz:

Se pode ser feito com JavaScript, vai ser feito com JavaScript

E isso é muito real, porque tudo que era possível e impossível já foi feito pelo menos uma vez em JavaScript. E isso torna as coisas muito mais difíceis, porque um dos principais princípios da Web e um dos quais o TC39 segue a risca é o "Don't break the web".

Hoje, em 2021, temos códigos JavaScript de aplicações legadas desde os anos 90 sendo servidas pela web afora, e embora isso possa ser algo louvável, é extremamente preocupante, porque qualquer alteração deve ser pensada com muito cuidado, e APIs antigas, como o Date, não podem ser simplesmente depreciadas.

E o maior problema da Web hoje, e consequentemente do JavaScript, é a imutabilidade. Se formos pensar no modelo DDD, nossos objetos podem ser definidos como entidades cujos estados mudam ao longo do tempo, mas também temos os value types, que são apenas definidos pelas suas propriedades e não por seus estados e IDs. Vendo por esse lado, o Date é claramente um value type, porque apesar de termos um mesmo objeto Date, a data 10/04/2021 é claramente diferente de 10/05/2021. E isso é um problema.

Hoje, o JavaScript trata objetos como o Date em forma de referência. Então se fizermos algo assim:

const d = new Date()
d.toISOString() // 2021-09-23T21:31:45.820Z
d.setMonth(11)
d.toISOString() // 2021-12-23T21:31:45.820Z

E isso pode nos dar muitos problemas porque se tivermos helpers como os que sempre fazemos: addDate, subtractDate e etc, vamos normalmente levar um parâmetro Date e o número de dias, meses ou anos para adicionar ou subtrair, se não clonarmos o objeto em um novo objeto, vamos mutar o objeto original e não seu valor.

Outro problema que também é citado neste outro artigo da Maggie é o que chamamos de Web Reality issue, ou seja, um problema que teve sua solução não por conta do que fazia mais sentido, mas sim porque a Web já funcionava daquela determinada forma, e a alteração ia quebrar a Web...

Este é o problema do parsing de uma data no formato ISO8601, vou simplificar a ideia aqui (você pode ler o extrato completo no blog), mas a ideia é que o formato padrão de datas do JS é o ISO8601, ou o nosso famoso YYYY-MM-DDTHH:mm:ss.sssZ, ele tem formatos que são date-only, então só compreendem a parte de datas, como YYYY, YYYY-MM e YYYY-MM-DD. E a sua contrapartida time-only que só compreendem as variações que contém algo relacionado a tempo.

Porém, existe uma citação que mudou tudo:

Quando o offset de fuso horário estiver ausente, os formatos date-only são interpretados como UTC, enquanto os formatos completos (date-time) são interpretados como o horário local.

Isso significa que new Date('2021-04-10') vai me dar uma data no fuso UTC que seria algo como 2021-04-10T00:00:00.000Z, porém new Date('2021-04-10T10:30') vai me dar uma string ISO8601 na minha hora local. Este problema foi parcialmente resolvido desde 2017, mas ainda sim existem várias discussões sobre o funcionamento do parser.

Temporal

A proposta do temporal é uma das mais antigas propostas em aberto do TC39, e também uma das mais importantes. No momento da publicação deste artigo, ela está no estágio 3, o que significa que a maioria dos testes já passou e os browsers estão quase prontos para implementá-la.

A ideia da API é ter um objeto global como um namespace, da mesma forma que o Math funciona hoje. Além disso, todos os objetos Temporal são completamente imutáveis e todos os valores podem ser representados em valores locais mas podem ser convertidos no calendário gregoriano.

Outras premissas são de que não são contados os segundos bissextos e todos os horários são mostrados em um relógio tradicional de 24h.

Você pode testar o Temporal diretamente na documentação usando o polyfill que já vem incluído no console, basta apertar F12 e entrar na aba console, digite Temporal e você deve ver o resultado dos objetos.

Todos os métodos do Temporal vão começar com Temporal., se você verificar no seu console, vai ver que temos cinco tipos de entidades com o temporal:

  • Instant: Um Instant é um ponto fixo no tempo, sem levar em conta um calendário ou um local. Portanto não tem conhecimento de valores de tempo, como dias, horas e meses.
  • Calendar: Representa um sistema de calendário.
  • PlainDate: Representa uma data que não está associada a um fuso horário específico. Também temos a variação do PlainTime e as variações locais de PlainMonthYear, PlainMonthDay e etc.
  • PlainDateTime: Mesma coisa da PlainDate, mas com horas.
  • Duration: Representa uma extensão de tempo, por exemplo, cinco minutos, geralmente utilizado para fazer operações aritméticas ou conversões entre datas e medir diferenças entre os próprio objetos Temporal.
  • Now: É um modificador de todos os tipos que temos antes. Fixando o tempo de referência como sendo o agora.
  • TimeZone: Representa um objeto de fuso horário. Os timezones são muito utilizados para poder converter entre objetos Instante objetos PlainDateTime.

A relação entre esses objetos é descrita como sendo hierárquica, então temos o seguinte:

Veja que o TimeZone implementa todos os tipos de objetos abaixo dele, portanto é possível obter qualquer objeto a partir deste, por exemplo, a partir de um TimeZone específico, podemos obter todos os objetos dele em uma data específica:

const tz = Temporal.TimeZone.from('America/Sao_Paulo')
tz.getInstantFor('2001-01-01T00:00') // 2001-01-01T02:00:00Z
tz.getPlainDateTimeFor('2001-01-01T00:00Z') // 2000-12-31T22:00:00

Vamos passar pelos principais métodos e atividades que podemos fazer com o Temporal.

Buscando a data e hora atual

const now = Temporal.Now.plainDateTimeISO()
now.toString() // Retorna no formato ISO, equivalente a Date.now.toISOString()

Se você só quiser a data, use plainDateISO().

Unix Timestamps

const ts = Temporal.Now.instant()
ts.epochMilliseconds // unix em ms
ts.epochSeconds // unix em segundos

Interoperabilidade com Date

const atual = new Date('2003-04-05T12:34:23Z')
atual.toTemporalInstant() // 2003-04-05T12:34:23Z

Interoperabilidade com inputs

Podemos setar inputs do tipo date usando o próprio Temporal, como estes valores aceitam datas no formato ISO, qualquer data setada neles como value pode ser obtida pelo Temporal:

const datePicker = document.getElementById('input')
const today = Temporal.Now.plainDateISO()
datePicker.value = today

Convertendo entre tipos

const date = Temporal.PlainDate.from('2021-04-10')
const timeOnDate = date.toPlainDateTime(Temporal.PlainTime.from({ hour: 23 }))

Veja que convertemos um objeto sem hora, para um objeto PlainDateTime, enviando um outro objeto PlainTime como horas.

Ordenando DateTime

Todos os objetos Temporal possuem um método compare() que pode ser usado em um Array.prototype.sort() como função de comparação. Dito isso, podemos imaginar uma lista de PlainDateTimes:

let a = Temporal.PlainDateTime.from({
  year: 2020,
  day: 20,
  month: 2,
  hour: 8,
  minute: 45
})
let b = Temporal.PlainDateTime.from({
  year: 2020,
  day: 21,
  month: 2,
  hour: 13,
  minute: 10
})
let c = Temporal.PlainDateTime.from({
  year: 2020,
  day: 20,
  month: 2,
  hour: 15,
  minute: 30
})

Depois, podemos criar uma função de comparação para mandar nosso array:

function sortedLocalDates (dateTimes) {
  return Array.from(dateTimes).sort(Temporal.PlainDateTime.compare)
}

E então:

const results = sortedLocalDates([a,b,c])
// ['2020-02-20T08:45:00', '2020-02-20T15:30:00', '2020-02-21T13:10:00']

Arredondando tipos

Os tipos de hora do Temporal possuem um método chamado round, que arredonda os objetos para o próximo valor cheio de acordo com o tipo de tempo que você procura. Por exemplo, arredondar para a próxima hora cheia:

const time = Temporal.PlainTime.from('11:12:23.123432123')
time.round({smallestUnit: 'hour', roundingMode: 'ceil'}) // 12:00:00

Conclusão

O Temporal é a ponta de um iceberg gigantesco que chamamos de "manipulação temporal", existem vários conceitos chave como ambiguidade que devem ser levados em consideração quando estamos trabalhando com horas e datas.

A API Temporal é a primeira chance de mudarmos a forma como o JavaScript encara as datas e como podemos melhorar nossa forma de trabalhar com elas, este foi um recorte do que é possível fazer e de como isto vai ser feito no futuro, leia a documentação completa para saber mais.

]]>
<![CDATA[ O que há de novo no TypeScript 4.4 ]]> https://blog.lsantos.dev/typescript-44/ 6140e99dae8d560f62fd5524 Qua, 15 Set 2021 08:00:00 -0300 No dia 26 de Agosto de 2021, tivemos o anúncio da versão 4.4 do TypeScript e, como é de costume, vou fazer um highlight de tudo que aconteceu de novo e todas as novidades mais legais do nosso superset favorito!

Análise de fluxo agora com variáveis

Quando usamos TypeScript, uma das grandes falácias que muita gente descreve como sendo um problema que impede o uso, é ter que ficar declarando tipos para todos os dados que você tem. Isso não é verdade.

O compilador do TS é poderoso o suficiente para entender o fluxo de controle e o fluxo do seu código, de forma que ele sabe quando uma variável ou algum outro dado é de um tipo específico de acordo com uma checagem feita anteriormente. Essa checagem é comumente chamada de type guard. E é quando fazemos algo assim:

function foo (bar: unknown) {
  if (typeof bar === 'string') {
    // O TS agora sabe que o tipo é String
    console.log(bar.toUpperCase())
  }
}

Isso é válido não só para casos de unknown mas também para casos onde o tipo é genérico como any.

O grande problema é que, se movermos essa verificação para uma constante ou uma função, o TS se perde no fluxo e não consegue mais entender o que está acontecendo, por exemplo:

function foo (bar: unknown) {
	const isString = typeof bar === 'string'
    if (isString) console.log(arg.toUpperCase())
    //                            ~~~~~~~~~~~
    // Error! Property 'toUpperCase' does not exist on type 'unknown'.
}

Agora, o TS consegue identificar a constante e o seu retorno, conseguindo prover o resultado sem erros. O mesmo é possível também em tipos complexos, ou tipos discriminantes (discriminant types):

type Animal = 
    | { kind: 'cat', meow: () => void }
    | { kind: 'dog', woof: () => void }

function speak (animal: Animal) {
  const { kind } = animal
  
  if (kind === 'cat') { animal.meow() }
  else { animal.woof() }
}

Dentro dos tipos extraídos pelo destructuring, agora temos a asserção correta da string. Outra coisa legal é que ele também vai entender de forma transitiva como todos os tipos funcionam, ou seja, ele vai tipo a tipo para poder inferir qual é o tipo atual do objeto a partir das análises que você já fez:

function f(x: string | number | boolean) {
    const isString = typeof x === "string"
    const isNumber = typeof x === "number"
    const isStringOrNumber = isString || isNumber
    if (isStringOrNumber) {
        x  // Type of 'x' is 'string | number'.
    }
    else {
        x  // Type of 'x' is 'boolean'.
    }
}

Index signatures com Symbols e templates

Existe um tipo chamado de index signature, essencialmente este tipo nos diz que o objeto em questão pode ter chaves de nome arbitrário, como se fosse um dicionário eles são representados como [key: string]: any.

Os únicos tipos possíveis para uma index signature são string e number atualmente, porque são os tipos mais comuns.

Porém, existe um outro tipo chamado Symbol, que é muito utilizado, principalmente por quem constrói libs, para poder indexar os tipos de seus arrays e objetos sem ter de exibí-los ou modificá-los. Com a chegada do 4.4 você agora pode fazer isso:

interface Colors {
    [sym: symbol]: number;
}

const red = Symbol("red");
const green = Symbol("green");
const blue = Symbol("blue");

let colors: Colors = {};

colors[red] = 255;    
let redVal = colors[red];  

Era impossível também ter um subset de string ou de number como os template string types como sendo chaves. Por exemplo, um objeto cujas chaves começam sempre com data-, agora isso é totalmente válido:

interface DataOptions {
  [key: `data-${string}`]: unknown
}

let b: DataOptions = {
    "data-foo": true
    "qualquer-coisa": true,  // Error! 'unknown-property' wasn't declared in 'DataOptions'.
};

Catch agora tem padrão de unknown

Como muitas pessoas sabem (e reclamaram!), quando usamos um try/catch dentro de qualquer função no TypeScript, o bloco catch vai sempre levar um parâmetro error que, por definição, teria um tipo any.

Depois de algumas discussões com a comunidade sobre qual seria o tipo correto, muitas pessoas optaram por ter o tipo unknown como padrão para os erros. Isto porque deixar um tipo aberto como any, essencialmente não dá tipagem nenhuma. Então o TS 4.4 introduz uma nova opção no tsconfig e uma nova flag chamada useUnknownInCatchVariables, que está desativado por padrão para não quebrar a compatibilidade, mas pode e deve ser ativada.

try {
    codigo();
}
catch (err) { // err: unknown

    // Error! Property 'message' does not exist on type 'unknown'.
    console.error(err.message);

    // Define o tipo de erro
    if (err instanceof Error) {
        console.error(err.message);
    }
}

Se você ativar a flag strict, esta flag também será ativada.

Propriedades opcionais exatas

Outro problema trazido pela comunidade era o conflito entre propriedades opcionais declaradas como prop?: <tipo>, pois este tipo de propriedade vai ser expandida para prop: <tipo> | undefined, mas e se a propriedade puder ter mesmo um valor undefined?

Então se alguém quisesse escrever uma propriedade opcional do tipo number, como undefined, isso era ok por padrão, mas causava vários problemas:

interface Pessoa {
  nome: string
  idade?: number
}
  
const Lucas: Pessoa = { nome: 'Lucas', idade: undefined } // ok

E esta prática ocorre em vários erros porque vamos estar tratando um valor válido com um inexistente. Ainda mais se tivéssemos que lidar com a propriedade idade em algum momento, além disso, cada tipo de método como o Object.assign, Object.keys, for-in, for-of, JSON.stringify e etc, tem tratamentos diferentes para quando uma propriedade existe ou não.

Na versão 4.4 o TS adiciona uma nova flag chamada exactOptionalPropertyTypes, que faz com que este erro desapareça, uma vez que você não poderá usar undefined em uma propriedade tipada como opcional.

interface Pessoa {
  nome: string
  idade?: number
}
  
const Lucas: Pessoa = { nome: 'Lucas', idade: undefined } // Erro

Assim como a anterior, a propriedade é parte do conjunto strict.

Suporte a blocos estáticos

O ECMA2022 prevê uma nova funcionalidade chamada de static initialization blocks, essa funcionalidade vai permitir que criemos códigos mais complexos de inicialização para membros estáticos de uma classe, vamos falar mais sobre isso aqui no blog em breve!

Mas por enquanto, o TS 4.4 já tem suporte a essa funcionalidade.

Conclusão

Estas foram as alterações mais importantes no TS 4.4, mas não as únicas, tivemos uma série de melhorias de performance e também de leitura e integração com o VSCode.

]]>
<![CDATA[ Removendo itens duplicados no JavaScript ES6 ]]> https://blog.lsantos.dev/removendo-itens-duplicados-no-javascript-es6/ 6132a58989441903487af3be Qua, 08 Set 2021 09:00:00 -0300

Acho que todo mundo assim como eu, em algum momento já teve que remover itens duplicados de uma lista Array, mas será que a forma que aprendemos é de fato a melhor?

Nesse artigo vou mostrar o meu ponto de vista, a forma que encontrei para remover itens duplicados de uma lista com mais de 1.000.000 de itens no meu dia a dia na @squidit, seja o array de tipos primitivos ou não

O jeito comum

Acredito que a forma mais comum que conhecemos é aquela que percorremos um Array e verificamos a cada interação se aquele item está ou não no novo array.

//  loop-itens.js
/**
 * @desc Gera um array de tamanho N com números aleatórios, respeitando N
 * @param {number} length 
 */
function generateRandomArray(length) {
  return Array.from(Array(length), () => parseInt(Math.random() * length));
}

const randomList = generateRandomArray(1000) // Um array com 1000 números aleatórios
const uniqueList = [] // Lista de array único

for(const value of randomList) {
  //  Caso o valor não esteja no uniqueList, adicionamos
  if (!uniqueList.includes(value)) uniqueList.push(value)
}
console.log(`uniqueList has ${uniqueList.length} itens`)

Que gera a seguinte saída:

Print-Quantidade-Itens-Duplicados

Isso pode até funcionar para uma lista pequena de alguns milhares de itens.

Se utilizarmos o console.time e o console.timeEnd para verificar quanto tempo essa operação demora, veremos que é super rápido.

//  Resto do código

console.time('Remove duplicated items') // Adicionamos 
for(const value of randomList) {
  //  Verificação do código anterior...
}
console.timeEnd('Remove duplicated items')

Gera a seguinte saída:

Print-Tempo-Para-Remover-Itens-Duplicados

O que aconteceria se por acaso aumentássemos esse Dataset? para uma lista com 100.000 de itens por exemplo?

//  Resto do código ... 

// equivale a 10^5, que é o mesmo que 100.000
const randomList = generateRandomArray(10 ** 5) 
const uniqueList = [] // Lista que conterá arrays únicos

console.time('Remove duplicated items')
for(const value of randomList) {
  //  Caso o valor não esteja no uniqueList, adicionamos
  if (!uniqueList.includes(value)) uniqueList.push(value)
}
console.timeEnd('Remove duplicated items')

Gera a seguinte saída:

Print-tempo-para-remover-itens-duplicados-com-100k

E se aumentarmos para 200.000 por exemplo, o tempo já aumenta drasticamente

Print-tempo-para-remover-itens-duplicados-com-200k

O problema

Usando o for ou .reduce a premissa ainda seria a mesma, que seria:

  • Iterar pelo array.
  • Verificar se o valor existe no novo array.
  • Adicionar no array.

Para cada iteração é necessário fazer uma segunda iteração no uniqueArray para verificar se existe o valor lá dentro, isso na programação é chamado de O(n)², onde n dita o número de operações que serão executadas na sua aplicação. Portanto o número de operações para esse algoritmo cresce de forma exponencial conforme o número de itens.

Vamos exemplificar com o código a seguir:

// Resto do código

// Itera 10 vezes de 10k em 10k até chegar em 100k
for (let length = 1; length <= 100000; length += 10000) {
  // Para cada interação, gera um novo array.
  const randomList = generateRandomArray(length)
  const uniqueList = [] // Lista que contera arrays único

  console.log(`List size of ${randomList.length}`)
  console.time(`Remove ${randomList.length} duplicated items`)
  for (const value of randomList) {
    // Caso o valor não esteja no uniqueList, adicionamos
    if (!uniqueList.includes(value)) uniqueList.push(value)
  }
  console.timeEnd(`Remove ${randomList.length} duplicated items`)
  console.log('---------')
}

É possível ver o tempo crescente de forma exponencial quando printamos quanto tempo leva para que a operação seja finalizada de acordo com o número de itens

Print-duracao-tempo-para-remover-itens-duplicadas-de-forma-exponencial

Usando o Set

No Javascript temos um objeto chamado Set, ele garante que os valores sejam guardados uma única vez, ou seja, sempre que tentarmos adicionar um valor que está na estrutura, esse valor não sera adicionado.

const set = new Set();

set.add(1) // [1]
set.add(2) // [1,2]
set.add(3) // [1,2,3]
set.add(2) // [1,2,3]

console.log(set) // Set(3) { 1, 2, 3 }

O set aceita objetos também, porém não vai remover a duplicidade deles porque os objetos, como sabemos, são passados via referência no JavaScript:

const set = new Set();

set.add({ a: 1, b: 2 }) // Objeto é adicionado [{}]
set.add({ a: 10, b: 20}) //  [{},{}]

// Por mais que os valores são iguais,
// o objeto ainda assim é diferente,
// pois ele está referenciado 
// em outro endereço de memoria
set.add({a: 1, b: 2}) //  [{}, {}, {}]


console.log(set) // Set(3) { { a: 1, b: 2 }, { a: 10, b: 20 }, { a: 1, b: 2 } }

Usando Set para remover duplicidade

Quando usamos a API Set para remover itens duplicados de array, percebemos a diferença de tempo usando Set em relação do for.

/**
 * @desc Gera um array de tamanho N com números aleatórios, respeitando N
 * @param {number} length 
 */
function generateRandomArray(length) {
  return Array.from(Array(length), () => parseInt(Math.random() * length));
}

// Itera 10 vezes de 10k em 10k até chegar em 100k
for (let length = 1; length <= 100000; length += 10000) {
  // Para cada iteração, gera um novo array.
  const randomList = generateRandomArray(length)

  console.log(`List size of ${randomList.length}`)
  console.time(`Remove ${randomList.length} duplicated items using Set API`)
  const uniqList = Array.from(new Set(randomList))
  console.timeEnd(`Remove ${randomList.length} duplicated items using Set API`)
  console.log('---------')
}

Gera a seguinte saída:

Print de tempo para remover itens duplicados usando Set

Isso acontece porque, diferente do loop, precisamos iteirar o array n vezes, e em cada iteração a API Set garante que estamos adicionando um único valor, e pelo fato do objeto Set implementar a interface iterable, podemos transforma-lo em um Array

Array.from(new Set([1,2,3,4,1,2,3,4])) // Gera [1,2,3,4]

Duplicidade em uma lista de objetos

No mundo real sabemos que listas não são compostas só do tipo primitivo, como faríamos então para os objetos?

Ao invés de usarmos o Set, usamos o Map junto com o método .reduce da API Array, mas pra isso preciso dar um panorama do que é o Map

Maps

A estrutura do Map serve como uma estrutura de dados de Chave valor, ou HashTable que, de forma resumida, é uma lista de dados composta de chave valor, onde para cada item adicionado existe um id or key relacionado, sendo possível realizar uma busca rápida apenas usando a key, sem a necessidade de percorrer toda a lista para encontrar o item

const map = new Map()

map.set(1, { a: 1, b: 2, b: 3 }) // Map(1) { 1 => { a: 1, b: 3 } }
console.log(map)

map.set(2, { a: 10, b: 20, c: 30 }) //  Map(2) { 1 => { a: 1, b: 3 }, 2 => { a: 10, b: 20, c: 30 } }
console.log(map)

// Sobrescreve o objeto na chave 1.
map.set(1, { a: 100 }) // Map(2) { 1 => { a: 100 }, 2 => { a: 10, b: 20, c: 30 } }

map.get(1)  // { a: 100 }
map.get(2)  // { a: 10, b: 20, c: 30 }
map.get(3)  // undefined, pois na chave 3 não existe nada

E claro, o valor da chave não precisa ser necessariamente um valor numérico, pode ser qualquer tipo de dado:

const map = new Map()

map.set('samsung', ['S10', 'S20']) // Map(1) { 'samsung' => [ 'S10', 'S20' ] }

map.set('outro valor', [2, 3, 4, 5]) // Map(2) { 'samsung' => [ 'S10', 'S20' ], 'outro valor' => [ 2, 3, 4, 5 ] }

Usando Map para remover itens duplicados

Agora tendo uma ideia de como usar o Map podemos tirar vantagem do .reduce para gerar um array único a partir de uma lista com duplicidades.

Primeiro vamos criar uma função que gera uma lista com o mesmo objeto, variando apenas o id de cada item.

/**
 * @desc Gera uma lista com o mesmo objeto,
 * onde o id sera aleatório
 * @param {number} length 
 */
function generateRandomObjectList(length) {
  const defaultObject = {
    name: 'Guilherme',
    developer: true
  }
  return Array.from(Array(length), () => {
    const randomId = parseInt(Math.random() * length)
    return {
      ...defaultObject,
      id: randomId
    }
  });
}

Agora vamos criar um objeto Map a partir do array gerado,
onde o id do Map sera o id do usuario, assim removemos IDs duplicados da lista:

const listObjectWithRandomId = generateRandomObjectList(10 ** 5) // 100k
const objectMap = listObjectWithRandomId.reduce((map, object) => {
  map.set(object.id, object);
  return map
}, new Map())

Como o Map também um objeto iterável, basta usar a função Array.from:

const uniqList = Array.from(objectMap, ([_, value]) => value)

O código todo ficaria assim:

/**
 * @desc Gera uma lista com o mesmo objeto,
 * onde o id sera randômico
 * @param {number} length 
 */
function generateRandomObjectList(length) {
  const defaultObject = {
    name: 'Guilherme',
    developer: true
  }
  return Array.from(Array(length), () => {
    const randomId = parseInt(Math.random() * length)
    return {
      ...defaultObject,
      id: randomId
    }
  });
}

const listObjectWithRandomId = generateRandomObjectList(10 ** 5) // 100k

console.time('uniq List usando Map') // Pra contabilizar o tempo da operação
const objectMap = listObjectWithRandomId.reduce((map, object) => {
  map.set(object.id, object);
  return map
}, new Map())


const uniqList = Array.from(objectMap, ([_, value]) => value)
console.timeEnd('uniq List usando Map')
console.log(`Lista duplicada: ${listObjectWithRandomId.length}`)
console.log(`Lista duplicada: ${uniqList.length}`)

Print do tempo para remover itens duplicados usando Map

Conclusão

Por mais que libs como o lodash tenham funções para remover itens duplicados, importar uma lib inteira para resolver um problema que pode ser resolvido em algumas linhas de código de forma nativa acaba sendo não necessário.

]]>
<![CDATA[ Conheça o SparkPlug, o novo compilador de JS do V8 ]]> https://blog.lsantos.dev/v8-sparkplug/ 611bff357b41064670cfc906 Ter, 17 Ago 2021 17:16:48 -0300 O JavaScript é uma caixinha de surpresas, essa parece ser uma linguagem extremamente simples que roda em todos os lugares. Mas é justamente essa versatilidade que torna o JS cada vez mais complexo.

Há um tempo atrás publiquei uma sequencia de 10 artigos sobre como o NodeJS funciona por baixo dos panos. E muito do que eu falei ali não se limita apenas ao NodeJS, mas sim ao JavaScript como um todo.

Por exemplo, o V8 é o engine que está por trás dos principais avanços de performance que o JavaScript teve ao longo dos anos, e isso veio graças aos avanços do browser (principalmente do Chrome).

Vamos entender o que foi adicionado recentemente ao V8, que pode ser muito benéfico para aplicações que tem vida curta, como CLIs e pequenos sites. Estamos falando do novo compilador super rápido chamado sparkplug!

Entendendo o V8

O V8 é a principal razão pela qual temos hoje um JavaScript extremamente rápido. Para alcançar esse patamar de eficiência, o V8 foi aprimorado ao longo de quase uma década para extrair o máximo possível de todas as etapas da construção de uma aplicação.

Estas etapas são o que chamamos de pipeline de compilação. Pense nela como uma sequencia de passos que a sua aplicação (o seu código) percorre para se tornar um código que é executável pelo browser e, consequentemente, pelo computador.

Eu não vou entrar em detalhes de como ele funciona aqui, porque eu já fiz isso na parte 4 da minha sequencia de artigos, mas hoje, temos a seguinte pipeline:

Um fluxograma do fluxo de código do V8

Veja que temos três etapas principais, a primeira é o parser de código, onde o código é interpretado de texto para uma representação intermediária chamada de bytecode (entenda mais sobre ele aqui) e passada para um outro interpretador chamado Ignition. O trabalho do Ignition é justamente otimizar os bytecodes para que o próximo compilador possa otimizá-lo ainda mais.

Em suma, o Ignition vai pegar o código completo em bytecode e vai otimizá-lo em uma única passada e ai sim passar para a próxima etapa que é o Turbofan.

O Turbofan é o compilador de otimização do V8, ele é dividido em camadas que operam para otimizar diferentes partes do código em diferentes momentos, além de gerar o código final para diferentes arquiteturas de sistema.

O que temos de novo

Desde 2016 o time do V8 vem notando que os gargalos de velocidade e performance do JavaScript estão acontecendo antes da compilação do código pelo Turbofan, ou seja, no início da pipeline.

Apesar de o Ignition ser bastante otimizado e otimizar o código em uma passada única, o que faz com que ele possa ser servido para o browser e executado de forma instantânea, ainda sim a performance não se mostrava satisfatória.

Isso veio a tona com uma alteração na forma como eles estavam medindo a performance, passaram a não utilizar mais benchmarks chamados de sintéticos (como ferramentas de teste tipo Octane) e começaram a usar dados reais de navegação para medir as performances de sites e do próprio engine.

O problema aqui é que existem coisas que não podem ser mais otimizadas do que já estão, por exemplo, o parser do V8 é bastante rápido, mas existem coisas que um parser precisa fazer que não podem ser simplesmente removidas da pipeline.

Além disso, com um modelo de dois compiladores na pipeline, não era possível fazer muita divisão e aumentar ainda mais a performance, isso porque a única forma de tornar tudo mais rápido seria remover as passagens de otimização o que, no final, acaba só por reduzir a performance ainda mais.

A solução, criar um novo compilador e colocar no meio dos dois:

A nova pipeline simplificada

Esse compilador foi chamado de Sparkplug.

O que é o Sparkplug?

O objetivo maior do Sparkplug é ser rápido, mas muito rápido mesmo. Ele é tão rápido que é possível ignorar quase que completamente o tempo de compilação e executar uma recompilação completa do código a qualquer momento.

O segredo para isso, na verdade, não é bem um segredo, é um hack. A realidade é que ele não compila as funções do zero, elas já foram compiladas para bytecode antes pelo Ignition, e ele já fez a maior parte do trabalho tentando descobrir quais são os valores das variáveis, se parênteses são arrow functions, transformando destructurings em atribuições e muito mais.

A grande sacada é que o Sparkplug não vai gerar nenhuma representação intermediária (chamada de IR). A IR é basicamente um código que é o meio termo entre código de máquina e o bytecode, geralmente são agrupados em trios de instruções e são muito comuns na maioria dos compiladores. Ao invés disso, o código pula algumas etapas e é compilado diretamente para a máquina.

Um fato interessante é que o Sparkplug é, na verdade, um grande switch dentro de um for que, basicamente, lê cada bytecode individualmente e manda a instrução para a geração do código de máquina

Isso é ótimo para velocidade, mas infelizmente não é possível otimizar muita coisa só com essas informações. Por isso o Sparkplug é um compilador sem otimizações.

Então qual é o motivo disso tudo, já que ele não otimiza o código? A grande ideia da adição do Sparkplug é que, mesmo sendo somente uma serialização do parser, ela ainda é útil, porque pré-compila todas as etapas que não poderiam ser otimizadas no interpretador em si. Dessa forma temos um aumento grande de performance só por remover esses pequenos passos não otimizáveis no início.

Segundo o time do V8, os ganhos de performance do Sparkplug são de 5-15% a mais do que sem o compilador!

Dê uma lida no artigo original que tem muito mais informações sobre como o Sparkplug mantém essa compatibilidade já com todo o ecossistema existente!

]]>
<![CDATA[ O ES2021 foi aprovado! Confira a lista de novidades no JavaScript ]]> https://blog.lsantos.dev/es2021/ 60df752dee7e124080fcf4d7 Qua, 14 Jul 2021 10:00:00 -0300 Como já sabemos, todos os anos a ECMA faz uma lista de novidades que vai sair nas suas próximas versões. Estas modificações são baseadas nas propostas do projeto no repositório do TC39 e precisam ser aprovadas antes de entrarem em alguma versão da linguagem.

A versão 2021 da especificação da ECMA está pronta e já foi validada! Então já sabemos o que vai vir por ai! Vamos a uma lista bem rápida.

Logical Assignment Operators

Esta é uma proposta que já está conosco há um tempo, eu mesmo já escrevi sobre ela. Basicamente a ideia é incluir três novos operadores na linguagem: &&=, ||= e ??=. O que eles fazem?

A ideia básica é substituir os operadores ternários, por exemplo. Ao invés de fazermos algo deste tipo:

if (!user.id) user.id = 1

Ou até algo mais simples:

user.id = user.id || 1

Podemos fazer uma substituição:

user.id ||= 1

O mesmo vale para quando temos um operador de validação nula como o ?? e o and com o &&.

Numeric separators

Existe apenas para prover uma separação visual entre números no código. Então agora, podemos utilizar _ no meio de números para separar suas casas sem contar como um operador ou uma parte do código, vou tirar o próprio exemplo da proposta para demonstrar:

1_000_000_000           // Ah, so a billion
101_475_938.38          // And this is hundreds of millions

let fee = 123_00;       // $123 (12300 cents, apparently)
let fee = 12_300;       // $12,300 (woah, that fee!)
let amount = 12345_00;  // 12,345 (1234500 cents, apparently)
let amount = 123_4500;  // 123.45 (4-fixed financial)
let amount = 1_234_500; // 1,234,500

Promise.any e AggregateError

Essas são as duas funções mais interessantes da proposta. Vamos começar com o Promise.any.

Esta especificação permite uma variação do Promise.all. A diferença é que, quando tínhamos um erro no Promise.all, todas as promises eram rejeitadas. Já no Promise.any, se qualquer uma das promises for resolvida, teremos um resultado.

Promise.any([
    fetch('https://existeenaofalha.com.br').then(()=>'home'),
    fetch('https://existeefalha.com.br').then(()=>'erro')
   ])
    .then((first) => console.log('o primeiro resultado que vier'))
	.catch((error) => console.error(error))

A questão do AggregateError é basicamente uma questão de facilidade. Como retornar uma sequencia de erros de várias promises que poderiam ter falhado? Então uma nova classe de erros foi criada para que seja possível encadear e adicionar múltiplos erros em um único erro agregado.

String.prototype.replaceAll

Antigamente, quando rodávamos algo como 'x'.replace('', '_'), iríamos somente obter a substituição para a primeira palavra uma única vez, se quiséssemos fazer isso no texto todo, teríamos que usar uma regex, como 'xxx'.replace(/(?:)/g, '_') para obter uma substituição geral.

Com o replaceAll, temos o resultado da segunda usando a sintaxe da primeira:

'xxx'.replaceAll('', '_') //'_x_x_x_'

WeakRefs e FinalizationRegistry

Estas são duas APIs avançadas que devem ser evitadas se possível. Tanto que não vou colocar muitos exemplos mas sim linkar diretamente para as documentações oficiais.

A ideia das WeakRefs é prover uma referência fraca a um objeto na memória, esta referência permite que estes objetos sejam coletados pelo Garbage Collector livremente, liberando a memória que estão alocando assim que qualquer referência para elas seja removida.

Em um caso normal, uma referência forte, como em listeners e outros objetos, iria impedir o GC de coletar a memória para que não haja nenhum tipo de erro de acesso futuro. Veja mais sobre ela na documentação.

Já os finalizers podem ou não ser usados em conjunto com as WeakRefs e provêm uma forma de executar uma função assim que o GC coletar estes objetos da memória. Mas não só estes objetos fracamente referenciados, os finalizers podem ser encaixados em qualquer objeto para executar um callback assim que eles forem coletados e destruídos. Veja mais na documentação.

let target = {};
let wr = new WeakRef(target);

// a WR e o target não são o mesmo objeto

// Criamos um novo registro
const registry = new FinalizationRegistry(value => {
  // ....
});

registry.register(myObject, "valor", myObject);
// ...se você não ligar mais para `myObject` algum tempo depois...
registry.unregister(myObject);
]]>
<![CDATA[ O Guia Completo do gRPC parte 4: Streams ]]> https://blog.lsantos.dev/o-guia-do-grpc-4/ 60d3933604791135df105eb6 Qua, 07 Jul 2021 09:00:00 -0300

Este artigo é parte de uma série

  1. O guia completo do gRPC parte 1: O que é gRPC?
  2. O guia completo do gRPC parte 2: Mãos à obra com JavaScript
  3. O guia completo do gRPC parte 3: Tipos em tudo com TypeScript!
  4. O guia completo do gRPC parte 4: Streams!

Nos artigos anteriores desta série aprendemos o que é gRPC, como ele funciona e como podemos utilizar esse protocolo para trafegar dados entre sistemas com diferentes tecnologias e linguagens. Porém tudo isso foi feito usando somente os modelos mais simples de definição do protobuf, ou seja, estávamos enviando uma requisição simples e recebendo uma resposta simples em um modelo cliente/servidor.

Streaming

Além das que são chamadas Unary Calls, temos também Streaming calls, que nada mais são do que respostas e requisições realizadas por meio de uma stream de dados assíncrona. Temos três tipos de streaming calls no gRPC:

  • Serverside streaming: Quando a requisição é enviada de forma simples (unária), mas a resposta do servidor é uma stream de dados.
  • Clientside streaming: É o oposto da anterior, quando temos a requisição sendo enviada em forma de streams de dados e a resposta do servidor é unária.
  • Duplex streaming: Quando tanto a requisição quando a resposta são streams de dados.

Isso é refletido dentro de um arquivo .proto de forma bem simples. Vamos voltar ao nosso repositório para o segundo artigo da série, lá temos o seguinte arquivo notes.proto:

syntax = "proto3";

service NoteService {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entities
message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

message Void {}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

// Responses
message NoteFindResponse {
  Note note = 1;
}

message NoteListResponse {
  repeated Note notes = 1;
}

Se quiséssemos modificar a chamada para que, ao invés de enviarmos uma lista de notas pronta, enviássemos uma stream de notas como resposta no serviço List, podemos simplesmente adicionar a palavra stream na direção que queremos:

service NoteService {
  rpc List (Void) returns (stream NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

Pronto! Não precisamos fazer mais nada, nossa resposta vai ser uma stream de notas como definida em NoteListResponse.

Para os outros modelos de stream, podemos seguir a mesma ideia, se quisermos uma clientside stream, colocamos stream somente do lado da request:

service NoteService {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (stream NoteFindRequest) returns (NoteFindResponse);
}

E para duplex streams, colocamos stream em ambos os lados:

service NoteService {
  rpc List (Void) returns (stream NoteListResponse);
  rpc Find (stream NoteFindRequest) returns (stream NoteFindResponse);
}

O que são streams

Se você ainda não conhece o conceito de streams, não se preocupe, eu fiz uma série de artigos no iMasters somente sobre isso:

Basicamente, streams são uma corrente contínua de dados que são carregados no momento da sua leitura. Esse modelo tem vários benefícios, por exemplo, quando estamos trabalhando com arquivos ou conteúdos muito grandes, se tivermos que devolver estes conteúdos para quem pediu, teríamos que carregar todo o arquivo na memória primeiro, para depois poder responder.

Se seu arquivo tem, digamos, 3GB, então você vai usar 3GB de memória. Enquanto em uma stream, você vai mostrando o arquivo conforme ele é carregado e o conteúdo que veio depois vai sendo descartado e liberado da memória. Dessa forma você tem um processamento muito mais rápido usando muito menos recursos.

Nesta palestra eu mostrei visualmente o que isso significa:

Por esse motivo, streams são muito utilizadas com arquivos e dados de grande porte, porque elas podem suportar uma quantidade imensa de informação usando pouquíssimos recursos.

Streams e gRPC

Como é tão simples de utilizar streams no gRPC, já era de se esperar que o suporte a elas no protocolo fosse muito bom. E isso é, de fato o que acontece, o suporte a streams no gRPC um dos melhores existentes e integram com quase todas as linguagens suportadas.

Para esta demonstração, vamos utilizar a mesma aplicação que usamos no artigo número 2, e vamos fazer algumas alterações sobre ela para transformar uma chamada unária em uma chamada assíncrona.

O código desta demonstração está no meu GitHub

Vamos começar de uma base, clonamos o repositório original do artigo 2 para podermos ter a aplicação completa. A primeira coisa que precisamos fazer é trocar o nosso arquivo .proto para adicionar uma stream ao serviço de listagem de notas.

A primeira alteração é simplesmente adicionar stream no rpc List. E depois vamos remover o NoteListResponse para que tenhamos uma resposta somente como Note, o arquivo fica assim:

syntax = "proto3";

service NoteService {
  rpc List (Void) returns (stream Note);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entities
message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

message Void {}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

// Responses
message NoteFindResponse {
  Note note = 1;
}

É importante ressaltar que estamos somente removendo a entidade de resposta porque, como estamos falando de uma stream, obviamente todos os dados que virão serão notas. Se mantivéssemos como uma resposta do tipo { note: { } }, a cada chunk da stream teríamos um novo objeto note que teria (claro), uma nota dentro... Isso é bastante repetitivo.

Servidor

O próximo passo é alterar o nosso servidor, na verdade, somente uma pequena parte dele. A primeira e mais simples alteração que vamos fazer é remover o nosso pequeno banco de dados in loco que temos nossas três notas fixas e passar ele para um arquivo notes.json que vai representar uma quantidade grande de dados.

Neste arquivo coloquei aproximadamente 200 notas:

[
  {
    "id": 0,
    "title": "Note by Lucas Houston",
    "description": "Content http://hoateluh.md/caahaese"
  }, {
    "id": 1,
    "title": "Note by Brandon Tran",
    "description": "Content http://ki.bo/kuwokal"
  }, {
    "id": 2,
    "title": "Note by Michael Gonzalez",
    "description": "Content http://hifuhi.edu/cowkucgan"
  }, { ...
Lembrando que 200 notas não é, nem de perto, uma quantidade grande de dados. Este é apenas um exemplo.

Agora, carregamos o arquivo no topo do nosso servidor com require (lembrando que isto não funciona para ES Modules:

const grpc = require('grpc')
const protoLoader = require('@grpc/proto-loader')
const path = require('path')
const notes = require('../notes.json')

A segunda parte do arquivo que vamos mudar será a definição do método List. Para isso vamos olhar a definição antiga por um momento:

function List (_, callback) {
  return callback(null, { notes })
}

Temos algumas coisas para alterar aqui:

  1. A resposta não pode ser mais { notes }, porque não vamos mais devolver um objeto
  2. Não vamos mais poder devolver todo o arquivo de uma vez, ou nossa chunk será muito grande, vamos iterar linha a linha por nota para poder devolver para o cliente
  3. A assinatura da função não leva mais um callback

Vamos resolver isso tudo da seguinte maneira, primeiramente, ao invés de dois parâmetros de uma chamada unária, uma stream somente leva um único parâmetro, que vamos chamar de call:

function List (call) {
    //
}

O objeto call é uma implementação de uma stream de escrita juntamente com o registro da chamada, portanto, se tivéssemos algum tipo de parâmetro para ser enviado, poderíamos obte-los através de call.request.parametro.

Vamos agora definir que uma chunk da nossa stream será uma nota individual, portanto vamos iterar pelo array de notas e devolver as notas individualmente:

function List (call) {
  for (const note of notes) {
    call.write(note)
  }
  call.end()
}

Perceba que estamos chamando call.write e passando diretamente a nota, isto porque alteramos nossa resposta para ser somente uma nota e não um objeto com uma chave note.

É interessante notar também que, assim que a chamada para write é detectada, a resposta vai ser enviada e o cliente vai receber a mesma, isso é interessante quando temos que fazer algum tipo de processamento, por exemplo, se precisássemos transformar todos os títulos em letras maiúsculas, poderíamos fazer essa transformação e ir enviando os resultados sem esperar que todas as notas fossem carregadas.

No final, chamamos call.end(), o que é importante, pois instrui o cliente a fechar a conexão, se isso não for feito, o mesmo cliente não poderá fazer outra chamada para o mesmo serviço.

Client

Para o cliente, pouquíssima coisa irá mudar, na verdade, somente a chamada do método. Nossa chamada antiga podia ser feita de duas maneiras:

client.listAsync({}).then(console.log)
client.list({}, (err, notes) => {
  if (err) throw err
  console.log(notes)
})

Agora não podemos mais chamar ela de duas formas, pois a stream é obrigatória assíncrona. Além disso, não vamos ter um callback, ao invés disso vamos realizar a chamada para o servidor que nos devolverá uma stream de leitura e, somente depois de criarmos um listener para esta stream, que a chamada será realmente feita e os dados serão retornados.

Isso significa que vamos trabalhar com o padrão event emitter e event listener, muito comuns no Node e no JavaScript. Nossa função ficará assim:

const noteStream = client.list({})
noteStream.on('data', console.log)

Para ser mais explicito, podemos fazer desta forma:

const noteStream = client.list({})
noteStream.on('data', (note) => console.log(note))

A stream também tem outro evento chamado end, que é executado quando a stream do servidor chama o método call.end(). Para escutá-lo, basta criar outro listener;

noteStream.on('end', () => {})

Clientside streaming

Para completarmos o artigo e não deixarmos nada para trás. No caso de utilizarmos um modelo como:

rpc Find (stream NoteFindRequest) returns (NoteFindResponse);

Onde o cliente que realiza a requisição utilizando streams, vamos ter uma implementação parecida no servidor. A grade diferença é que nosso método Find, do lado do servidor receberá, como primeiro parâmetro, a stream do cliente e o segundo continuará sendo o callback.

Este é o nosso método antigo, com as duas chamadas unárias:

function Find ({ request: { id } }, callback) { }

Ele ainda é válido porque a chamada possui uma propriedade request. Mas não temos o método on, então vamos atualizar para:

function Find (call, callback) { }

E podemos receber os dados do cliente da mesma forma que recebemos os dados do servidor em serverside streaming:

function Find (call, callback) {
    call.on('data', (data) => {
        // fazer algo
    })
    call.on('end', () => {
        // a chamada terminou
    })
}

E no client, teremos uma chamada exatamente igual ao do servidor, porém temos que contar que o servidor, desta vez, não nos retorna uma stream, portanto temos um callback:

const call = client.find((err, response) => {
    if (err) throw err
    console.log(response)
})

call.write({ id: 1 })
call.end()

A função interna do find só será executada após o método end() ser chamado.

Duplex streams

Para duplex streams (ou bidirectional streams), basta implementarmos, tanto do lado do servidor quanto do cliente, o parâmetro call. Este parâmetro é uma stream bidirecional que contém tanto o método on quanto o write.

No server teríamos algo como:

function duplex (call) {
    call.on('data', (data) => {
        // recebendo dados do cliente
    })
    call.write('devolvendo dados para o cliente')
    call.end() // servidor encerra a conexão
}

E no client teríamos uma chamada como:

const duplex = client.duplex()
duplex.on('data' (data) => {
	// recebe dados do servidor
})
duplex.write('envia dados ao servidor')
duplex.close() // client fecha conexão
]]>
<![CDATA[ Apresentando o KEDA HTTP Add-on ]]> https://blog.lsantos.dev/apresentando-o-keda-http-add-on/ 60df66a3ee7e124080fcf457 Sex, 02 Jul 2021 17:01:58 -0300 Uma das coisas que me deixa ainda mais feliz em trabalhar com open source é quando podemos transformar os projetos em realidade e ajudar muitas pessoas com o que pretendemos fazer!

Há um tempo atrás, iniciei uma jornada ajudando os mantenedores do KEDA a criar um novo add-on para o ecossistema e, finalmente, este add-on foi publicado e está em beta!

O que é o KEDA

Atualmente, temos uma série de formas de escalar workloads no Kubernetes, todas elas usam a API de Autoscaling nativa para poder criar o que chamamos de HorizontalPodAutoscalers.

Estes scalers permitem que façamos a escalabilidade baseado em recursos da máquina, como CPU e memória, e também (com bastante dificuldade) métricas customizadas provenientes de outros serviços como, por exemplo, a contagem de requisições ou então requisições por segundo de um ingress controller padrão.

Porém, justamente por conta da dificuldade de fazer este tipo de escalabilidade nativamente, surgiram outros projetos para facilitar a escalabilidade não só através de métricas locais, mas também a partir de serviços externos. E, o mais importante, permitir que escalássemos as aplicações para zero.

O caso mais comum é quando temos uma aplicação que é um worker, ou seja, ouve de uma fila de mensagens e processa apenas uma mensagem por vez. Não faz sentido que esta aplicação seja executada a todo momento, uma vez que ela não vai estar o tempo todo realizando algum trabalho, apenas consumindo recursos de máquina. Seria muito mais eficiente que não houvessem workers ativos até que uma mensagem exista na fila.

E é ai que o KEDA (que significa Kubernetes Event Driven Autoscaling) entra em cena. O KEDA funciona com uma série de scalers, cada scaler é em si uma pequena aplicação que conecta a uma fonte de métricas e as repassa para o controlador principal, que toma a decisão de escalar ou não uma aplicação alvo.

Dessa forma podemos escalar baseado em vários serviços. Inclusive, o KEDA possui uma lista extensa de scalers já suportados, porém nenhum deles permitia fazer algo muito simples porém muito útil: escalar baseado na quantidade de requisições HTTP.

HTTP Add-on

Escalar aplicações com base no tráfego de entrada delas é algo que não pensamos muito, porque quase sempre temos um site que fica recebendo milhares de requisições por minuto ou por segundo, ou seja, sempre temos que ter este site no ar.

Mas talvez essa não seja a realidade, e por isso que, com o KEDA, poderíamos plugar um scaler usando o Prometheus para poder extrair métricas de requisições, mas isso exigiria uma instrumentação e também a instalação do Prometheus na sua instância, mesmo que você não estivesse utilizando ele como seu monitor principal. Pensando nisso, começamos a desenvolver a ideia do que viria a se tornar um add-on para o KEDA em si, não um scaler, mas parte do produto.

O artigo oficial de lançamento do projeto explica um pouco sobre a configuração e dá um pequeno tutorial de como começar a trabalhar com o KEDA Add-on, mas ainda sim vou fazer um pequeno tutorial e explicar alguns dos conceitos base que pensamos.

Como funciona?

O add-on é baseado em três principais componentes que seguem a mesma ideia do padrão operator do kubernetes:

  • Interceptor: É o componente principal, ele intercepta todas as requisições HTTP que vem para o serviço da sua aplicação e verifica se o serviço já possui pelo menos uma aplicação para servir a requisição, então realiza uma contagem e faz um simples forwarding para a aplicação de destino. Caso contrário, "segura" a requisição até que a aplicação seja escalada.
  • External Scaler: Este é um componente que já existe no KEDA, a possibilidade de você poder criar seu próprio scaler e implementar uma interface gRPC definida faz com que ele seja muito escalável. Este é um scaler do tipo push que realiza um ping no interceptor para saber a contagem da fila de pendencias e então transforma estes dados para que o KEDA possa entender.
  • Operator: O operador é o cérebro de controle, ele roda pela conveniência de não precisar criar todas estas aplicações manualmente. Como o add-on é baseado em CRDs, a criação de um novo CRD do tipo HTTPScaledObject faz com que sejam criados tanto o serviço quando o interceptor e o scaler para sua aplicação.

A arquitetura roda da seguinte maneira:

Arquitetura do projeto KEDA HTTP

Veja que o add-on não interfere na aplicação do usuário, que continua tendo poder completo sobre o load balancer e o ingress, bem como os serviços e o deploy da aplicação.

Como usar

Como este projeto é um add-on do KEDA, precisamos primeiramente instalar o KEDA em um cluster Kubernetes. Para facilitar, vamos instalar tudo usando o Helm:

helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install --create-namespace -n <namespace> keda kedacore/keda

Depois, podemos instalar o operador do add-on pelo mesmo serviço de charts:

helm install \
  --create-namespace <namespace> \
  http-add-on \
  kedacore/keda-add-ons-http

Depois, podemos criar um objeto HTTPScaledObject:

apiVersion: http.keda.sh/v1alpha1
kind: HTTPScaledObject
metadata:
  name: meuApp
spec:
  scaleTargetRef:
    deployment: meuDeployment
    service: meuService
    port: 8080

Isso é o suficiente para o add-on poder criar tudo para você e começar a escalar o seu app!

Conclusão e próximos passos

Atualmente a aplicação ainda está em beta então não recomendamos o uso em produção, pois a API pode mudar drásticamente no futuro. Existem várias propostas novas que estamos considerando, como tráfego norte-sul através de Ingresses e a Gateway API e também leste-oeste (comunicação entre serviços) usando service meshes.

Além disso, estamos constantemente atualizando o projeto e criando novas issues e ajudando no desenvolvimento futuro do projeto!

Este projeto é um esforço em conjunto de muitas pessoas, incluindo eu mesmo, o agradecimento vai para:

]]>
<![CDATA[ Iniciando com ECMAScript Modules ]]> https://blog.lsantos.dev/os-ecmascript-modules-estao-aqui/ 60ca548ce232655391141f42 Qua, 23 Jun 2021 08:00:00 -0300 Já faz algum tempo que se fala em termos disponibilidade de utilizarmos os ECMAScript Modules em nossos pacotes e código JavaScript. Apesar de o modelo ser suportado na web como um todo através de uma tag <script type="module"> já faz algum tempo, só agora com a depreciação oficial do Node 10 em favor do Node 16 que vamos poder ter este suporte completo no servidor!

Veja um exemplo de uso de módulos ESM no browser neste repositório

Um pouco de história

Desde 2012 existem conversas no GitHub e nos repositórios oficiais do TC39 para a implementação padrão de um novo sistema de módulos que seja mais apropriado para os novos tempos do JavaScript.

Atualmente, o modelo mais comum utilizado é o famoso CommonJS, com ele temos a sintaxe clássica do require() no topo de módulos Node.js, mas ele não era suportado oficialmente pelos browsers sem a ajuda de plugins externos como o Browserify e o RequireJS.

A exigência por um modelo de módulos começou a partir daí. Com as pessoas querendo modularizar suas aplicações JavaScript também no lado do cliente, mas implementar um sistema de módulos não é fácil e levou vários anos até que uma implementação aceitável surgisse.

Com isso, agora temos o chamado ESM (ECMAScript Modules), que muitas pessoas já conheciam, principalmente por se tratar de uma sintaxe que já acompanha o TypeScript desde sua criação, ou seja, não vamos mais trabalhar com módulos através de require(), mas sim através de uma chave imports e outra exports.

CommonJS

Em um caso clássico de uso do CommonJS temos um código que pode ser assim:

function foo () { }

module.exports = foo

Perceba que tudo que o Node.js (neste caso) vai ler, é um objeto chamado module, dentro deste nós estamos definindo uma chave exports que contém a lista de coisas que vamos exportar para este módulo. Depois, outro arquivo poderá importa-lo como:

const foo = require('./foo')

Quando importamos um módulo usando esta sintaxe, nós o estamos carregando sincronamente, porque o algoritmo de resolução de módulos precisa primeiramente encontrar o tipo do módulo, se for um módulo local é obrigatório que o mesmo comece com ./ caso contrário a resolução de módulos irá buscar nas pastas conhecidas por módulos existentes.

Depois de encontrar o módulo, precisamos ler o conteúdo, fazer o parsing e gerar o objeto module que será utilizado para descobrir o que podemos ou não importar deste módulo.

Este tipo de importação, principalmente por ser sincrona, causa alguns problemas na hora de executar aplicações na natureza mais assíncrona do Node.js, portanto muitas pessoas acabavam importando os módulos somente quando necessários.

ESM

No ESM temos uma mudança drástica de paradigma. Ao invés de importarmos módulos de forma síncrona, vamos começar a importar de forma assíncrona, ou seja, não vamos travar o event loop com algum tipo de I/O.

Além disso, não temos mais que definir manualmente o que os módulos importam ou exportam, isso é feito através das duas keywords imports e exports, sempre que parseadas, o compilador irá identificar um novo símbolo que será exportado ou importado e adicionar automaticamente à lista de exportação.

Os ESM também vem com algumas regras padrões que deixam a resolução de módulos mais precisa e, portanto, mais rápida. Por exemplo, é sempre obrigatório que você acrescente a extensão do arquivo quando importando algum módulo. O que significa que a importação de módulos somente pelo nome do arquivo não é mais válida:

import foo from './foo.js'

Isso faz com que o sistema de resolução não tenha que saber que tipo de arquivo estamos tentando importar, pois com require() podemos importar vários tipos de arquivos além do .js, como JSON. O que nos leva para a segunda grande mudança, muitos dos tipos de arquivos que antes eram suportados por importação direta, agora precisam ser lidos via fs.promises.readFile.

Por exemplo, quanto queríamos importar um arquivo JSON diretamente, poderíamos rodar um require('arquivo.json'), porém agora não temos mais essa capacidade e precisamos utilizar o módulo de leitura de arquivos para poder ler o JSON nativamente.

Existe uma API ainda experimental para poder permitir a funcionalidade no Node.js mas ela vem desativada por padrão, veja mais sobre ela aqui

Então, para importar um JSON como um objeto você pode fazer assim:

import {promises as fs} from 'fs';

const packageJson = JSON.parse(await fs.readFile('package.json', 'utf8'))

Todo o caminho para um módulo no ESM é uma URL, portanto o modelo suporta alguns protocolos válidos como file:, node: e data:. Isso significa que podemos importar um módulo nativo do Node com:

import fs from 'node:fs/promises'

Não vamos nos estender aqui, mas você pode checar mais sobre essa funcionalidade na documentação do Node.

O ESM também suporta uma nova extensão de arquivo chamada .mjs, que é muito útil porque não precisamos nos preocupar com a configuração, já que o Node e o JavaScript já sabem resolver este tipo de arquivo.

Outras mudanças incluem a remoção de variáveis como __dirname dentro de módulos no Node.js. Isto porque, por padrão, módulos possuem um objeto chamado import.meta, que possui todas as informações daquele módulo, que antes eram populadas pelo runtime em uma variável global, ou seja, temos um estado global a menos para nos preocupar.

Para poder resolver um caminho do módulo local a sem usar o __dirname, uma boa opção é utilizar o fileURLToPath:

import { fileURLToPath } from 'node:url'
import path from 'node:path'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

Embora você também possa importar utilizando a URL diretamente com new URL(import.meta.url) já que muitas APIs do Node aceitam URLs como parâmetros.

Por fim, a mais esperada de todas as mudanças que vieram nos módulos é o top-level await, isso mesmo, não precisamos mais estar dentro de uma função async para executar um await, mas isso só para módulos! Então coisas deste tipo serão muito comuns:

async function foo () {
  console.log('Hello')
}

await foo() // Hello

Já até tivemos que utilizar essa funcionalidade dentro da nossa função para ler um arquivo JSON.

Interoperabilidade

O ESM demorou tanto tempo porque ele precisava ser o mínimo compatível com o CommonJS da forma como ele estava no momento, por isso a interoperabilidade entre os dois é muito importante, já que temos muito mais módulos em CommonJS do que em ESM.

No CJS (CommonJS) tínhamos a possibilidade de um import assíncrono usando a função import(), e estas expressões são suportadas dentro do CJS para carregar módulos que estão escritos em ESM. Então podemos realizar uma importação de um módulo ESM desta forma:

// esm.mjs
export function foo () {
  return 1
}

// cjs.js
const esm = import('./esm.mjs')
esm.then(console.log) // { foo: [λ: foo], [Symbol(Symbol.toStringTag)]: 'Module' }

Do outro lado, podemos utilizar a mesma sintaxe de import para um módulo CJS, porém temos que ter em mente que todo módulo CJS vem com um namespace, no caso padrão de um módulo como o abaixo, o namespace será o default:

function foo () { }
module.exports = foo

E, portanto, para importar esse módulo podemos importar seu namespace através de um named import:

import {default as cjs} from './cjs.js'

Ou então através de um import padrão:

import cjs from './cjs.js'
Se você quiser observar como é um export de um módulo CJS, basta executar um import geral com import * as cjs from './cjs.js' e logar o resultado no console.

No caso do Node.js, também temos uma ótima opção onde, quando usamos exports nomeados com CJS, desta forma:

exports.foo = () => {}
exports.bar = () => {}

O runtime vai tentar resolver cada chave de exports para um import nomeado, ou seja, vamos poder fazer isso:

import { foo } from './cjs.js'

Principais diferenças

Vamos resumir as principais diferenças entre os dois tipos de sistema de módulos para podermos aprender como utilizar:

  • No ESM não existem require, exports ou module.exports
  • Não temos as famosas dunder vars como filename e dirname, ao invés disso temos import.meta.url
  • Não podemos carregar JSON como módulos, temos que ler através de fs.promises.readFile ou então module.createRequire
  • Não podemos carregar diretamente Native Modules
  • Não temos mais NODE_PATH
  • Não temos mais require.resolve para resolver caminhos relativos, ao invés disso podemos utilizar a montagem de uma URL com new URL('./caminho', import.meta.url)
  • Não temos mais require.extensions ou require.cache
  • Por serem URLs completas, módulos ESM podem levar query strings como se fossem páginas HTML, então é possível fazer algo desta forma import {foo} from './module?query=string', isso é interessante para quando temos que fazer um bypass do cache.

Usando ESM com Node.js

Existem duas formas de utilizar o ESM, através de arquivos .mjs ou através da adição da chave type no package.json com o valor "module", isso vai permitir que você continue usando extensões .js mas que tenham módulos ao invés de CJS.

// Usando CJS
{
  "name": "pacote",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
}

// Usando ESM
{
  "name": "pacote",
  "version": "0.0.1",
  "description": "",
  "type": "module",
  "exports": "./index.mjs",
}

Se você está criando um novo pacote do zero com JavaScript, prefira começar já com ESM, para isso você não precisa nem adicionar uma chave type no seu package.json, basta que você altere a chave "main", para exports como neste exemplo:

// Usando CJS
{
  "name": "pacote",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
}

// Usando ESM
{
  "name": "pacote",
  "version": "0.0.1",
  "description": "",
  "exports": "./index.mjs",
}

Outro passo importante é você adicionar a chave engines restringindo quais são as versões do Node que podem executar seu pacote sem quebrar, para esta chave use os valores "node": "^12.20.0 || ^14.13.1 || >=16.0.0".

Se você estiver usando 'use strict' em algum arquivo, remova-os.

A partir daí todos os seus arquivos serão módulos e precisarão das refatorações padrões, como a troca de require por import e a adição das extensões nos nomes de arquivos locais. Como já falamos anteriormente.

ESM com TypeScript

Apesar de utilizar o modelo ESM já há algum tempo, o TypeScript não costuma gerar compilados JavaScript no modelo ESM, somente com CJS. Para que possamos forçar o uso do ESM inclusive nos arquivos de distribuição gerados pelo TS, vamos precisar de algumas configurações básicas.

Primeiramente vamos editar nosso package.json como se tivéssemos criando um módulo JS normal. Isso significa fazer esta lista de coisas:

  • Criar uma chave "type": "module"
  • Susbstituir "main": "index.js" por "exports": "./index.js"
  • Adicionar a chave "engines" com o valor da propriedade "node" para as versões que mostramos anteriormente

Depois, vamos gerar um arquivo tsconfig.json com tsc --init e modificá-lo para adicionar uma chave "module": "ES2020". Isso já será suficiente para os arquivos finais serem expostos como ESM, porém existem algumas precauções que temos que ter quando escrevemos nossos arquivos em TypeScript:

  • Não usar importações relativas parciais como import index from '.', sempre use o caminho completo import index from './index.js'
  • É recomendado usar o protocolo node: para importar módulos nativos do Node como o fs

A parte mais importante e também a que, na minha opinião, é a que mais deixa a desejar para usarmos ESM com TS é que sempre precisamos importar os arquivos com a extensão .js, mesmo que estejamos usando .ts, ou seja, se dentro de um arquivo a.ts você quiser importar o módulo presente em b.ts, você precisará de um import do tipo import {b} from './b.js'.

Isto porque ao compilar, como o TS já usa nativamente ESM como sintaxe, ele não vai remover ou corrigir as linhas de importação dos seus arquivos fonte.

]]>
<![CDATA[ Criando ambientes de teste dinamicamente com GitHub Actions ]]> https://blog.lsantos.dev/ambientes-dinamicos-helm-actions/ 60b92985a338d23c60bbf046 Qua, 09 Jun 2021 10:00:00 -0300

Como otimizar um pipeline de testes para que os times não tenham problemas de concorrência ao testar suas funcionalidades e seus módulos é um assunto recorrente em vários tópicos que tratei tanto no passado quanto recentemente.

Inclusive, já fiz algumas talks sobre o assunto e tenho até um repositório de exemplo usando o Azure DevOps como ferramenta de CI. Você pode ver os slides e o vídeo abaixo!

A questão é: Como podemos fazer com que vários times de desenvolvimento consigam testar suas funcionalidades em um ambiente completamente separado dos demais de forma simples e rápida?

A resposta está, é claro, em containers. Quando utilizamos o Kubernetes com Helm juntamente a uma ferramenta de CI, podemos fazer muitas coisas dinamicamente. Neste artigo, vou atualizar minha palestra anterior e mostrar a mesma aplicação, porém rodando no pipeline do GitHub Actions. Para deixar o cenário mais real, vamos utilizar a Azure com o Azure Kubernetes Service baixando imagens de um Azure Container Registry privado já integrado de forma privada com o cluster. Todos os dados serão armazenados em um CosmosDB do tipo Mongo.

Vamos lá!

Antes de começar

Vamos ter que criar o ambiente antes de começar a mostrar como a parte dinâmica pode ser feita. Como esse não é o intuito do post, vou deixar apenas as referências de comandos do que vamos fazer por aqui, mas todas as documentações necessárias podem ser encontradas diretamente na documentação de cada serviço.

Primeiramente você precisa ter uma conta na Azure, uma vez com esta conta, instale o Azure CLI, vamos utilizar somente a linha de comando.

O primeiro comando será o comando az login para fazer o login na sua conta e escolher qual será a subscription que será utilizada pra criar os recursos. Assim que o login estiver pronto, vamos começar criando o primeiro recurso, o resource group.

az group create -l eastus -n ship-manager-pipeline

Agora vamos criar o nosso ACR para poder armazenar nossas imagens:

az acr create -n shipmanager --sku Basic -g ship-manager-pipeline

Espere até o CR ser criado e execute o seguinte comando para habilitar o login via usuário e senha, só assim vamos poder fazer o login pelo nosso CI para poder construir as imagens:

az acr update -n shipmanager --admin-enabled true

Agora vamos partir para o CosmosDB, com um simples comando podemos criar toda a estrutura:

az cosmosdb create --kind MongoDB -n ship-manager-db -g ship-manager-pipeline

Este comando demora um pouco mais para ser executado, então seja paciente. É importante dizer que, apesar de ser incrível, o CosmosDB não é recomendado para este caso específico de criação de bancos de dados quando temos ambientes efêmeros como estes pois é mais complexo de remover futuramente. Mas, para simplificar, vamos utilizá-lo e vamos entender alternativas que podemos ter a esta abordagem mais a frente no artigo.

Por fim vamos criar o nosso AKS que vai unir todas as partes:

az aks create -n ship-manager -g ship-manager-pipeline \
--enable-addons http_application_routing \
--attach-acr shipmanager \
--vm-size Standard_B2s \
--generate-ssh-keys \
--node-count 2

Isso fará com que nosso AKS seja criado já em conjunto com o ACR, dessa forma não precisamos criar um secret para cada namespace com o nosso arquivo de login do Docker para baixar as imagens, e também não precisamos fazer um bind em uma service account.

Obtenha as credenciais do AKS com o seguinte comando:

az aks get-credentials -n ship-manager -g ship-manager-pipeline --admin

Lembrando que você precisa ter o kubectl instalado na máquina para este comando funcionar, caso não o possua, use o comando az aks install-cli para instalá-lo.

Criando o chart

O primeiro passo para a criação da pipeline é saber como ela vai funcionar, inicialmente vamos trabalhar somente com dois ambientes, o primeiro será o ambiente de produção e o segundo será o ambiente de testes.

O ambiente de produção será publicado sempre que um push com uma tag v* for feito. Já o ambiente de teste será publicado em um push de qualquer outro branch que não seja o master ou main (dependendo do caso).

Para que você possa acompanhar, preparei este repositório de exemplo que contém tanto o código da aplicação quanto o código das actions em si.

Se você quiser acompanhar passo a passo, faça um fork do repositório, mas não se esqueça de remover a pasta .github para que as actions sejam removidas.

Antes de criar os arquivos da pipeline, vamos criar os arquivos do Helm, para podermos criar nosso chart! Crie uma pasta chamada kubernetes na raiz do repositório, depois crie uma segunda pasta chamada ship-manager.

Poderíamos criar o chart do helm de forma automática via CLI, porém ele cria vários arquivos que não precisamos, então vamos criá-lo manualmente para facilitar.

Dentro da pasta ship-manager crie mais duas pastas: templates e charts. Agora crie dois arquivos no mesmo nível da pasta templates, um deles se chamará Chart.yaml e o outro values.yaml.

Agora, vamos para dentro da pasta charts, nela, crie uma pasta backend e, dentro desta última adicione um arquivo Chart.yaml seguido de uma pasta templates.

A estrutura final deverá ser assim:

kubernetes
└── ship-manager
    ├── Chart.yaml
    ├── charts
    │   └── backend
    │       ├── Chart.yaml
    │       └── templates
    │
    ├── templates
    └── values.yaml

No arquivo Chart.yaml da pasta ship-manager vamos escrever o seguinte:

apiVersion: v2
name: ship-manager
description: Chart for the ship manager app
version: 0.1.0

E no da pasta backend será o seguinte:

apiVersion: v2
name: backend
description: Chart for the backend part of the ship manager app
version: 0.1.0

O que fizemos foi criar o arquivo equivalente a um package.json do Helm, ou seja, o arquivo que define o pacote que vamos instalar no nosso cluster.

O Helm funciona com base em uma hierarquia, o que acabamos de criar aqui é uma ordem de dependências, ou seja, acabamos de dizer que o frontend da aplicação, que está no ship-manager, é dependente de um backend localizado na pasta charts, se criássemos outra pasta charts dentro de backend iríamos dizer que o backend é dependente dela e assim sucessivamente. Desta forma, quando damos apenas um comando, o Helm já instala todas as dependências em ordem para nós.

Vamos criar nosso primeiro template, crie um arquivo frontend.yaml dentro da pasta templates localizada na pasta ship-manager. Este template vai ser o que vai ser de fato criado dentro do cluster. Nele vamos ter todos os recursos do Kubernetes, começando pelo Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ship-manager-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ship-manager-frontend
  template:
    metadata:
      labels:
        app: ship-manager-frontend
    spec:
      containers:
        - image: {{ required "Registry is required" .Values.global.registryName }}/{{ required "Image name is required" .Values.frontend.imageName }}:{{ required "Image tag is required" .Values.global.imageTag }}
          name: ship-manager-frontend
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 250m
              memory: 256Mi
          ports:
            - containerPort: 8080
              name: http
          volumeMounts:
            - name: config
              mountPath: /usr/src/app/dist/config.js
              subPath: config.js
      volumes:
        - name: config
          configMap:
            name: frontend-config

Veja que estamos utilizando placeholders do Helm para poder identificar partes que podem ser alteradas, e é isso que faz com que tudo seja possível. A criação de ambientes e alteração de variáveis em tempo de CLI e não depois de compilado faz com que possamos passar os valores que quisermos para essas variáveis ao criar o ambiente.

Depois temos as outras configurações:

apiVersion: v1
kind: Service
metadata:
  name: ship-manager-frontend
spec:
  selector:
    app: ship-manager-frontend
  ports:
    - name: http
      port: 80
      targetPort: 8080
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ship-manager-frontend
  annotations:
    kubernetes.io/ingress.class: addon-http-application-routing
spec:
  rules:
    - host: {{ default "ship-manager-frontend" .Values.frontend.ingress.hostname }}.{{ .Values.global.dnsZone }}
      http:
        paths:
          - path: /
            backend:
              serviceName: ship-manager-frontend
              servicePort: http
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: frontend-config
data:
  config.js: |
    const config = (() => {
      return {
        'VUE_APP_BACKEND_BASE_URL': 'http://{{ default "ship-manager-backend" .Values.backend.ingress.hostname }}.{{ .Values.global.dnsZone }}',
        'VUE_APP_PROJECT_VERSION': '{{ .Values.global.imageTag }}'
      }
    })()

Perceba que estou buscando tudo de .Values, este é o arquivo values.yaml que vamos ver logo mais. Perceba também que a maioria das coisas que podem ser alteradas e que precisam ser alteradas, como o nome da imagem, a tag, o hostname e banco de dados, também são variáveis.

Nestes casos, utilizar configmaps e secrets ajuda muito a manter a pipeline simples.

O arquivo final será:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ship-manager-frontend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ship-manager-frontend
  template:
    metadata:
      labels:
        app: ship-manager-frontend
    spec:
      containers:
        - image: {{ required "Registry is required" .Values.global.registryName }}/{{ required "Image name is required" .Values.frontend.imageName }}:{{ required "Image tag is required" .Values.global.imageTag }}
          name: ship-manager-frontend
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 250m
              memory: 256Mi
          ports:
            - containerPort: 8080
              name: http
          volumeMounts:
            - name: config
              mountPath: /usr/src/app/dist/config.js
              subPath: config.js
      volumes:
        - name: config
          configMap:
            name: frontend-config
---
apiVersion: v1
kind: Service
metadata:
  name: ship-manager-frontend
spec:
  selector:
    app: ship-manager-frontend
  ports:
    - name: http
      port: 80
      targetPort: 8080
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ship-manager-frontend
  annotations:
    kubernetes.io/ingress.class: addon-http-application-routing
spec:
  rules:
    - host: {{ default "ship-manager-frontend" .Values.frontend.ingress.hostname }}.{{ .Values.global.dnsZone }}
      http:
        paths:
          - path: /
            backend:
              serviceName: ship-manager-frontend
              servicePort: http
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: frontend-config
data:
  config.js: |
    const config = (() => {
      return {
        'VUE_APP_BACKEND_BASE_URL': 'http://{{ default "ship-manager-backend" .Values.backend.ingress.hostname }}.{{ .Values.global.dnsZone }}',
        'VUE_APP_PROJECT_VERSION': '{{ .Values.global.imageTag }}'
      }
    })()

Vamos fazer o mesmo com o backend, criando um arquivo backend.yaml na pasta charts/backend/templates:

# backend.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ship-manager-backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ship-manager-backend
  template:
    metadata:
      labels:
        app: ship-manager-backend
    spec:
      containers:
        - image: {{ required "Registry is required" .Values.global.registryName }}/{{ required "Image name is required" .Values.imageName }}:{{ required "Image tag is required" .Values.global.imageTag }}
          name: ship-manager-backend
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 250m
              memory: 256Mi
          ports:
            - containerPort: 3000
              name: http
          env:
            - name: DATABASE_MONGODB_URI
              valueFrom:
                secretKeyRef:
                  key: database_mongodb_uri
                  name: backend-db
            - name: DATABASE_MONGODB_DBNAME
              value: {{ default "ship-manager" .Values.global.dbName }}
---
apiVersion: v1
kind: Service
metadata:
  name: ship-manager-backend
spec:
  selector:
    app: ship-manager-backend
  ports:
    - name: http
      port: 80
      targetPort: 3000
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ship-manager-backend
  annotations:
    kubernetes.io/ingress.class: addon-http-application-routing
spec:
  rules:
    - host: {{ default "ship-manager-backend" .Values.ingress.hostname }}.{{ .Values.global.dnsZone }}
      http:
        paths:
          - path: /
            backend:
              serviceName: ship-manager-backend
              servicePort: http
---
apiVersion: v1
kind: Secret
metadata:
  name: backend-db
type: Opaque
stringData:
  database_mongodb_uri: {{ required "DB Connection is required" .Values.global.dbConn | quote }}

Perceba que tenho algumas funções também, como required, default e quote, esta são funções nativas do Helm e quebram um bom galho quando a gente precisa de funcionalidades mais complexas.

O arquivo values

Assim como os charts, o arquivo values.yaml é baseado em uma hierarquia de escopos, ou seja, tome como exemplo a nossa estrutura:

# values.yaml
global:
  chave: # Acessível a todos os charts, tanto o frontend como o backend como `.Values.global.chave`

backend:
  chave: # Acessível somente ao frontend e ao backend, porém para o frontend será `.Values.backend.chave` e o backend usará como `.Values.chave`

frontend:
  chave: # Acessível pelo frontend como `.Values.frontend.chave`, mas não pelo backend

chave: # Acessível somente ao frontend como `.Values.chave`

Perceba que temos uma quebra de escopo dentro do arquivo values, as chaves que tem o mesmo nome dos seus charts dependentes serão acessadas somente por eles e pelos charts de mais alta ordem, então como o nosso frontend é o chart de maior ordem, ele tem acesso a todos os valores, enquanto o backend só tem acesso às chaves definidas sob backend:.

Perceba também que dentro de backend o escopo sofre um "levelling", ou seja, o escopo é removido de dentro de backend então você não precisa acessar o valor como .Values.backend.chave se você estiver dentro do chart backend, mas somente como .Values.chave.

É possível ter mais arquivos values dentro dos charts dependentes e a regra se mantém a mesma, a diferença é que o chart de maior ordem será alterado, porém este padrão torna a manutenção bastante complexa.

Nosso arquivo values precisa ter as mesmas chaves que definimos dentro dos nossos templates, então elas vão ser as seguintes:

global:
  registryName:
  imageTag:
  dbName: ship-manager
  dbConn:
  dnsZone:

backend:
  imageName: ship-manager-backend
  ingress:
    hostname:

frontend:
  imageName: ship-manager-frontend
  ingress:
    hostname:

As chaves que estou deixando em branco são as que serão ou preenchidas pelo CLI ou pelas funções default.

Criando a pipeline

Para criamos a pipeline, vamos utilizar o modo manual de criação, ou seja, vamos criar uma pasta .github e dentro dela uma pasta workflows. O primeiro workflow será o mais simples, o de produção.

Dentro da pasta workflows vamos criar um arquivo deploy-production.yml (pode ser qualquer nome, na verdade) e começar escrevendo o nome da nossa pipeline e quais são os gatilhos que vão fazer ela funcionar.

name: Build and push the tagged build to production

on:
  push:
    tags:
      - 'v*'

Aqui estamos dizendo que nossa action vai rodar em todos os pushes com uma tag v*, ou seja, v1.0.0 e até mesmo vabc, se você quiser reduzir as possibilidades pode usar regex como v[0-9]\.[0-9]\.[0-9].

Depois, vamos criar nosso primeiro job e definir uma variável em comum:

name: Build and push the tagged build to production

on:
  push:
    tags:
      - 'v*'

env:
  IMAGE_NAME: ship-manager

jobs:
  build_push_image:
    runs-on: ubuntu-20.04

Criamos um job chamado build_push_image que vai rodar no ubuntu 20, e uma variável compartilhada que será o nome base da imagem. Agora vamos para a ação de verdade, vamos começar a criar os passos do nosso job, começando com dois super importantes:

name: Build and push the tagged build to production

on:
  push:
    tags:
      - 'v*'

env:
  IMAGE_NAME: ship-manager

jobs:
  build_push_image:
    runs-on: ubuntu-20.04

    steps:
      - uses: actions/checkout@v2

      - name: Set env
        id: tags
        run: echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV

O primeiro passo é um checkout do nosso repositório, ele está praticamente presente em todas as actions e é sempre o primeiro passo. O segundo é a definição de uma segunda variável, o nome da tag.

Por padrão $GITHUB_REF é ou o nome do branch ou o nome da tag como /refs/heads/main ou /refs/tags/v1.0.0, temos que remover o /refs/* e ficar só com o final, por isso estamos usando uma substituição via shell para adicioná-la as variáveis globais, porém esta variável só funciona dentro deste job.

Não podemos definir a variável dentro de env porque esta chave não executa nenhum tipo de shell, portanto não podemos usar substituição de valores e nem expansões.

Agora vamos fazer o pipeline do Docker, ou seja, build e push das imagens do backend e frontend.

name: Build and push the tagged build to production

on:
  push:
    tags:
      - 'v*'

env:
  IMAGE_NAME: ship-manager

jobs:
  build_push_image:
    runs-on: ubuntu-20.04

    steps:
      - uses: actions/checkout@v2

      - name: Set env
        id: tags
        run: echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV

      - name: Set up Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to ACR
        uses: docker/login-action@v1
        with:
          # Username used to log in to a Docker registry. If not set then no login will occur
          username: ${{secrets.ACR_LOGIN }}
          # Password or personal access token used to log in to a Docker registry. If not set then no login will occur
          password: ${{secrets.ACR_PASSWORD }}
          # Server address of Docker registry. If not set then will default to Docker Hub
          registry: ${{ secrets.ACR_NAME }}

      - name: Build and push frontend image
        uses: docker/build-push-action@v2
        with:
          # Docker repository to tag the image with
          tags: ${{secrets.ACR_NAME}}/${{ env.IMAGE_NAME }}-frontend:latest,${{secrets.ACR_NAME}}/${{ env.IMAGE_NAME }}-frontend:${{env.tag}}
          labels: |
            image.revision=${{github.sha}}
            image.release=${{github.ref}}
          file: frontend/Dockerfile
          context: frontend
          push: true

      - name: Build and push backend image
        uses: docker/build-push-action@v2
        with:
          # Docker repository to tag the image with
          tags: ${{secrets.ACR_NAME}}/${{ env.IMAGE_NAME }}-backend:latest,${{secrets.ACR_NAME}}/${{ env.IMAGE_NAME }}-backend:${{env.tag}}
          labels: |
            image.revision=${{github.sha}}
            image.release=${{github.ref}}
          file: backend/Dockerfile
          context: backend
          push: true

O Docker tem 3 actions que são utilizadas, a primeira delas é o setup do buildx o utilitário de build de imagens do Docker, a segunda é o login em um registry, no caso será nosso ACR, e aqui temos o nosso primeiro secret que vamos criar dentro do nosso repositório, que serão os dados de login do ACR.

Por fim, estamos criando e dando um push na imagem e criando as tags latest e o nome da tag do GitHub, desta forma sabemos quais são as imagens "de produção" e quais serão as de teste, adicionamos também duas labels para cada imagem, uma delas tem a revisão, o sha do nosso commit, e a outra o nome da tag.

Com isso finalizamos nosso primeiro job, o segundo é a parte onde vamos fazer o deploy para o cluster. O início é o mesmo então vou omitir o conteúdo até aqui para focarmos só nessa parte:

# inicio do arquivo
jobs:
  build_push_image:
    # job de envio da imagem

  deploy:
      runs-on: ubuntu-20.04
      needs: build_push_image
  
      steps:
        - uses: actions/checkout@v2
  
        - name: Set env
          id: tags
          run: echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV
  
        - name: Install Helm
          uses: Azure/setup-helm@v1
          with:
            version: v3.3.1

Perceba que estamos criando uma relação entre os dois jobs com a chave needs, isso diz que o segundo job só será executado se o primeiro passar. Fazemos o checkout e copiamos a criação da variável, depois rodamos um step simples de instalação do Helm na máquina.

# inicio do arquivo
jobs:
  build_push_image:
    # job de envio da imagem

  deploy:
      runs-on: ubuntu-20.04
      needs: build_push_image
  
      steps:
        - uses: actions/checkout@v2
  
        - name: Set env
          id: tags
          run: echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV
  
        - name: Install Helm
          uses: Azure/setup-helm@v1
          with:
            version: v3.3.1

      - name: Get AKS Credentials
        uses: Azure/aks-set-context@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
          # Resource group name
          resource-group: ship-manager-pipeline
          # AKS cluster name
          cluster-name: ship-manager

      - name: Run Helm Deploy
        run: |
          helm upgrade \
            ship-manager-prd \
            ./kubernetes/ship-manager \
            --install \
            --create-namespace \
            --namespace production \
            --set global.registryName=${{ secrets.ACR_NAME }} \
            --set global.dbConn="${{ secrets.DB_CONNECTION }}" \
            --set global.dnsZone=${{ secrets.DNS_NAME }} \
            --set global.imageTag=${{env.tag}}

Por fim vamos ter o comando para obter as credenciais do Kubernetes e o deploy do Helm. Perceba que estamos fazendo o deploy para um namespace production e setando os valores do values.yaml através das flags --set, isso torna tudo mais fácil quando precisarmos remover os dados, pois só precisamos remover o namespace e tudo é deletado.

A pipeline de testes é quase igual, a diferença é que estamos setando mais variáveis e mudamos o namespace de publicação e também o trigger. O outro arquivo, que chamei de deploy-test ficou assim:

# deploy-test.yml
name: Build and push the tagged build to test

on:
  push:
    branches-ignore:
      - 'main'
      - 'master'

env:
  IMAGE_NAME: ship-manager

jobs:
  build_push_image:
    runs-on: ubuntu-20.04

    steps:
      - uses: actions/checkout@v2

      - name: Set env
        id: tags
        run: echo tag=${GITHUB_REF#refs/heads/} >> $GITHUB_ENV

      - name: Set up Buildx
        uses: docker/setup-buildx-action@v1

      - name: Login to ACR
        uses: docker/login-action@v1
        with:
          # Username used to log in to a Docker registry. If not set then no login will occur
          username: ${{secrets.ACR_LOGIN }}
          # Password or personal access token used to log in to a Docker registry. If not set then no login will occur
          password: ${{secrets.ACR_PASSWORD }}
          # Server address of Docker registry. If not set then will default to Docker Hub
          registry: ${{ secrets.ACR_NAME }}

      - name: Build and push frontend image
        uses: docker/build-push-action@v2
        with:
          # Docker repository to tag the image with
          tags: ${{ secrets.ACR_NAME }}/${{ env.IMAGE_NAME }}-frontend:${{env.tag}}
          labels: |
            image.revision=${{github.sha}}
          file: frontend/Dockerfile
          context: frontend
          push: true

      - name: Build and push backend image
        uses: docker/build-push-action@v2
        with:
          # Docker repository to tag the image with
          tags: ${{ secrets.ACR_NAME }}/${{ env.IMAGE_NAME }}-backend:${{env.tag}}
          labels: |
            image.revision=${{github.sha}}
          file: backend/Dockerfile
          context: backend
          push: true

  deploy:
    runs-on: ubuntu-20.04
    needs: build_push_image

    steps:
      - uses: actions/checkout@v2

      - name: Set env
        id: tags
        run: echo tag=${GITHUB_REF#refs/tags/} >> $GITHUB_ENV

      - name: Install Helm
        uses: Azure/setup-helm@v1
        with:
          version: v3.3.1

      - name: Get AKS Credentials
        uses: Azure/aks-set-context@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
          # Resource group name
          resource-group: ship-manager-pipeline
          # AKS cluster name
          cluster-name: ship-manager

      - name: Run Helm Deploy
        run: |
          helm upgrade \
            ship-manager-${{env.tag}} \
            ./kubernetes/ship-manager \
            --install \
            --create-namespace \
            --namespace test-${{env.tag}} \
            --set global.registryName=${{ secrets.ACR_NAME }} \
            --set global.dbConn="${{ secrets.DB_CONNECTION }}" \
            --set global.dbName=ship-manager-test-${{env.tag}} \
            --set global.dnsZone=${{ secrets.DNS_NAME }} \
            --set backend.ingress.hostname=ship-manager-backend-${{env.tag}} \
            --set frontend.ingress.hostname=ship-manager-frontend-${{env.tag}} \
            --set global.imageTag=${{env.tag}}

Secrets

Agora que já temos as pipelines criadas, vamos ao GitHub criar nossos secrets! Abra o repositório no seu browser, navegue até a aba Settings e depois secrets, então clique em New repository secret para criar um novo secret local.

Vamos criar um secret chamado ACR_LOGIN que será o nome do nosso ACR, ou seja, shipmanager. Outro chamado ACR_NAME, que não é bem um segredo, porque é o DNS do nosso CR, porém assim evitamos de ter um valor fixo na nossa action, este valor é o shipmanager.azurecr.io.

Ambas as informações podem ser obtidas no portal da Azure, sabendo o nome do ACR o login é o mesmo e o DNS é sempre <nome>.azurecr.io

A senha do ACR pode ser obtida com um comando do AZ CLI: az acr credential show -n shipmanager --query "passwords[0].value" -o tsv e deve ser colocada em outro secret chamado ACR_PASSWORD.

Para obtermos a chave do nosso AKS, vamos precisar de um acesso via service principal na Azure, que pode ser obtido pelo comando az ad sp create-for-rbac --sdk-auth, este comando vai te devolver um JSON, copie todo o JSON e cole no secret chamado AZURE_CREDENTIALS.

A conexão com o banco do secret chamado DB_CONNECTION pode ser obtida também pelo comando az cosmosdb keys list -n ship-manager-db -g ship-manager-pipeline --type connection-strings --query "connectionStrings[0].connectionString".

E o secret final pode ser obtido através de uma query na lista de addons habilitados do AKS, como ligamos o HTTP Application Routing, vamos ter uma zona de DNS liberada que podemos obter com o comando az aks show -n ship-manager -g ship-manager-pipeline --query "addonProfiles.httpApplicationRouting.config.HTTPApplicationRoutingZoneName e colocar no secret chamado DNS_NAME.

Testando

Commitamos nossas mudanças e agora vamos criar uma tag com git tag -a v<versão> -m'nova versão e depois git push --tags para podermos fazer a trigger na nosso build. Teremos um pequeno delay e então uma saída como esta:

Se visualizarmos a nossa aplicação depois de alguns minutos (o DNS demora para propagar), iremos ver que temos um endereço igual ao do nosso ingress (podemos obter o endereço do frontend com kubectl get ing -n production), ao acessar, vamos ter a nossa aplicação rodando:

Nossa aplicação está em um branch de produção e será acessível e atualizada para a versão mais nova sempre que fizermos um push com uma tag específica. O mesmo vai acontecer quando criarmos um novo branch e fizermos um push, experimente criar um branch qualquer e enviar um código!

Conclusão e melhorias

A criação de um ambiente dinâmico não é simples, porém pode ser a solução entre um time que demora a testar suas funcionalidades para um time que pode ser muito mais eficiente. Neste exemplo chegamos aos 50% do que é necessário, a outra parte importante do pipeline é também remover seus recursos sempre que não estão mais utilizados.

Por este motivo, utilizar outro banco de dados ao invés da mesma instância é muito mais preferível, o ideal seria criarmos uma dependência no chart backend para um chart do MongoDB completamente vazio, desta forma podemos ter certeza que este ambiente está totalmente isolado e podemos remover todo o ambiente sem nenhum problema.

Deixem seus comentários e quem sabe podemos continuar essa série!

]]>
<![CDATA[ O guia completo do gRPC parte 3: Tipos em tudo com TypeScript! ]]> https://blog.lsantos.dev/o-guia-do-grpc-3/ 60a6c9ea5868507c4d4f3a6a Qua, 26 Mai 2021 12:10:32 -0300

Este artigo é parte de uma série

  1. O guia completo do gRPC parte 1: O que é gRPC?
  2. O guia completo do gRPC parte 2: Mãos à obra com JavaScript
  3. O guia completo do gRPC parte 3: Tipos em tudo com TypeScript!
  4. O guia completo do gRPC parte 4: Streams

No nosso artigo anterior do guia vimos como podemos integrar o gRPC com JavaScript de uma forma bem simples e rápida. Agora chegou a hora de subirmos mais um nível e adicionarmos os tipos à esta aplicação! E, quando eu falo de tipos automaticamente pensamos em TypeScript!

Para este artigo vamos converter a nossa API gRPC de notas para utilizar TypeScript. Mas primeiro, vamos entender o que significa "converter para TypeScript" e o que queremos dizer quando fazemos isso usando gRPC, ainda mais quando usamos gRPC com JavaScript.

O que é converter?

Como já mencionei na primeira parte deste guia, o gRPC, apesar de ser uma tecnologia estabelecida, não tem uma documentação e nem um conjunto de ferramentas muito boa para algumas linguagens, infelizmente uma dessas linguagens é o JavaScript...

No entanto, o ferramental que temos para JS funciona muito bem, apesar de ser um pouco complicado e meio obscuro de ser usado. Com uma base mais sólida basta que adicionemos as declarações de tipos para aqueles arquivos que já foram gerados pelo compilador.

Com isso, "converter" uma aplicação para TypeScript, em suma, significa que temos que adicionar arquivos .d.ts a todos os arquivos .js que são gerados pelo compilador. A tarefa em si não é muito complicada, uma vez que temos bibliotecas oficiais que fazem esse tipo de coisa. Então está tudo certo, não é?

Infelizmente o problema que temos volta-se novamente às ferramentas que temos para JavaScript. Por exemplo, temos dois pacotes oficiais, um mais antigo, chamado apenas de grpc que foi o primeiro pacote de implementação gRPC para JavaScript, mas este pacote está aos poucos sendo substituído pelo pacote @grpc/grpc-js, que não possui o loader de arquivos .proto, deixando o pacote mais leve e com menos dependências, já que ele terceiriza essa funcionalidade para outro pacote específico chamado @grpc/proto-loader.

Usamos ambos os pacotes no artigo anterior dessa série

As diferenças entre estes pacotes vão além de serem somente um uma variação do outro, ou então terem menos coisas juntas. A realidade é que o pacote grpc é mais bem implementado porque está conosco há muito mais tempo, enquanto, por exemplo, o pacote @grpc/grpc-js não possui algumas implementações, como a grpc.Server.bind().

Trabalhando com as diferenças

Já sabemos que estes pacotes possuem diferenças, mas é possível trabalhar com elas? Sim, é totalmente possível mas temos que tomar cuidado com alguns pormenores que não estão muito bem documentados e são, em grande parte, o motivo pelo qual estou montando este guia. Nosso ecossistema de JavaScript para gRPC é extremamente disperso e esparso em diversos níveis, o que torna a implementação na linguagem muito complicada para quem está começando.

Como tivemos um pacote, o grpc, na maioria do tempo que o gRPC existiu, começamos a ter bibliotecas que foram sendo criadas para ele, como o Protobuf.js, uma ferramenta oficial que compreende basicamente a criação de arquivos para englobar as classes de geração de mensagens no gRPC, ou seja, podemos gerar classes para as nossas mensagens e utilizá-las dessa forma:

const pbjs = require('protobuf.js')
pbjs.load('notes.proto', (err, root) => {
  if (err) throw err

  const NoteFindRequest = root.lookupType('NoteFindRequest')
  const payload = { id: 1 }

  const isValid = NoteFindRequest.verify(payload)
  if (!isValid) throw new Error(isValid)

  const message = NoteFindRequest.create(payload)
  const buffer = NoteFindRequest.encode(message).finish()
})

Como você pode perceber, temos um modelo de reflexão, ou seja, temos que fazer um lookup dentro do nosso arquivo .proto, mas é possível também gerar arquivos de forma estática, assim teremos classes prontas para isso. Adicionalmente, o protobuf.js inclui também o conceito de verify, de forma que podemos verificar as mensagens para saber se elas estão corretas, seguindo um fluxo como este:

E, além de tudo isso, ela também gera definições para TypeScript, já que o grpc e o @grpc/grpc-js possuem tipagem estática junto com seus pacotes. Assim podemos realizar basicamente as mesmas funcionalidades porém com nossos tipos definidos.

Conseguiu perceber o problema que temos aqui? Apesar de muito boa, essa biblioteca não possui uma forma boa e simples de gerarmos um código TypeScript que seja auto-contido, ou seja, que possamos instanciar como uma instância de um serviço sem precisar confiar no carregamento dinâmico de um arquivo .proto, ou seja, temos que usar .load no nosso arquivo de definição e confiar que ele estará completo e válido e, em um superset estáticamente tipado como o TypeScript, qualquer coisa dinâmica é um problema.

Então, apesar de o protobuf.js gerar tipos válidos, ele é muito melhor para quando estamos fazendo um client do que quando estamos realizando a criação de um server, já que ele provê muito mais ferramental para mensagens do que para inferência dos tipos internos do servidor. Mas ele também pode ser utilizado como gerador de tipos em tempo de execução com decorators.

O que queremos

Vamos pensar um pouco, temos um arquivo de definição completo que é o seguinte:

syntax = "proto3";

service Notes {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entities
message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

message Void {}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

// Responses
message NoteFindResponse {
  Note note = 1;
}

message NoteListResponse {
  repeated Note notes = 1;
}

Com isso nós já sabemos basicamente tudo que precisamos para poder criar nosso servidor. Um servidor gRPC possui duas principais interfaces, a primeira é a interface base que contém todos os métodos que podem ser criados e a segunda é a implementação do servidor. Então o ideal é que pudéssemos fazer algo assim:

class NotesServer implements NotesServerInterface {
  find (call: ServerUnaryCall<NoteFindRequest, NoteFindResponse>, callback: sendUnaryData<NoteFindResponse>) { /* implementação */ }

  list (_: ServerUnaryCall<Void, NoteListResponse>, callback: sendUnaryData<NoteListResponse>) { /* implementação */ }
}

const server = new grpc.Server()
server.addService(NotesService, new NotesServer())

Todos os tipos que estamos usando na chamada anterior, como o ServerUnaryCall e o sendUnaryData são provenientes do @grpc/grpc-js nativamente, já que a lib é escrita em TypeScript, veja mais dos tipos no repositório oficial.

A parte importante é, além dos tipos da classe, os tipos que vamos passar para o servidor, então nossa chamada server.addService tem uma assinatura como esta:

abstract class Server {
  function addService (service: ServiceDefinition, implementation: UntypedServiceImplementation): void
}

E os tipos ServiceDefinition e UntypedServiceImplementation possuem, respectivamente, as seguintes definições:

type ServiceDefinition<ImplementationType = UntypedServiceImplementation> = {
    readonly [index in keyof ImplementationType]: MethodDefinition<any, any>
}

Veja que ela não é complexa, o que temos aqui é um generic, uma definição de serviço leva um parâmetro de tipo, quais são os métodos que são implementados, se não mandarmos nada para ele, vamos ter uma UntypedServiceImplementation, que significa "não faço a menor ideia do que tem aqui":

export declare type UntypedHandleCall = HandleCall<any, any>
export interface UntypedServiceImplementation {
    [name: string]: UntypedHandleCall
}

Ambos os tipos são index signatures que definem apenas um objeto com uma chave do formato string.

Resumindo isso tudo, o que temos é um objeto que precisa possuir as mesmas chaves da sua implementação, de forma que NotesService precisa ter as mesmas chaves de NotesServer e os dois tipos que essas chaves se referem precisam ser implementações de métodos, ou seja, funções com a seguinte assinatura:

export type MethodImplementation = (call: ServerUnaryCall<RequestInput, RequestOutput>, callback: sendUnaryData<RequestOutput>): void

Poderíamos até deixar mais bonito fazendo tudo com generics:

export type MethodImplementation<In, Out> = (call: ServerUnaryCall<In, Out>, callback: sendUnaryData<Out>): void

E tudo isso é só para mostrar para você que os tipos que precisamos para o gRPC não são absolutamente nada de mais, apenas objetos que precisam ter as mesmas chaves que outros objetos e precisam implementar funções com uma assinatura específica.

Streams e outros tipos de dados

Uma pequena extensão do que estamos falando, mas sem relação direta com o conteúdo deste guia, se nosso tipo de chamada não for uma chamada Unary, ou seja, se tivermos streaming de dados de um dos lados como, por exemplo, neste .proto:

service Notes {
  rpc List (Void) returns (stream NoteListResponse);
  rpc Find (NoteFindRequest) returns (stream NoteFindResponse);
}

Então nossas respostas não seriam mais ServerUnaryCall e nem sendUnaryData porque não estamos mais mandando um único tipo como retorno, mas sim estamos abrindo uma stream de dados, nossa call seria uma ReadableStream e nosso callback seria uma WritableStream - ou essencialmente qualquer coisa que implemente Readable e Writable respectivamente.

Vamos aprender mais sobre streaming com gRPC nas próximas partes deste guia.

Mãos a obra

Agora que já entendemos os pormenores dos tipos do TS e como eles, na verdade, são apenas índices de um grande objeto, vamos partir para nossa implementação.

Para convertermos nossa API para TypeScript, vamos utilizar uma outra biblioteca mais especializada no grpc-js, já que o protobuf.js não nos atende para essa tarefa. O repositório desse exemplo está disponível no meu GitHub se você quiser dar uma olhada no código final e na estrutura geral.

A primeira coisa que vamos fazer é uma pequena alteração no nosso arquivo .proto. As alterações são mínimas, vamos apenas mudar o nome da nossa implementação rpc para Notes

syntax = "proto3";

service Notes {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entities
message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

message Void {}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

// Responses
message NoteFindResponse {
  Note note = 1;
}

message NoteListResponse {
  repeated Note notes = 1;
}

Isso é só porque se chamarmos de NoteService, teremos um nome estranho no nosso tipo como NoteServiceServer (sim, eu tenho TOC com nomes de variáveis).

Vamos criar uma nova pasta para nosso projeto, dentro dela vamos criar uma pasta proto e vamos colocar o arquivo notes.proto com o conteúdo que acabamos de ver. Em seguida, damos um npm init -y para criar um novo projeto Node.js e vamos instalar os seguintes pacotes:

$ npm i -D @types/long @types/node grpc_tools_node_protoc_ts grpc-tools typescript

Como você pode perceber, vamos utilizar a biblioteca grpc-tools que é, basicamente, um wrapper do protoc com algumas modificações interessantes, incluindo a geração de arquivos .d.ts. E vamos usar um plugin para esta biblioteca que é o grpc_tools_node_protoc_ts, que faz o trabalho de gerar os tipos de forma mais estruturada para criarmos nossos serviços somente a partir de uma única interface.

Estas são nossas dependências de desenvolvimento, para as dependências de execução só vamos ter uma, o grpc-js, então vamos executar npm i @grpc/grpc-js. Isto porque estamos gerando arquivos estáticos que não vão precisar ser lidos a partir de um arquivo .proto, então não precisamos de mais nada além do servidor.

Se você quer gerar os arquivos de forma dinâmica, então você vai precisar do loader do protobuf e, infelizmente, os tipos gerados podem não ser os ideais para você

Vamos agora iniciar nosso projeto TypeScript com npx tsc --init, ao criar, vamos modificar as variáveis do tsconfig.json para que ele seja o mais restrito possível:

//tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Agora vamos editar nosso arquivo package.json para criarmos o nosso script de compilação. Vamos criar um script chamado compile que será basicamente a compilação do nosso arquivo .proto:

grpc_tools_node_protoc \
    --js_out=import_style=commonjs,binary:./proto \
    --grpc_out=grpc_js:./proto \
    -I ./proto ./proto/*.proto && \
  grpc_tools_node_protoc \
    --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts \
    --ts_out=grpc_js:./proto \
    -I ./proto ./proto/*.proto

Este script vai primeiramente gerar os arquivos estáticos em JavaScript, tanto os arquivos de tipo das mensagens (descritos por js_out) quanto o arquivo que contém o servidor pré-pronto (descrito por grpc_out), isso tudo dentro da nossa pasta ./proto usando os arquivos que terminam em *.proto.

Logo em seguida, vamos ler os arquivos .js gerados nessa pasta e vamos ativar o plugin protoc-gen-ts para poder gerar seus tipos e salvá-los na mesma pasta.

Importante: Veja que temos uma chave ts_out=grpc_js na segunda parte do comando, isso porque esta biblioteca está preparada para gerar tipos tanto para o grpc quanto para o @grpc/grpc-js

Os últimos dois scripts que vamos criar são os scripts para iniciar o servidor e o client, compilando de TS para JS antes. No final, vamos ter uma chave scripts assim no nosso package.json:

//package.json
{
  // ... conteúdo omitido
  "scripts": {
    "compile": "grpc_tools_node_protoc --js_out=import_style=commonjs,binary:./proto --grpc_out=grpc_js:./proto -I ./proto ./proto/*.proto && grpc_tools_node_protoc --plugin=protoc-gen-ts=./node_modules/.bin/protoc-gen-ts --ts_out=grpc_js:./proto -I ./proto ./proto/*.proto",
    "start:server": "tsc && node dist/server.js",
    "client": "tsc && node dist/client.js"
  },
  // ... conteúdo omitido
}

Execute o script npm run compile e veja que vamos ter 4 arquivos novos na pasta proto:

proto
├── notes.proto # arquivo protobuf de definição
├── notes_grpc_pb.d.ts # tipos do servidor gRPC
├── notes_grpc_pb.js # servidor gRPC
├── notes_pb.d.ts # tipos das mensagens
└── notes_pb.js # mensagens da definição

Onde tudo se junta

Se você clicar no arquivo notes_grpc_pb.js verá, no final, que ele possui uma variável chamada NotesService:

var NotesService = exports.NotesService = {
  list: {
    path: '/Notes/List',
    requestStream: false,
    responseStream: false,
    requestType: notes_pb.Void,
    responseType: notes_pb.NoteListResponse,
    requestSerialize: serialize_Void,
    requestDeserialize: deserialize_Void,
    responseSerialize: serialize_NoteListResponse,
    responseDeserialize: deserialize_NoteListResponse,
  },
  find: {
    path: '/Notes/Find',
    requestStream: false,
    responseStream: false,
    requestType: notes_pb.NoteFindRequest,
    responseType: notes_pb.NoteFindResponse,
    requestSerialize: serialize_NoteFindRequest,
    requestDeserialize: deserialize_NoteFindRequest,
    responseSerialize: serialize_NoteFindResponse,
    responseDeserialize: deserialize_NoteFindResponse,
  },
};

Compare essa variável com o tipo ServiceDefinition que temos no grpc-js. Eles são os mesmos, ou seja, podemos criar um servidor diretamente a partir desse construtor. Logo abaixo temos uma chamada ao grpc.makeGenericClientConstructor que é um método que gera um client genérico de chamadas para o gRPC que podemos usar sem precisar de uma instancia do nosso arquivo .proto, vamos falar mais sobre ele nas próximas edições.

Criando o servidor

Vamos passar agora para a parte interessante, onde criamos nosso servidor. Crie uma pasta src e um arquivo server.ts dentro desta pasta.

Vamos começar criando nosso "banco de dados", lembre-se que ele é um array de notas, mas notas não são mais simples objetos, agora temos tipos que definem não só as notas como objetos mas também as notas como uma classe de mensagem, por isso o tipo que precisamos dar para esse banco é Notes.AsObject[], ou seja, um array de objetos que são mensagens válidas de Notes:

import { Note } from '../proto/notes_pb'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

Agora vamos implementar nosso servidor. Existem duas formas de fazermos isso, a primeira é a implementação via objeto, como já fizemos antes, com um objeto do tipo:

const server = {
  find (call, cb) { ... },
  list (call, cb) { ... }
}

E a segunda é a implementação no modelo de classe que, no final da compilação acaba virando um objeto, mas é mais bonito e mais fácil de entender quando estamos lendo o código, então será esse que vamos usar. Vamos começar com uma classe NotesServer que implementa uma interface INotesServer gerada pelo nosso compilador de .proto:

import { Note } from '../proto/notes_pb'
import { INotesServer } from '../proto/notes_grpc_pb'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {

}

Dica: Se você estiver usando o VSCode, ao criar uma nova classe que implementa uma interface, um pequeno bulbo de luz azul vai aparecer ao lado, selecione-o e haverá uma opção para implementar todos os objetos automaticamente:

Bulbo de luz azul mostrando a opção automática de implementação da interface no VSCode

Ao clicar, você terá as interfaces implementadas com os tipos necessários, basta substituir pela implementação real:

Classe pré implementada com métodos genéricos

Veja que temos, além dos nossos métodos, um index signature do TypeScript no início. Este é um dos problemas que mencionei quando disse que as bibliotecas não são preparadas para trabalhar com o TypeScript inicialmente. Este bug está documentado na biblioteca e, infelizmente, é uma limitação do TypeScript para tipos não conhecidos de índices, como o tipo diz que podemos ter N strings como chaves e seus valores, precisamos deixar claro que esta classe implementa e permite este tipo de coisa também.

Na implementação usando somente a lib grpc isto não ocorre porque os tipos não exigem uma assinatura de índice dinâmico como esta

Vamos implementar nossas funções, começando com a função list que é a mais simples, vamos adicionar uma assinatura como o tipo manda:

import { sendUnaryData, ServerUnaryCall, UntypedHandleCall } from '@grpc/grpc-js'
import { Note } from '../proto/notes_pb'
import { INotesServer } from '../proto/notes_grpc_pb'
import { Note, NoteListResponse, Void } from '../proto/notes_pb'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {

  list (_: ServerUnaryCall<Void, NoteListResponse>, callback: sendUnaryData<NoteListResponse>): void {
    const response = new NoteListResponse()
    notes.forEach((note) => {
      response.addNotes(
        (new Note).setId(note.id)
                  .setTitle(note.title)
                  .setDescription(note.description)
        )
    })
    callback(null, response)
  }

  [name: string]: UntypedHandleCall

}

Perceba que agora, ao invés de só retornarmos o Array, temos obrigatoriamente que converter cada item do array em um Note e adicionar ao tipo da resposta. Ao mesmo tempo que isto é muito bom, pois temos a segurança de tipos, é pior porque temos que escrever mais e não há forma de converter todo o array de uma única vez de forma nativa do compilador (você pode, no entanto, escrever uma função para fazer exatamente isso).

Falando de imports

Outro detalhe importante para notar, aqui recebemos o tipo Void que é um tipo criado por nós dentro do nosso arquivo .proto, se quisermos uma implementação mais "correta" podemos usar o well-known type chamado Empty, que vem juntamente com a biblioteca padrão do protobuf quando você faz o download do repositório oficial do protoc, você precisa mover esta pasta para /usr/includes ou qualquer outra pasta que esteja no seu PATH.

Então podemos importar este tipo dentro do nosso arquivo .proto:

syntax = "proto3";
package notes;

import "google/protobuf/empty.proto";

service Notes {
  rpc List (google.protobuf.Empty) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entities
message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

message Void {}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

// Responses
message NoteFindResponse {
  Note note = 1;
}

message NoteListResponse {
  repeated Note notes = 1;
}

Precisamos adicionar uma declaração package para dizer que nosso arquivo está em outro namespace, isto é obrigatório para não gerar confusões no compilador.

Depois, no nosso servidor, instalaríamos o pacote google-protobuf com npm i google-protobuf e importamos o tipo Empty naturalmente:

import { sendUnaryData, ServerUnaryCall, UntypedHandleCall } from '@grpc/grpc-js'
import { Note } from '../proto/notes_pb'
import { INotesServer } from '../proto/notes_grpc_pb'
import { Note, NoteListResponse } from '../proto/notes_pb'
import { Empty } from 'google-protobuf/empty_pb'

const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {

  list (_: ServerUnaryCall<Empty, NoteListResponse>, callback: sendUnaryData<NoteListResponse>): void {
    const response = new NoteListResponse()
    notes.forEach((note) => {
      response.addNotes(
        (new Note).setId(note.id)
                  .setTitle(note.title)
                  .setDescription(note.description)
      )
    })
    callback(null, response)
  }

  [name: string]: UntypedHandleCall

}

Este foi um pequeno contorno do artigo para mostrar que é possível deixar as coisas mais separadas e utilizar libs de tipos externas no nosso servidor. Embora não estaremos usando essa lib aqui, é um conteúdo interessante para se manter em mente.

Voltando aos trilhos

Vamos implementar o método find, ele segue exatamente a mesma ideia, vamos apenas adicionar a implementação básica do tipo e retornar um objeto Note:

import { sendUnaryData, ServerUnaryCall, UntypedHandleCall } from '@grpc/grpc-js'
import { Note } from '../proto/notes_pb'
import { INotesServer } from '../proto/notes_grpc_pb'
import { Note, NoteFindRequest, NoteFindResponse, NoteListResponse, Void } from '../proto/notes_pb'


const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {

  list (_: ServerUnaryCall<Void, NoteListResponse>, callback: sendUnaryData<NoteListResponse>): void {
    const response = new NoteListResponse()
    notes.forEach((note) => {
      response.addNotes(
        (new Note).setId(note.id)
                  .setTitle(note.title)
                  .setDescription(note.description)
      )
    })
    callback(null, response)
  }

  find (call: ServerUnaryCall<NoteFindRequest, NoteFindResponse>, callback: sendUnaryData<NoteFindResponse>) {
    const id = call.request.getId()
    const foundNote = notes.find((note) => note.id === id)
    if (!foundNote) return callback(new Error('Note not found'), null)

    const response = new NoteFindResponse()
    response.setNote(
       (new Note()).setTitle(foundNote.title)
                   .setId(foundNote.id)
                   .setDescription(foundNote.description)
    )
    return callback(null, response)
  }

  [name: string]: UntypedHandleCall

}

Apesar de ser mais verboso, o método fica muito mais fácil de ler e entender, além de manter a coesão e os tipos obrigatórios.

Para finalizar, vamos iniciar nosso servidor, temos duas formas de fazer isso, a primeira delas é via callback:

import { INotesServer, NotesService } from '../proto/notes_grpc_pb'
import { Note, NoteFindRequest, NoteFindResponse, NoteListResponse, Void } from '../proto/notes_pb'
import { sendUnaryData, Server, ServerCredentials, ServerUnaryCall, UntypedHandleCall } from '@grpc/grpc-js'


const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {
  /* Nossa implementação */
}

const server = new Server()
server.addService(NotesService, new NotesServer())

server.bindAsync('0.0.0.0:50052', ServerCredentials.createInsecure(), (err, port) => {
  if (err) throw err
  console.log(`listening on ${port}`)
  server.start()
})

Isso é um problema proveniente do TypeScript, uma vez que é possível utilizar o bindAsync como uma Promise no JavaScript. Isto acontece porque o tipo do servidor não possui uma definição para uma promise. Mas, sempre podemos dar um jeito e usar o promisify:

import { promisify } from 'util'
import { INotesServer, NotesService } from '../proto/notes_grpc_pb'
import { Note, NoteFindRequest, NoteFindResponse, NoteListResponse, Void } from '../proto/notes_pb'
import { sendUnaryData, Server, ServerCredentials, ServerUnaryCall, UntypedHandleCall } from '@grpc/grpc-js'


const notes: Note.AsObject[] = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

class NotesServer implements INotesServer {
  /* Nossa implementação */
}

const server = new Server()
server.addService(NotesService, new NotesServer())

const bindPromise = promisify(server.bindAsync).bind(server)

bindPromise('0.0.0.0:50052', ServerCredentials.createInsecure())
  .then((port) => {
    console.log(`listening on ${port}`)
    server.start()
  })
  .catch(console.error)

Muito melhor, não? Note que precisamos utilizar o .bind(server) porque o método start() possui uma checagem para saber se o servidor já foi inicializado chamando this.started.

Agora, se rodarmos npm run start:server teremos nosso servidor rodando na porta 50052. Falta criarmos o client, mas perceba que o fluxo de trabalho e entendimento do código ficou muito melhor do que antes.

Criando o client

A criação do client é quase que completamente automática porque temos o nosso genericClientConstructor já chamado com o tipo de servidor que precisamos em NotesService, crie um novo arquivo em src chamado client.ts e ele dispensa muitas explicações:

import { ChannelCredentials } from '@grpc/grpc-js'
import { NotesClient } from '../proto/notes_grpc_pb'
import { NoteFindRequest, Void } from '../proto/notes_pb'

const client = new NotesClient('0.0.0.0:50052', ChannelCredentials.createInsecure())
client.list(new Void(), (err, notes) => {
  if (err) return console.log(err)
  console.log(notes.toObject())
})

client.find((new NoteFindRequest).setId(1), (err, note) => {
  if (err) return console.log(err)
  console.log(note.toObject())
})

client.find((new NoteFindRequest).setId(3), (err, note) => {
  if (err) return console.log(err.message)
  console.log(note.toObject())
})

O client já está praticamente instanciado com todos os métodos, mas lembre-se de que todos os tipos de retorno e de envio precisam ser um tipo válido do gRPC aos olhos do TypeScript, por isso estamos criando uma nova classe vazia com new Void() o que parece ser meio contraprodutivo, mas ajuda muito a manter a coesão de tudo que fazemos.

Rode npm run client e veja a mágica acontecer!

Conclusão

Como pudermos ver, temos uma grande facilidade em transformar tudo em TypeScript, porém precisamos primeiro entender o que queremos e entender as limitações das ferramentas, o que pode ser provar um grande problema já que elas acabam não se conversando muito.

Apesar de gRPC com TypeScript ser incrível, ainda temos alguns passos a percorrer para transformar essa modalidade em uma forma mais útil para que todos possamos utilizá-lo. Estou tentando fazer isso com algumas libs como o protots para gerar interfaces e não apenas tipos, dessa forma podemos abstrair um pouco mais das funcionalidades de forma que não precisemos da uma implementação completa do gRPC para ter os tipos funcionais.

Nas próximas partes da série, vamos abordar muitas coisas, como buf e streams, então curta e compartilhe este post com seus amigos e amigas para que possamos espalhar a palavra do gRPC e mostrar que não é tão difícil assim fazer uma API que possa ser utilizada de forma unificada por todos!

]]>
<![CDATA[ Veja o que há de novo no Node.js 16 ]]> https://blog.lsantos.dev/veja-o-que-ha-de-novo-no-node-js-16/ 60a52072ae44673926130402 Qua, 19 Mai 2021 14:46:04 -0300 Em abril de 2021 foi anunciado o lançamento da versão 16 do Node.js, como é de costume, as versões pares do runtime são as consideradas production ready ou seja, as versões que serão definitivas para produção.

Inicialmente, a versão LTS (Long Term Support), está sendo a versão 14 até outubro de 2021 enquanto a versão 16 segue como a versão current. Após outubro, a versão 14 entrará em estado de manutenção e a versão 16 será promovida a LTS, isso significa que a versão 14 receberá somente atualizações de segurança e manutenção, enquanto a versão 16 estará recebendo suporte ativo. Tudo isso pode ser visto no calendário de releases oficial.

Calendário de releases do Node.js mostrando a versão 16 como current

Esse controle de versões é importante, pois como podemos ver no diagrama, a versão 10 perdeu totalmente o suporte em maio, esta era a última versão que não suportava ES Modules nativamente, o que significa que agora todo mundo que mantém um pacote ou uma lib no NPM poderá utilizar por padrão a nova estrutura!

Vamos as principais diferenças desta nova versão

V8 foi atualizado para versão 9.0

O engine JavaScript mais conhecido do mundo foi atualizado para a versão 9.0 nessa release do Node.js, embora essa não seja a versão mais recente, ela já tem um incrível suporte a muitas coisas legais.

Na versão 9.1 do V8, teremos suporte a top-level await, o que vai tornar a nossa vida muito mais simples

Além das naturais melhorias de performance e estabilidade, esta versão tem uma modificação especial nas expressões regulares, que agora trazem uma nova chave para o resultado de exec. Anteriormente, não tínhamos como saber quais eram os inícios e finais de uma string que foi comparada com RegExp, ou seja, não tínhamos como saber em qual índice da string este valor apareceu, agora, através da chave indices podemos saber exatamente o início e o final de uma string que foi rodada contra uma RegExp que tenha a flag /d setada:

const str = /(Java)(Script)/d.exec('JavaScript')

str.indices // [ [0,10], [0,4], [4,10] ]
str.indices[0] // [0,10] -> toda a string
str.indices[1] // [0,4] -> primeiro grupo ("Java")
str.indices[2] // [4,10] -> segundo grupo ("Script")

Biblioteca timers/promises estável

Sempre que precisamos usar uma função do tipo setTimeout, setInterval ou qualquer outra função que dependa de um timer, geralmente o que fazemos é uma de duas coisas:

  • Trabalhar com um modelo convertido manualmente para promises
function asyncTimeout (ms) {
    return new Promise((resolve) => setTimeout(resolve, ms))
}

;(async () => {
  await asyncTimeout(3000)
  console.log('Hello')
})()
  • Usar o util.promisify
const { promisify } = require('util')
const asyncTimeout = promisify(setTimeout)

;(async () => {
  await asyncTimeout(3000)
  console.log('Hello')
})()

Agora temos uma API nativa de timers com promises que estava em beta na versão 15 do Node:

import { setTimeout } from 'timers/promises';
async function run() {
  await setTimeout(5000);
  console.log('Hello, World!');
}
run();

Conclusão

Temos algumas alterações muito legais para o que vem por ai no Node.js! Esperamos que, no futuro, tenhamos ainda mais mudanças e muitas outras novidades!

]]>
<![CDATA[ Não só de Docker vivem os containers ]]> https://blog.lsantos.dev/nao-so-de-docker-vivem-os-containers/ 60a6c23b5868507c4d4f3a25 Qui, 13 Mai 2021 17:18:00 -0300 Tive o prazer de ser convidado para uma live com o Fabrício Veronez sobre como podemos entender melhor o ambiente de containers muito além do Docker, com exemplos práticos e também código funcional!

Conversamos por quase duas horas sobre todos os aspectos de containers e suas ferramentas! Foi uma live sensacional!

Conteúdos comentados:

]]>
<![CDATA[ O guia completo do gRPC parte 2: Mãos à obra com JavaScript ]]> https://blog.lsantos.dev/o-guia-do-grpc-2/ 6089bf6f82f748731771304f Qua, 12 Mai 2021 10:00:00 -0300

Este artigo é parte de uma série

  1. O guia completo do gRPC parte 1: O que é gRPC?
  2. O guia completo do gRPC parte 2: Mãos à obra com JavaScript
  3. O guia completo do gRPC parte 3: Tipos em tudo com TypeScript!
  4. O guia completo do gRPC parte 4: Streams

Chegamos à segunda parte da nossa série sobre o que é o gRPC e como podemos utilizá-lo de forma eficiente para substituir o que utilizamos hoje com o ReST. Na primeira parte desta série dei toda a explicação de como funciona o gRPC por dentro e como ele é montado em uma requisição HTTP/2 padrão com um payload binário usando o protobuf como camada de encoding.

Nesta parte da série, vamos mergulhar nas implementações de como o gRPC funciona para JavaScript. Vamos então dar uma passada rápida pela nossa agenda de hoje.

Agenda

  • Quais são as ferramentas existentes para gRPC no JavaScript hoje em dia
  • Como funciona o modelo cliente/servidor e os modelos disponíveis que podemos usar
  • Criando seu primeiro arquivo .proto
  • Vantagens e desvantagens dos modelos estáticos e dinâmicos
  • Hora do código!

As ferramentas que trabalhamos

Como dito por Russel Brown em sua incrível série "The Weird World of gRPC Tooling for Node.js", a documentação do protobuf especialmente para JavaScript ainda não é totalmente bem documentada, e isso é um tema recorrente. Todo o protobuf foi feito com foco em trabalhar com várias linguagens de mais baixo nível como Go e C++. Para estas linguagens, a documentação é muito boa, porém quando chegamos no JavaScript e TypeScript, começamos a ver um problema de documentação onde ela ou não está totalmente completa ou então não existe de forma nenhuma.

Felizmente este cenário está mudando muito, grande parte graças a Uber, que está trabalhando em ferramentas incríveis como o Buf e também uma série de boas práticas criadas em outra incrível ferramenta chamada Prototool.

Para este artigo vamos nos manter nas ferramentas tradicionais criadas pela própria equipe do gRPC e, em um futuro artigo, vamos explorar ainda mais este mundo com outras ferramentas de suporte.

Proto Compiler, ou, protoc

A nossa principal ferramenta de manipulação de protofiles, chamada protoc faz parte do mesmo pacote dos protocolbuffers, podemos pensar nela como sendo o CLI do protobuf.

Esta é a implementação principal do gerador de códigos e parser do protobuf em diversas linguagens, que estão descritas no README do repositório. Existe uma página com os principais tutoriais, mas, como esperado para nós, ela não cobre JavaScript...

Podemos utilizar o protoc como uma linha de comando para poder converter os nossos arquivos .proto de definição de contratos em um arquivo .pb.js que contém o código necessário para podermos serializar e desserializar nossos dados no formato binário usado pelo protobuf e enviar pelo protocolo de transporte HTTP/2.

Em teoria, podemos criar uma request manual para um serviço gRPC utilizando somente um client HTTP/2, sabendo a rota que queremos mandar o nosso dado e os headers necessários. Todo o resto do payload pode ser identificado como a representação binária do que o protobuf produz no final da compilação. Vamos ver sobre isto mais futuramente.

protobufjs

É a implementação alternativa do protoc feita inteiramente em JavaScript, é ótima para lidar com os arquivos protobuf como mensagens, ou seja, se você está utilizando o protobuf como sistema de envio de mensagens entre filas, por exemplo, como já demonstramos no artigo anterior, ele é excelente para poder gerar uma implementação mais amigável para ser utilizada em JavaScript.

O problema é que ele não tem suporte ao gRPC, ou seja, não podemos definir serviços e nem RPCs em cima de arquivos protobuf, o que faz deste pacote, essencialmente, o decoder de mensagens.

@grpc/proto-loader

É a peça que faltava para o protobufjs conseguir gerar as definições de stub e skeletons de forma dinâmica a partir de arquivos .proto. Hoje é a implementação recomendada para o que vamos fazer no resto do artigo, que é a implementação de forma dinâmica dos arquivos de contrato, sem que precisemos pré-compilar todos os protofiles antes.

grpc e grpc-js

O core que faz o gRPC funcionar dentro de linguagens dinâmicas como o JS e o TS. O pacote original grpc possui duas versões, uma versão implementada como uma lib em C que é mais utilizada para quando estamos escrevendo ou o client ou o server em C ou C++.

Nota importante: Desde que este artigo foi publicado, a biblioteca grpc foi marcada para depreciação, então a partir de agora, use sempre a versão mantida e mais nova que é a @grpc/grpc-js.

Para o nosso caso, o ideal é utilizarmos a implementação como um pacote do NPM que, essencialmente, pega a implementação em C que falamos anteriormente, utiliza o node-gyp para compilar esta extensão como um native module do Node.js, de forma que todos os bindings entre o C e o Node são feitos utilizando a N-API que faz o intermédio entre os código C++ e os códigos JavaScript, permitindo que possamos integrar código JavaScript com código C++ em runtime.

Se você quiser saber mais sobre a integração do Node com o C++ e como tudo funciona por baixo dos panos, eu tenho uma série de 10 partes sobre os internals do Node.js que recomendo a leitura.

Atualmente, o pacote do NPM para o gRPC é o mais utilizado para criar clientes gRPC, embora, atualmente, muitas pessoas estejam migrando para o grpc-js, uma implementação feita completamente em JS do client do gRPC.

O modelo cliente servidor no gRPC

O modelo cliente e servidor que temos no gRPC não é nada mais do que uma comunicação HTTP/2 padrão, a diferença são os headers que estamos enviando. Como expliquei na primeira parte da série, toda a comunicação via gRPC é, na verdade, uma chamada HTTP/2 com um payload binário encodado em base64.

Para ilustrar essa comunicação, junto com o código que vamos fazer por aqui, coloquei um pequeno exemplo de uma chamada gRPC utilizando uma ferramenta chamada grpc-web que permite a conexão do browser diretamente com um client gRPC, pois o browser, apesar de suportar HTTP/2, não expõe essa configuração para que os clients das aplicações possam fazer requests usando o protocolo.

Se você quiser saber um pouco mais sobre como ele funciona, este artigo do Mark Kose tem um bom overview de como uma comunicação deste tipo pode ser feita.

O problema é que, devido às regras de CORS mais restritas e a falta de um servidor que me permita alterar estas opções, a chamada foi bloqueada de retornar, porém para o que quero mostrar por aqui (que é apenas a request) vai servir.

Uma chamada gRPC feita a partir do browser mostrando a URL e as informações de requisição

Veja que nossa URL de requisição é /{serviço}/{metodo}, isso é válido para qualquer coisa que tenhamos que executar, inclusive, se tivermos serviços com namespaces como, por exemplo, com.lsantos.notes.v1 nossa URL se comportará de forma diferente, sendo uma expressão do nosso serviço completo, por exemplo http://host:porta/com.lsantos.notes.v1.NoteService/Find.

Neste serviço vamos criar um sistema de notas que possui apenas dois métodos, o List e Find. O método List não recebe nenhum parâmetro, já o Find recebe um parâmetro id que estamos enviando no payload como podemos ver na imagem. Veja que ele está encodado como base64 com o valor AAAAAAMKATI=.

Dentro do repositório do código, temos um arquivo request.bin, que é o resultado de um echo "AAAAAAMKATI=" | base64 -d > request.bin. Se abrirmos este arquivo com algum Hex Editor (como o que mostramos no primeiro artigo da série, no VSCode), vamos ver os seguintes bytes: 00 00 00 00 03 0A 01 32. Removemos todos os 00 e também o 03 já que ele é apenas um marcador do encoding para o grpc-web. No final vamos ter 0A 01 32 e podemos passar pelo mesmo modelo de análise que fizemos antes no outro artigo da série:

Diagramação dos bits da request

Podemos ver que estamos mandando uma string com o valor "2" como payload, que é o primeiro índice.

Proto files

Vamos colocar a mão na massa e desenvolver o nosso primeiro arquivo .proto que vai descrever como toda a nossa API vai funcionar.

Primeiramente, vamos criar um novo projeto em uma pasta com o npm init -y, você pode chamá-lo como quiser. Em seguida vamos instalar as dependências que vamos precisar com npm i -D google-protobuf protobufjs.

Agora vamos criar uma pasta proto e dentro dela um arquivo chamado notes.proto. Este vai ser o arquivo que vai descrever a nossa API e todo o nosso serviço. Sempre vamos começar utilizando uma notação de sintaxe:

// notes.proto
syntax = "proto3";

Existem duas versões de sintaxe do protobuf, você pode ver mais sobre estas versões neste artigo. Para nós, as partes mais importantes é que, agora, todos os campos do protobuf se tornam opcionais, não temos mais a notação required que existia na versão 2 da sintaxe, e também não temos mais os valores padrões para propriedades (que, essencialmente, as torna opcionais).

Agora, vamos começar com a organização do arquivo, eu geralmente organizo um arquivo protobuf seguindo a ideia de Serviço -> Entidades -> Requests -> Responses. De acordo com as boas práticas da Uber, também é interessante utilizarmos um marcador de namespace como com.seuusername.notes.v1 caso precisemos manter mais de uma versão ao mesmo tempo, porém, para facilitar o desenvolvimento aqui, vamos utilizar a forma mais simples sem nenhum namespace.

O protobuf também permite importação de pacotes de outros namespaces e reutilização de definições entre diferentes arquivos.

Vamos definir primeiramente o nosso serviço, ou RPC, que é a especificação de todos os métodos que nossa API vai aceitar:

// notes.proto
syntax = "proto3";

service NoteService {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

Alguns detalhes são importantes quando estamos falando de services:

  • Cada rpc é uma rota e, essencialmente, uma ação que pode ser feita na API.
  • Cada RPC só pode receber um parâmetro de entrada e um de saída.
  • O tipo Void que definimos, pode ser substituido pelo tipo google.protobuf.Empty, que é um chamado Well-Known type, porém ele exige que a biblioteca com estes tipos esteja instalada em sua máquina.
  • Uma outra boa prática da Uber é colocar Request e Response nos seus parâmetros, essencialmente criando um wrapper deles em volta de um objeto maior.

Vamos então definir as entidades que queremos, primeiro vamos definir o tipo Void, que é nada mais do que um objeto vazio:

// notes.proto
syntax = "proto3";

service NoteService {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entidades
message Void {}

Cada tipo de objeto é definido com a keyword message, pense em cada message como sendo um objeto JSON. Nossa aplicação é uma lista de notas, então vamos definir a entidade de notas:

// notes.proto
syntax = "proto3";

service NoteService {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entidades
message Void {}

message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

Aqui estamos definindo todos os nossos tipos para a nossa entidade principal, a própria nota. Temos diversos tipos escalares no protobuf, assim como enumeradores e outros tipos bem definidos na documentação da linguagem.

Perceba também que definimos a mensagem e seus campos no modelo tipo nome = indice;. Temos obrigatoriamente que passar os indices para a mensagem, caso contrário o protobuf não saberá decodificar o binário.

Note que mudamos um pouco a definição do nosso tipo para não receber mais uma string e sim um inteiro, diferente do que fizemos no grpc-web anteriormente. Você consegue descobrir qual é o binário gerado?

Agora vamos especificar os tipos Request e Response que criamos na nossa definição de serviço no início do arquivo. Primeiro vamos começar com os mais simples, a request para o método Find leva somente um ID, então vamos especificar o NoteFindRequest:

// notes.proto
syntax = "proto3";

service NoteService {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entidades
message Void {}

message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

Partimos para a resposta deste mesmo método, que deve devolver uma nota caso ela for encontrada. Para isto vamos criar a NoteFindResponse e entender porque é uma boa prática este modelo.

// notes.proto
syntax = "proto3";

service NoteService {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entidades
message Void {}

message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

// Responses
message NoteFindResponse {
  Note note = 1;
}

Por que estamos criando uma response ao invés de utilizar diretamente o tipo Note como resposta? Poderíamos alterar o nosso serviço para receber o Note como resposta:

service NoteService {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (Note);
}

O problema é que se fizermos assim, teremos mais problemas para buscar estes detalhes diretamente do cliente, como uma boa prática é sempre interessante envolvermos a resposta de algum tipo composto (como o Note) em um índice de mesmo nome, essencialmente nosso retorno passa de:

{
  "id": 1,
  "title": "titulo",
  "description": "descrição"
}

Para:

{
  "note": {
    "id": 1,
    "title": "titulo",
    "description": "descrição"
  }
}

É muito mais semântico, não acha?

Para finalizar, vamos criar a resposta do nosso serviço de listagem:

// notes.proto
syntax = "proto3";

service NoteService {
  rpc List (Void) returns (NoteListResponse);
  rpc Find (NoteFindRequest) returns (NoteFindResponse);
}

// Entidades
message Void {}

message Note {
  int32 id = 1;
  string title = 2;
  string description = 3;
}

// Requests
message NoteFindRequest {
  int32 id = 1;
}

// Responses
message NoteFindResponse {
  Note note = 1;
}

message NoteListResponse {
  repeated Note notes = 1;
}

Aqui temos uma keyword nova, a repeated, ela identifica um array do tipo subsequente, neste caso um array de Note.

Este será o nosso arquivo de definição de contrato. Pense que podemos também usar ele para, se tivéssemos um serviço de fila, por exemplo, codificar uma Nota exatamente como é utilizada em outros sistemas de forma binária, e enviarmos pela rede sem ter medo de que o outro lado não entenda o que estamos enviando. Ou seja, podemos padronizar todas as entradas e saídas de todas as APIs de um grande sistema somente com arquivos declarativos.

Estático ou dinâmico

O gRPC sempre terá duas formas de ser compilado, a primeira forma é o modelo estático de compilação.

Neste modelo, executamos o protoc para poder compilar os arquivos em arquivos .js que contém as definições de tipo e de encoding das nossas mensagens. A vantagem deste modelo é que podemos utilizar os tipos como uma lib ao invés de ler eles diretamente, porém eles são muito mais complexos de se trabalhar do que se simplesmente tentarmos gerar dinamicamente o conteúdo dos pacotes.

Veja o script compile dentro do arquivo package no repositório deste projeto para entender como podemos compilar os arquivos e como eles ficam depois de compilados.

Não vou me estender no modelo de geração estática neste artigo, mas novamente o Russel Brown tem um artigo excelente sobre a criação de serviços estáticos usando gRPC.

O que vamos fazer é a geração dinâmica, neste modelo não temos que, manualmente, encodar e decodar todas as mensagens. O modelo dinâmico também suporta melhor pacotes importados. Porém, como tudo tem um lado ruim, o contra de se usar a geração dinâmica é que vamos sempre precisar ter as fontes originais, ou seja, temos que importar e baixar os arquivos .proto juntamente com os arquivos do nosso projeto. Isso pode ser um problema em alguns casos:

  • Quando temos diversos sistemas interconectados, temos que ter um repositório central onde vamos buscar todos os protofiles.
  • Sempre que atualizarmos um arquivo .proto vamos ter que identificar esta mudança e atualizar todos os serviços correspondentes.

Os problemas são facilmente resolvidos com um sistema de gerenciamento de pacotes como o NPM, porém mais simples. Além disso, ou próprio Buf, que citamos anteriormente, já está trabalhando para trazer esta funcionalidade ao protobuf.

Servidor

Para começarmos a criar o servidor, vamos instalar os pacotes necessários do gRPC, começando pelo próprio grpc e o proto-loader com o comando npm i grpc @grpc/proto-loader.

Crie uma pasta src e um arquivo server.js. Vamos começar importando os pacotes e carregando a definição do protobuf dentro do servidor gRPC:

//server.js
const grpc = require('grpc')
const protoLoader = require('@grpc/proto-loader')
const path = require('path')

const protoObject = protoLoader.loadSync(path.resolve(__dirname, '../proto/notes.proto'))
const NotesDefinition = grpc.loadPackageDefinition(protoObject)

O que estamos fazendo aqui é essencialmente a ideia do que comentamos sobre geração dinâmica. O arquivo proto será carregado na memória e parseado em tempo de execução, e não pré-compilado. Primeiramente o protoLoader carrega um objeto a partir de um arquivo .proto, pense nele como uma representação intermediária entre o serviço real e o que você pode manipular com o JavaScript.

Depois passamos esta interpretação para o grpc, essencialmente gerando uma definição válida que podemos utilizar para criar um serviço e, consequentemente, uma API. Tudo o que vier daqui para frente agora é a implementação específica da nossa regra de negócio. Vamos começar criando o nosso "banco de dados".

Como queremos algo simples, vamos criar apenas um objeto e um array de notas que serão manipuladas pelas nossas funções:

const grpc = require('grpc')
const protoLoader = require('@grpc/proto-loader')
const path = require('path')

const protoObject = protoLoader.loadSync(path.resolve(__dirname, '../proto/notes.proto'))
const NotesDefinition = grpc.loadPackageDefinition(protoObject)

const notes = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

Vamos agora criar e iniciar nosso servidor adicionando o serviço que acabamos de ler do arquivo .proto:

//server.js
const grpc = require('grpc')
const protoLoader = require('@grpc/proto-loader')
const path = require('path')

const protoObject = protoLoader.loadSync(path.resolve(__dirname, '../proto/notes.proto'))
const NotesDefinition = grpc.loadPackageDefinition(protoObject)

const notes = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]


const server = new grpc.Server()
server.addService(NotesDefinition.NoteService.service, { List, Find })

server.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure())
server.start()
console.log('Listening')

Veja que estamos adicionando a NotesDefinition.NoteService.service, que é uma classe que contém o nosso servidor HTTP que irá responder pelas requisições enviadas, depois disso estamos mandando um objeto {List, Find}, estas são as implementações dos nossos dois métodos que ainda temos que fazer.

Além disso, estamos ouvindo na porta 50051, esta porta pode ser qualquer uma que você possuir livre no seu computador até 65535. Embora, é uma boa prática escolher as portas acima de 50000 para deixarmos uma boa diferença das portas comuns como 8080, 443, 9090, 3000 e etc.

Por fim, estamos utilizando o createInsecure porque, por padrão, o HTTP/2 exige um certificado digital para ser iniciado, então estamos somente passando uma certificação vazia para não termos que criar um localmente. Se você for colocar este serviço em produção, você deve utilizar um novo certificado digital para as comunicações.

Implementação

Para podermos ter o nosso servidor executando, precisamos implementar cada um dos RPCs que definimos nele. Neste caso criamos um RPC List e outro Find. A implementação deles é simplesmente uma função que leva um erro e um callback como assinatura. Porém, elas precisam ter o mesmo nome dos RPCs obrigatoriamente.

Vamos aprender com o exemplo mais simples, a implementação do método List. O que ele faz é sempre devolver a lista total de notas.

function List (_, callback) {
  return callback(null, { notes })
}

Veja que também temos que seguir o mesmo modelo de resposta, se dizemos no nosso protofile que estamos esperando que o retorno seja uma lista de Note dentro de um índice chamado notes, temos que devolver um objeto { notes }.

O callback é uma função que vamos chamar no modelo callback (err, response), ou seja, se tivermos erros, vamos mandá-los no primeiro parâmetro e a resposta como nula e vice-versa.

Para fazermos o método Find temos que tratar alguns erros e realizar um find dentro do nosso array. O método é bastante simples, porém ele leva um parâmetro id, para buscar este parâmetro vamos utilizar o primeiro parâmetro da função - que ignoramos no List com o _ - para pegar um objeto request, dentro do qual haverá o nosso parâmetro id enviado:

function Find ({ request: { id } }, callback) {
  const note = notes.find((note) => note.id === id)
  if (!note) return callback(new Error('Not found'), null)
  return callback(null, { note })
}

É importante dizer que, se tivermos um erro dentro do gRPC e não o retornamos como o primeiro parâmetro (se simplesmente dermos um return ou um throw) isso fará com que nosso client não receba as informações corretas, por isso que devemos criar uma estrutura de erro e retorná-la no callback.

Da mesma forma, quando chamamos a função callback no final da execução, estamos passando o erro como nulo, o que indica que tudo correu bem, e também estamos mandando um objeto { note }, conforme nossa NoteFindResponse especificou.

O arquivo completo do servidor fica assim:

//server.js
const grpc = require('grpc')
const protoLoader = require('@grpc/proto-loader')
const path = require('path')

const protoObject = protoLoader.loadSync(path.resolve(__dirname, '../proto/notes.proto'))
const NotesDefinition = grpc.loadPackageDefinition(protoObject)

const notes = [
  { id: 1, title: 'Note 1', description: 'Content 1' },
  { id: 2, title: 'Note 2', description: 'Content 2' }
]

function List (_, callback) {
  return callback(null, { notes })
}

function Find ({ request: { id } }, callback) {
  const note = notes.find((note) => note.id === id)
  if (!note) return callback(new Error('Not found'), null)
  return callback(null, { note })
}

const server = new grpc.Server()
server.addService(NotesDefinition.NoteService.service, { List, Find })

server.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure())
server.start()
console.log('Listening')

Client

O client não é muito diferente, as primeiras linhas são exatamente as mesmas do servidor, afinal estamos carregando o mesmo arquivo de definição. Vamos codá-lo na mesma pasta src em um arquivo client.js:

//client.js
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')
const path = require('path')

const protoObject = protoLoader.loadSync(path.resolve(__dirname, '../proto/notes.proto'))
const NotesDefinition = grpc.loadPackageDefinition(protoObject)

Aqui estou usando, para fins de explicação, o pacote @grpc/grpc-js, a grande diferença entre ele e o pacote grpc original, além da implementação, é que ele não possui um método bind para o servidor, então você precisa utilizar o bindAsync (caso você queira utilizar ele para fazer o server também). No cliente, você pode substituir ele tranquilamente pelo pacote grpc assim como no servidor. Se você quiser seguir este tutorial e utilizar os dois, então instale o grpc-js com o comando npm i @grpc/grpc-js.

A grande diferença entre o servidor e o cliente é que, no cliente, ao invés de carregarmos o serviço inteiro para poder subir um servidor, vamos apenas carregar a definição do serviço de notas. Afinal só precisamos da chamada de rede e do que ele vai responder.

//client.js
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')
const path = require('path')

const protoObject = protoLoader.loadSync(path.resolve(__dirname, '../proto/notes.proto'))
const NotesDefinition = grpc.loadPackageDefinition(protoObject)

const client = new NotesDefinition.NoteService('localhost:50051', grpc.credentials.createInsecure())

Veja que estamos inicializando uma nova instancia de NoteService e não adicionando um NoteService.service. Ainda sim temos que passar o mesmo endereço do servidor para podermos ter uma comunicação feita.

A partir daqui já temos tudo que precisamos, nosso cliente possui todos os métodos definidos no nosso RPC e podemos chamá-lo como se fosse uma chamada de objeto local:

//client.js
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')
const path = require('path')

const protoObject = protoLoader.loadSync(path.resolve(__dirname, '../proto/notes.proto'))
const NotesDefinition = grpc.loadPackageDefinition(protoObject)

const client = new NotesDefinition.NoteService('localhost:50051', grpc.credentials.createInsecure())

client.list({}, (err, notes) => {
  if (err) throw err
  console.log(notes)
})

Esta chamada fará com que o servidor nos envie a lista de notas, assim como chamarmos o endpoint de Find fará a busca pelas notas:

//client.js
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')
const path = require('path')

const protoObject = protoLoader.loadSync(path.resolve(__dirname, '../proto/notes.proto'))
const NotesDefinition = grpc.loadPackageDefinition(protoObject)

const client = new NotesDefinition.NoteService('localhost:50051', grpc.credentials.createInsecure())

client.list({}, (err, notes) => {
  if (err) throw err
  console.log(notes)
})

client.find({ id: 2 }, (err, { note }) => {
  if (err) return console.error(err.details)
  if (!note) return console.error('Not Found')
  return console.log(note)
})

Perceba que, no client, a chamada para as funções estão em letras minúsculas, porém existem as duas versões dentro do mesmo objeto.

Já estamos tratando o erro de não haver uma nota com o ID informado, bem como enviando o parâmetro { id: 2 } como especificado na nossa NoteFindRequest.

Indo além

Trabalhar com callbacks é meio ruim então podemos converter as chamadas para um formato mais atual com async desta forma:

function callAsync (client, method, parameters) {
  return new Promise((resolve, reject) => {
    client[method](parameters, (err, response) => {
      if (err) reject(err)
      resolve(response)
    })
  })
}

E chamar seu cliente desta forma:

callAsync(client, 'list', {}).then(console.log).catch(console.error)

Uma outra possibilidade é também retornar todos os métodos como funções assíncronas, essencialmente tornando o cliente inteiro assíncrono. Podemos pegar todas as propriedades enumeráveis do objeto e, para cada uma, criar uma variante {propriedade}Async:

function promisify (client) {
  for (let method in client) {
    client[`${method}Async`] = (parameters) => {
      return new Promise((resolve, reject) => {
        client[method](parameters, (err, response) => {
          if (err) reject(err)
          resolve(response)
        })
      })
    }
  }
}

E modificar o nosso arquivo para ser assim:

const client = new NotesDefinition.NoteService('localhost:50051', grpc.credentials.createInsecure())
promisify(client)

client.listAsync({}).then(console.log)

Como saída, vamos ter nosso objeto Note.

Conclusão

Chegamos ao fim do nosso segundo artigo da série, aqui discutimos um pouco sobre como podemos criar nosso serviço gRPC usando JavaScript, descobrimos como podemos transformar ele em algo assíncrono e também entendemos melhor os conceitos e ferramentas por trás do desenvolvimento de uma aplicação gRPC utilizando JavaScript.

No próximo artigo, vamos melhorar ainda mais esta aplicação trazendo os tipos do TypeScript!

Se você curtiu este post, compartilhe com seus amigos e, se você não quiser ficar por fora dos demais lançamentos e dicas, assine a newsletter :D!

Até mais!

]]>
<![CDATA[ O que é Open Source? ]]> https://blog.lsantos.dev/o-que-e-open-source/ 60a6c7865868507c4d4f3a4e Ter, 11 Mai 2021 17:41:00 -0300 Participei da incrível live (que, infelizmente, precisou ser gravada) do IFCE sobre "O que é o Software Open Source". Foi uma live sensacional e discutimos um monte de pontos legais sobre softwares open source e suas premissas, falamos sobre a história dos computadores e também sobre licenças open source!

]]>
<![CDATA[ O que há de novo no beta do TypeScript 4.3 ]]> https://blog.lsantos.dev/o-que-ha-de-novo-no-beta-do-typescript-4-3/ 6099696a8529952cae14fdaf Seg, 10 Mai 2021 15:38:14 -0300 A nova versão do TypeScript saiu em beta no dia 1 de abril de 2021! Por enquanto essa versão ainda não está pronta para ser utilizada em produção, mas ela já inclui algumas mudanças e correções super legais!

Para testar isso tudo você pode instalar a versão mais nova com npm i typescript@beta e sair usufruindo das novas funcionalidades!

Tipos de escrita e leitura separados

Originalmente quando temos algum tipo de propriedade em uma classe que pode ser escrita e lida de formas diferentes, fazemos um getter e um setter para essa propriedade, por exemplo:

class Foo {
    #prop = 0
    
    get prop() {
        return this.#prop
    }

	set prop (value) {
        let val = Number(value)
        if (!Number.isFinite(num)) return
        this.#prop = val
    }
}

No TypeScript, por padrão, o tipo é inferido a partir do tipo de retorno no get, o problema é que, se tivermos uma propriedade set que pode ser setada de várias formas, por exemplo, como uma string ou number, o tipo de retorno desta propriedade será inferido como unknown ou any.

O problema disso é que, quando estamos utilizando unknown, forçamos um cast para o tipo que queremos, e any realmente não faz nada. Esse modelo nos forçava a tomar uma escolha entre ser preciso ou permissivo. No TS 4.3 podemos especificar tipos separados para entrada e saída das propriedades:

class Foo {
    private prop = 0
    
    get prop(): number {
        return this.prop
    }

	set prop (value: string | number) {
        let val = Number(value)
        if (!Number.isFinite(num)) return
        this.prop = val
    }
}

E isso não é limitado apenas às classes, podemos fazer a mesma coisa com objetos literais:

function buildFoo (): Foo {
  let prop = 0
  return {
    get prop(): number { return prop }
    set prop(value: string | number) {
      let val = Number(value)
      if (!Number.isfinite(val) return
      prop = val
    }
  }
}

E isso também vale para interfaces:

interface Foo {
  get prop (): number
  set prop (value: string | number)
}

A única limitação que temos aqui é que o método set precisa ter na sua lista de tipos o mesmo tipo do get, ou seja, se temos um getter que retorna um number o setter precisa aceitar um number.

Keyword override

Uma mudança menos comum mas igualmente importante vem quando temos classes derivadas. Geralmente, quando usamos uma classe derivada com extends, temos vários métodos da classe pai que precisam ser sobrescritos, ou então adaptados. Para isso nós escrevemos um método na classe derivada com a mesma assinatura:

class Pai {
  metodo (value: boolean) { }
  outroMetodo (value: number) {}
}

classe Filha extends Pai {
  metodo () { }
  outroMetodo () { }
}

O que acontece é que estamos sobrescrevendo os dois métodos da classe pai e utilizando somente os da classe derivada. Porém, se modificarmos a classe pai e removermos os dois métodos em favor de um único método, assim:

class Pai {
  metodoUnico (value: boolean) { }
}

classe Filha extends Pai {
  metodo () { }
  outroMetodo () { }
}

O que acontece é que nossa classe filha não vai sobrescrever mais o método da classe pai e, portanto, vai ter dois métodos completamente inúteis que nunca serão chamados.

Por conta disso, o TypeScript 4.3 adicionou uma nova keyword chamada override. O que esta keyword faz é informar o servidor que um método da classe filha está sendo explicitamente sobrescrito, então podemos fazer desta forma:

class Pai {
  metodo () { }
  outroMetodo () { }
}

classe Filha extends Pai {
  override metodo () { }
  override outroMetodo () { }
}

Neste exemplo estamos dizendo para o TypeScript procurar explicitamente na classe pai se existem dois métodos com estes nomes. E ai se modificarmos a nossa classe pai e mantermos a classe filha:

class Pai {
  metodoUnico (value: boolean) { }
}
classe Filha extends Pai {
  override metodo () { }
  override outroMetodo () { }
}

// Error! This method can't be marked with 'override' because it's not declared in 'Pai'.

Além disso uma nova flag --noImplicitOverride foi adicionada para evitar que esqueçamos de fazer essa identificação. Quando isso acontecer não vamos poder sobrescrever algum método sem escrever override antes e, todos os métodos não marcados, não serão estendidos.

Auto Imports

A última atualização importante que vamos comentar é mais sobre uma melhoria significativa de vida para todos que escrevem imports (que é, basicamente, todo mundo). Antes, quando escrevíamos `import {` o TypeScript não tinha como saber o que íamos importar, então frequentemente escrevíamos `import {} from 'modulo.ts'` e depois voltávamos aos `{}` para poder usar o autocomplete no que sobrou.


Na versão 4.3, vamos ter a inteligência dos auto-imports que já existem no editor para poder completar as nossas declarações, como o vídeo mostra:

Animação descrevendo a importação de um módulo usando o autoimport para completar o texto
Animação com os detalhes do autoimport para completar a declaração d emódulos importador no TypeScript

A parte importante aqui é que precisamos que o editor suporte essa funcionalidade, por enquanto ela está disponível na versão 1.56 do VSCode normal, mas somente com a extensão do TS/JS nightly instalada.

Outras atualizações

Além das atualizações que comentamos, o TypeScript também modificou e melhorou bastante a forma como os templete literal types são inferidos e identificados. Agora podemos utilizar eles de uma maneira muito mais simples e direta.

Também temos melhores asserções de Promises e uma breaking change nos arquivos .d.ts que podem ser lidas lá no artigo oficial do lançamento.

]]>
<![CDATA[ O jeito mais simples de aprender Kubernetes é com a sua API ]]> https://blog.lsantos.dev/drop-o-jeito-mais-simples-de-aprender-kubernetes/ 60941776a749161e1b4975b5 Qui, 06 Mai 2021 15:50:34 -0300 NOVIDADE!
Este é o primeiro artigo de um novo tipo de conteúdo que estou chamando de drop. Drops são pequenos textos (com até 5 minutos de leitura) que mostram aspectos interessantes de algum assunto. No geral, serão pequenos tutoriais ou dicas interessantes.

O objetivo destes drops são para que eu possa criar conteúdo mais frequentemente, ao invés de criar semanalmente um conteúdo longo, que estava ficando bastante pesado para mim, como único escritor/editor do blog.

Espero que gostem :)

Um dos pontos que quase todo dev concorda é que o Kubernetes é bastante complicado. Principalmente para quem está aprendendo e quem está entrando agora no mundo de aplicações distribuídas e containers.

Apesar de existirem excelentes livros sobre o assunto, o conteúdo ainda é complexo e exige que as pessoas pensem um pouco fora do que estamos acostumados a ver em um ambiente de deploy tradicional. Em parte, isto vem porque precisamos configurar muitas extensões e o Kubernetes é absurdamente extensível, e assim pensamos que o Kubernetes é um único grande sistema, mas na verdade ele é composto de diversas pequenas APIs que manipulam arquivos.

A grande ideia

A grande ideia por trás do Kubernetes é que tudo é um pequeno arquivo, assim como o unix já nos mostrou antes, essa é uma excelente ideia para quando trabalhamos com configurações extensíveis.

Simplificando MUITO o fluxo. Quando criamos um Deployment, um Pod, um Service, o que estamos fazendo é, na verdade, adicionando um item em um banco de dados (etcd) que, por sua vez, é monitorado por uma série de control loops que chamamos de controllers. E são estes controllers que, de fato, fazem o trabalho de sincronizar os estados desejados e existentes deste cluster.

O mais legal de tudo isso é que o Kubernetes possui uma API muito boa para podermos obter estes recursos.

Entendendo a API

Toda a API do Kubernetes segue à risca a ideia do ReST. Então sempre vamos ter um recurso que começa desta forma:

http://<dns do control plane>/api/<versão>/namespaces/<namespace>/<recurso>/[nome][?opções]

Primeiramente precisamos obter o endereço do nosso control plane. Isso é super simples, podemos somente executar o comando kubectl cluster-info --context <nome do context>.

O contexto pode ser omitido se você quiser a informação do cluster que está no contexto atual.

Isso vai nos dar uma saída como esta:

Kubernetes control plane is running at https://algundns.subdominio.tld:443
CoreDNS is running at https://algundns.subdominio.tld:443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
Metrics-server is running at https://algundns.subdominio.tld:443/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy

Lembrando que esta saída pode variar dependendo de onde você está hospedando seu cluster.

Segurança

A API do Kubernetes é um servidor ReST simples, porém as características de segurança utilizadas para poder manter seu cluster seguro, já que esta API tem total controle dentro do control plane, são bem elaboradas.

O uso de certificados digitais atrelados a objetos de usuário e RBAC do sistema (ou até mesmo usando técnicas mais avançadas como AD) são empregadas para proteger a informação.

Como estamos fazendo apenas uma demonstração, podemos utilizar o próprio kubectl para gerenciar este acesso para nós, já que ele tem todos os dados de acesso de todos os clusters. Basta executarmos kubectl proxy & para rodar um processo em segundo plano que vai fazer um port forwarding da API do kubernetes para uma porta local, assim poderemos acessar os dados da api sem precisar nos preocupar com configurações de permissão.

$ kubectl proxy &
[1] 5705
Starting to serve on 127.0.0.1:8001

Manipulando a API

Agora que temos o cluster rodando localmente, você pode usar o gerenciador de requests de sua preferência, como o cURL, wget, postman. Eu estou usando o insomnia.

Vamos obter a lista de pods do meu cluster usando a api com a request GET http://localhost:8001/api/v1/namespaces/default/pods:

Gerenciador Insomnia mostrando a saída da request para obter os pods do Kubernetes

Alguns recursos, como os deployments, não estão no que chamamos de "core API" do Kubernetes. A core API é quando não precisamos especificar nada no campo apiVersion do recurso, como nos Pods, que é apiVersion: v1, isso significa que podemos acessar com /api/v1.

Os deployments fazem parte de apps/v1, para isso temos um novo recurso base chamado apis, então podemos obter a lista de deployments com http://localhost:8001/apis/apps/v1/namespaces/default/deployments:

Insomnia com a resposta para a lista de deployments do kubernetes

O mesmo vale para os ingresses que estão em networking.k8s.io/v1beta1 (ou em v1 dependendo da versão do seu cluster). Desta forma o endereço é http://localhost:8001/apis/networking.k8s.io/v1/namespaces/default/ingresses

Insomnia mostrando a saída da requisição para a lista de ingresses
Quando estamos tratando com o namespace default, que é o padrão, podemos omitir completamente a parte /namespaces/default, ficando somente http://localhost:8001/api/v1/pods.

Quer saber mais?

Dê uma olhada na documentação da API do Kubernetes, ou também passe a flag -v6 para qualquer comando kubectl para ver o caminho que ele está chamando (se você passar -v8 vai ver também o body de resposta):

 $ kubectl get pods -v6
I0506 15:28:38.203647    6011 loader.go:379] Config loaded from file:  /home/khaosdoctor/.kube/config
I0506 15:28:38.796623    6011 round_trippers.go:445] GET https://dominio.subdominio.tld:443/api/v1/namespaces/default/pods?limit=500 200 OK in 580 milliseconds

E aqui está uma lista de livros sensacionais sobre Kubernetes que você pode utilizar para aprender mais!

Kubernetes: Tudo sobre orquestração de contêineres eBook: Santos, Lucas: Amazon.com.br: Loja Kindle
Kubernetes: Tudo sobre orquestração de contêineres eBook: Santos, Lucas: Amazon.com.br: Loja Kindle
Kubernetes Básico: Mergulhe no futuro da infraestrutura eBook: Burns, Brendan, Beda, Joe, Hightower, Kelsey: Amazon.com.br: Loja Kindle
Kubernetes Básico: Mergulhe no futuro da infraestrutura eBook: Burns, Brendan, Beda, Joe, Hightower, Kelsey: Amazon.com.br: Loja Kindle
DevOps Nativo de Nuvem com Kubernetes: Como Construir, Implantar e Escalar Aplicações Modernas na Nuvem | Amazon.com.br
Compre online DevOps Nativo de Nuvem com Kubernetes: Como Construir, Implantar e Escalar Aplicações Modernas na Nuvem, de Arundel, John, Domingus, Justin na Amazon. Frete GRÁTIS em milhares de produtos com o Amazon Prime. Encontre diversos livros escritos por Arundel, John, Domingus, Justin com ótim…
KUBERNETES: A Simple Guide to Master Kubernetes for Beginners and Advanced Users (2020 Edition) (English Edition) - eBooks em Inglês na Amazon.com.br
Compre KUBERNETES: A Simple Guide to Master Kubernetes for Beginners and Advanced Users (2020 Edition) (English Edition) de Docker, Brian na Amazon.com.br. Confira também os eBooks mais vendidos, lançamentos e livros digitais exclusivos.
Quick Start Kubernetes (English Edition) eBook: Poulton, Nigel: Amazon.com.br: Loja Kindle
Quick Start Kubernetes (English Edition) eBook: Poulton, Nigel: Amazon.com.br: Loja Kindle
]]>
<![CDATA[ O guia completo do gRPC parte 1: O que é gRPC? ]]> https://blog.lsantos.dev/guia-grpc-1/ 6064b5bf4f2e0673b0faa9b2 Ter, 20 Abr 2021 08:00:00 -0300

Este artigo é parte de uma série

  1. O guia completo do gRPC parte 1: O que é gRPC?
  2. O guia completo do gRPC parte 2: Mãos à obra com JavaScript
  3. O guia completo do gRPC parte 3: Tipos em tudo com TypeScript!
  4. O guia completo do gRPC parte 4: Streams

Quem me segue há algum tempo sabe que eu sou um grande fã de falar sobre novas tecnologias – também daquelas que não são tão novas assim – e, principalmente, sou um grande fã do gRPC!

Já fiz algumas palestras antes sobre o assunto, como você pode ver no vídeo a seguir (não deixe de ver os slides no meu SpeakerDeck) e este é um tema bastante recorrente para mim porque, pelo menos aqui no Brasil, a maioria das pessoas não sabe o que é ou nunca utilizou gRPC em nenhum projeto.

Meu vídeo sobre gRPC apresentado pelo HackBuddy

Porém, o gRPC não é uma tecnologia muito nova, ele já está aqui há algum tempo e já vem sendo utilizado em larga escala em projetos muito grandes como o Docker e o Kubernetes, então decidi montar esta série de artigos para explicar de uma vez por todas o que é o gRPC e como você consegue criar suas aplicações JavaScript e TypeScript com ele de forma simples e fácil!

Roadmap

Antes de começarmos com a informação em si, vamos entender o que iremos ver ao longo desta jornada. Dividi este guia em três partes, nesta primeira parte vamos passar pela história do gRPC, entender as ideias por trás da construção desta tecnologia, problemas, vantagens e muito mais.

Já na segunda parte, vamos por mais a mão na massa e construir nossa aplicação usando gRPC enquanto entendemos todo o ecossistemas e as ferramentas que compõem a aplicação. Tudo isso usando JavaScript.

Por fim, na terceira parte vamos modificar a aplicação e melhorá-la para usar TypeScript ao invés de JavaScript. Desta forma vamos ter a inferência nativa de tipos da nossa API e como podemos nos comunicar com todas as camadas de forma correta.

História

O gRPC foi criado pela Google como um projeto de código aberto em 2015 como uma melhoria em uma arquitetura de comunicação chamada de RPC (Remote Procedure Call).

O RPC é um modelo de comunicação que remonta desde meados dos anos 70 quando Bruce Jay Nelson em 1981, que trabalhava na Xerox PARC utilizou essa nomenclatura para descrever a comunicação entre dois processos dentro do mesmo sistema operacional – isso ainda é utilizado – porém, o modelo RPC é mais usado para comunicação de baixo nível, até que o Java implementou uma API chamada JRMI (Java Remote Method Invocation) que funciona basicamente da mesma forma que o gRPC funciona hoje em dia, porém de uma maneira mais voltada para métodos e classes e não para comunicação entre processos.

Nós vamos falar um pouco mais sobre a arquitetura de uma chamada gRPC nos próximos parágrafos.

O "g" no gRPC não significa Google, na verdade, ele não tem um significado único, ele muda de acordo com cada release do engine do gRPC. Existe até um documento mostrando todos os nomes que o "g" teve ao longo das versões.

A ideia base do gRPC era ser muito mais performático do que a sua contraparte ReST por ser baseado no HTTP/2 e utilizar uma Linguagem de Definição de Interfaces (IDL) conhecida como Protocol Buffers (protobuf). Este conjunto de ferramentas torna possível que o gRPC seja utilizado em diversas linguagens ao mesmo tempo com um overhead muito baixo enquanto continua sendo mais rápido e mais eficiente do que as demais arquiteturas de chamadas de rede.

Além disso, a chamada de um método remoto é, essencialmente, uma chamada comum de um método local, que é interceptada por um modelo local do objeto remoto e transformada em uma chamada de rede, ou seja, você está chamando um método local como se fosse um método remoto. Vamos ver um exemplo.

Exemplo de funcionamento

Vamos mostrar um exemplo de um servidor gRPC escrito em Node.js para o controle de livros, como falamos, o gRPC utiliza o protobuf, que vamos ver em mais detalhes nos próximos parágrafos, este é o nosso arquivo protobuf que gerou nosso serviço:


syntax = "proto3";
message Void {}

service NoteService {
  rpc List (Void) returns (NoteList);
  rpc Find (NoteId) returns (Note);
}

message NoteId {
  string id = 1;
}

message Note {
  string id = 1;
  string title = 2;
  string description = 3;
}

message NoteList {
  repeated Note notes = 1;
}

Nele estamos definindo toda a nossa API gRPC de forma simples, rápida e, o melhor de tudo, versionável. Agora podemos carregar nosso servidor com este código:

const grpc = require('grpc')
const NotesDefinition = grpc.load(require('path').resolve('../proto/notes.proto'))

const notes = [
  { id: '1', title: 'Note 1', description: 'Content 1' },
  { id: '2', title: 'Note 2', description: 'Content 2' }
]

function List (_, callback) {
  return callback(null, notes)
}

function Find ({ request: { id } }, callback) {
  return callback(null, notes.find((note) => note.id === id))
}

const server = new grpc.Server()
server.addService(NotesDefinition.NoteService.service, { List, Find })

server.bind('0.0.0.0:50051', grpc.ServerCredentials.createInsecure())
server.start()

E veja como nosso client fica simples nas chamadas:

  
const grpc = require('grpc')
const NotesDefinition = grpc.load(require('path').resolve('../proto/notes.proto'))

const client = new NotesDefinition.NoteService('localhost:50051', grpc.credentials.createInsecure())

client.list({}, (err, notes) => {
  if (err) throw err
  console.log(notes)
})

client.find(Math.floor(Math.random() * 2 + 1).toString(), (err, note) => {
  if (err) throw err
  if (!note.id) return console.log('Note not found')
  return console.log(note)
})

Veja que, basicamente as nossas chamadas são como se estivéssemos chamando um método de um objeto client local, e este método vai ser convertido em uma chamada de rede e enviado para o servidor, que irá receber a chamada e converter novamente em um objeto local e devolver a resposta.

Arquitetura

Arquiteturas RPC são muito parecidas. A ideia base é que temos sempre um servidor e um cliente, do lado do servidor temos uma camada que é chamada de skeleton, que é essencialmente um decriptador de uma chamada de rede para uma chamada de função, este é o responsável por chamar a função do lado do servidor.

Enquanto isso, do lado do cliente, temos uma chamada de rede feita por um stub, que é como um "falso" objeto representando o objeto do lado do servidor. Este objeto tem todos os métodos com suas assinaturas.

Estes nomes variam de implementação para implementação, no JRMI tínhamos skeleton e stub, porém a implementação do gRPC nomeia os dois lados como stubs.

Este é o diagrama de funcionamento de uma chamada RPC comum.

Diagrama de funcionamento do RPC

O gRPC tem um funcionamento muito próximo do diagrama que acabamos de ver, a diferença é que temos uma camada extra que é o framework gRPC interpretando as chamadas codificadas com a IDL do protobuf:

Diagrama de um serviço gRPC

Como você pode ver, o funcionamento é basicamente o mesmo, temos um cliente que converte as chamadas feitas localmente em chamadas de rede binárias com o protobuf e as envia pela rede até o servidor gRPC que as decodifica e responde para o cliente.

HTTP/2

O HTTP/2 já é utilizado faz algum tempo e vem se tornando a principal forma de comunicação na web desde 2015.

História do HTTP ao longo das décadas

Entre as muitas vantagens do HTTP/2 (que também foi criado pela Google), está o fato de que ele é muito mais rápido do que o HTTP/1.1 por conta de vários fatores que vamos entender.

Multiplexação de requests e respostas

Tradicionalmente, o HTTP não pode enviar mais de uma requisição por vez para um servidor, ou então receber mais de uma resposta na mesma conexão, isso torna o HTTP/1.1 mais lento, já que ele precisa criar uma nova conexão para cada requisição.

No HTTP/2 temos o que é chamado de multiplexação, que consiste em poder justamente receber várias respostas e enviar várias chamadas em uma mesma conexão. Isto só é possível por conta da criação de um novo frame no pacote HTTP chamado de Binary Framing. Este frame essencialmente separa as duas partes (headers e payload) da mensagem em dois frames separados, porém contidos na mesma mensagem dentro de um encoding específico.

Binary framing em ação

Compressão de headers

Outro fator que transforma o HTTP/2 em um protocolo mais rápido é a compressão de headers. Em alguns casos os headers de uma chamada HTTP podem ser maiores do que o seu payload, por isso o HTTP/2 tem uma técnica chamada HPack que faz um trabalho bastante interessante.

Inicialmente tudo na chamada é comprimido, inclusive os headers, isso ajuda na performance porque podemos trafegar os dados binários ao invés de texto. Além disso, o HTTP/2 mapeia os headers que vão e vem de cada lado da chamada, dessa forma é possível saber se os headers foram alterados ou se eles estão iguais aos da última chamada.

Se os headers foram alterados, somente os headers alterados são enviados, e os que não foram alterados recebem um índice para o valor anterior do header, evitando que headers sejam enviados repetidamente.

Compressão de headers em funcionamento

Como você pode ver, somente o path dessa requisição mudou, portanto só ele será enviado.

Protocol Buffers

Os protocol buffers (ou só protobuf), são um método de serialização e desserialização de dados que funciona através de uma linguagem de definição de interfaces (IDL).

Foi criado pela Google em 2008 para facilitar a comunicação entre microsserviços diversos. A grande vantagem do protobuf é que ele é agnóstico de plataforma, então você poderia escrever a especificação em uma linguagem neutra (o próprio proto) e compilar esse contrato para vários outros serviços, dessa forma a Google conseguiu unificar o desenvolvimento de diversos microsserviços utilizando uma linguagem única de contratos entre seus serviços.

O protobuf em si não contém nenhuma funcionalidade, ele é apenas um descritivo de um serviço. O serviço no gRPC é um conjunto de métodos, pense nele como se fosse uma classe. Então podemos descrever cada serviços com seus parâmetros, entradas e saídas.

Cada método (ou RPC) de um serviço só pode receber um único parâmetro de entrada e um de saída, por isso é importante podermos compor as mensagens de forma que elas formem um único componente.

Além disso, toda mensagem serializada com o protobuf é enviada em formato binário, de forma que a sua velocidade de transmissão para seu receptor é muito mais alta do que o texto puro, já que o binário ocupa menos banda e, como o dado é comprimido pelo HTTP/2, o uso de CPU também é muito menor.

Outra grande vantagem que contribui para o aumento da velocidade do protobuf é a separação de contexto e conteúdo. Quando estamos usando formatos como JSON, o contexto vem junto com a mensagem, por exemplo:

{
  "name": "Lucas",
  "age": 26
}

Quando convertemos isso para uma mensagem no formato protobuf, vamos ter o seguinte arquivo:

syntax = "proto3";

message Name {
  string name = 1;
  int32 age = 2;
}

Veja que não temos o header da mensagem junto da mensagem, apenas um índice informando qual é o local que aquele campo deve estar.

Encoding

Quando utilizamos o compilador do protobuf (chamado de protoc), podemos rodar o comando a seguir usando o nosso exemplo anterior: echo 'name: "Lucas";age: 26' | protoc --encode=Name name.proto > name.bin.

Isso vai criar um arquivo binário com o nome name.bin, se abrirmos o arquivo binário em um hex viewer (como o do VSCode), teremos a seguinte cadeia de bits:

0A 05 4C 75 63 61 73 10 1A

Temos 9 bytes representados aqui, contra os 24 do JSON, e isso é o suficiente para poder entender a mensagem, por exemplo, o que temos aqui é o seguinte:

Diagrama do encoding do protobuf
  • O primeiro byte 0A, diz o índice e o tipo da mensagem. 0A em decimal é 10, ou seja, 0000 1010 em binário, de acordo com a especificação de encoding do protobuf, os últimos três bits são reservados para o tipo e o MSB (primeiro bit da esquerda) pode ser descartado, então reagrupando os bits temos 0001 010, portanto nosso tipo é 010, que é 2 em binário, o numero que representa uma string no protobuf. Já no primeiro byte 0001 temos o índice do campo, que é 1, como definimos na nossa mensagem.
  • O byte seguinte 05 nos diz o tamanho desta string, que é de 5 bytes porque "Lucas" tem 5 letras.
  • Os 5 bytes seguintes 4C 75 63 61 73 são a string "Lucas" convertidas para hexadecimal e desconvertidas para UTF-8.
  • O penúltimo byte 10 é relativo ao segundo campo, se convertermos em binário o numero 10 teremos 0001 0000, como fizemos no primeiro campo, vamos agrupar os 3 bits da direita passando o zero mais a esquerda (4º bit da direita para a esquerda) para o grupo seguinte e removemos o MSB ficando 0010 000, ou seja, temos o tipo 0, que é varint, pelos últimos 3 bits, e o primeiro grupo nos dá 0010, ou 2 em binário, que é o índice do segundo campo.
  • O último bit é o valor deste varint, o valor 0x1A para binário é 0001 1010, então podemos somente converter para um decimal comum somando as potências de 2: 2 + 8 + 16 = 26, que é o valor que colocamos no segundo campo.

Então essencialmente, nossa mensagem é 125Lucas2026 , veja que temos 12 bytes aqui, mas no encoding temos apenas 9, isto porque dois bytes representam 2 valores ao mesmo tempo e temos apenas 1 byte para o número 26 enquanto usamos 2 para a string "26".

É possível usar o protobuf sem o gRPC?

Sim, uma das coisas mais legais no gRPC é que ele é um conjunto de ferramentas, que juntas, trabalham muito bem. Portanto o gRPC é um conjunto de HTTP/2 com protobuf e um sistema de chamadas remotas muito rápido.

Isso significa que podemos utilizar o compilador do protobuf para gerar um SDK de encoding, que vai permitir que você codifique e decodifique suas mensagens utilizando o protobuf.

Por exemplo, vamos criar um arquivo simples:

syntax = "proto3";
message Pessoa {
  uint64 id = 1;
  string email = 2;
}

Agora podemos executar a seguinte linha no nosso terminal para gerar um arquivo .js que conterá uma classe Pessoa com os setters e getters configurados, bem como os encoders e decoders:

mkdir -p dist && protoc --js_out=import_style=commonjs,binary:dist ./pessoa.proto

O compilador vai criar um arquivo pessoa_pb.js na pasta dist usando o modelo de importação CommonJS (isso é obrigatório se você for executar com Node.js), e ai podemos escrever um arquivo index.js:

const {Pessoa} = require('./pessoa_pb')

const p = new Pessoa()
p.setId(1)
p.setEmail('hello@lsantos.dev')

const serialized = p.serializeBinary()
console.log(serialized)

const deserialized = Pessoa.deserializeBinary(serialized)
console.table(deserialized.toObject())
console.log(deserialized)

Então vamos precisar instalar o protobuf com npm install google-protobuf e executamos o código:

Uint8Array(21) [
    8,   1,  18,  17, 104, 101,
  108, 108, 111,  64, 108, 115,
   97, 110, 116, 111, 115,  46,
  100, 101, 118
]
┌─────────┬─────────────────────┐
│ (index) │       Values        │
├─────────┼─────────────────────┤
│   id    │          1          │
│  email  │ 'hello@lsantos.dev' │
└─────────┴─────────────────────┘
{
  wrappers_: null,
  messageId_: undefined,
  arrayIndexOffset_: -1,
  array: [ 1, 'hello@lsantos.dev' ],
  pivot_: 1.7976931348623157e+308,
  convertedPrimitiveFields_: {}
}

Veja que temos um encoding igual ao que analizamos antes, uma tabela dos valores em objetos e a classe inteira.

Utilizar o protobuf como camada de contratos é muito útil, por exemplo, para padronizar as mensagens enviadas entre serviços de mensageria e entre microsserviços. Como estes serviços podem receber qualquer tipo de entrada, o protobuf acaba criando uma forma de garantir que todas as entradas sejam válidas.

Vantagens do gRPC

Como pudemos ver, o gRPC tem várias vantagens sobre o modelo ReST tradicional:

  1. Mais leve e mais rápido por utilizar codificação binária e HTTP/2
  2. Multi plataforma com a mesma interface de contratos
  3. Funciona em muitas plataformas com pouco ou nenhum overhead
  4. O código é auto documentado
  5. Implementação relativamente fácil depois do desenvolvimento inicial
  6. Excelente para trabalhos entre times que não vão se encontrar, principalmente para definir contratos de projetos open source.

Problemas

Assim como toda a tecnologia, o gRPC não é uma bala de prata e não resolve todos os problemas, temos alguns defeitos:

  1. O protobuf não possui um package manager para poder gerenciar as dependências entre arquivos de interface
  2. Exige uma pequena mudança de paradigma em relação ao modelo ReST
  3. Curva de aprendizado inicial é mais complexa
  4. Não é uma especificação conhecida por muitos
  5. Por conta de não ser muito conhecido, a documentação é esparsa
  6. A arquitetura de um sistema usando gRPC pode se tornar um pouco mais complexa

Casos de uso

Independente dos problemas e de tudo que a tecnologia tem para oferecer, temos uma série de casos de uso bem famosos no mundo open source que utiliza o gRPC como meio de comunicação.

Kubernetes

O Kubernetes em si utiliza o gRPC como meio de comunicação entre o Kubelet e os CRIs que compõe a plataforma de execução de containers (como já falamos em vários artigos, como este, este e este).

A facilidade de implementar uma interface usando o protobuf facilita a comunicação entre os times, ainda mais um time como o do Kubernetes que tem que suportar uma larga quantidade de provedores que não são nem conhecidos.

KEDA

O projeto KEDA, também para Kubernetes, utiliza como uma funcionalidade principal a capacidade de se criar external scalers usando uma interface gRPC para a comunicação com o operador principal.

Um dos projetos da CNCF no qual sou um contribuidor, o HTTP add on para o KEDA, utilizar este meio para criar um scaler externo que se comunica com o KEDA para aumentar a quantidade de pods em um cluster baseado na quantidade de requisições HTTP, como você pode ver aqui.

containerd

O principal runtime de containers da atualidade, o containerd é o projeto que dá vida ao Docker e ao Kubernetes atualmente. Ele também possui uma interface gRPC para comunicação com serviços externos.

Conclusão

Nesta primeira parte mergulhamos um pouco sobre como funciona e o que é o gRPC e seus componentes, nas próximas partes deste guia vamos construir algumas aplicações e mostrar o ecossistema de ferramentas que existe para esta tecnologia sensacional.

]]>
<![CDATA[ Hospedando suas imagens Docker em seu próprio registry ]]> https://blog.lsantos.dev/docker-registry-local/ 6074c08e33c5a2622339845d Ter, 13 Abr 2021 10:00:00 -0300 Uma das coisas mais legais do Docker é que você já tem "de fábrica" um repositório gigantesco de imagens que é o Docker Hub para poder baixar a hospedar suas imagens de forma pública (e até mesmo de forma privada, mediante a um pagamento).

Porém muitas vezes o Hub simplesmente não é uma das opções para você, isso pode acontecer quando você não quer tirar as imagens de dentro da sua rede local, do seu firewall ou até mesmo quando você precisa realizar um teste rápido.

Por este motivo o registry, que é a plataforma que utilizamos para armazenar as nossas imagens, possui uma vantagem grande que muitos não sabem, que é ser construido como uma imagem Docker ele mesmo.

Vamos aprender a criar o nosso próprio Docker Registry e armazenar nossas imagens localmente!

Hospedando o nosso próprio registry

Além das soluções como o Azure Container Registry, temos a possibilidade de subir a nossa própria instância do Docker registry para continuar a usufruir de uma experiência igual a que já temos com o Docker atualmente.

Primeiramente o que precisamos fazer é executar uma instância local do registry usando a imagem oficial. Podemos fazer isso com o seguinte comando:

docker run -d -p 5000:5000 --name docker-registry registry:2.7
Lembrando que o comando anterior vai somente executar o registry com um armazenamento efêmero interno, ou seja, você vai perder suas imagens se pausar o container.

Para executar um registry persistente podemos utilizar o mesmo comando, porém com a opção -v seu/caminho:/var/lib/registry setada antes do nome da imagem.

A partir de agora podemos mandar qualquer imagem para nosso registro local.

Trabalhando com imagens

Naturalmente, quando criamos um registry que só é acessível via localhost, como estamos fazendo aqui, não temos por que definir uma senha. Porém, a melhor prática é que, se você for criar um container que seja acessível por mais pessoas fora da sua máquina local, sempre utilize uma senha e autenticação com o seu registry.

Vamos aprender a fazer essa autenticação em outro artigo, porém agora vamos focar em como podemos enviar nossas imagens. Para este exemplo, escolha qualquer imagem do hub e baixe-a em sua máquina com o comando docker pull <imagem>, vou utilizar a imagem do Debian.

Toda a imagem Docker deve ser identificada pelo seu nome, de forma que o Docker saiba a qual registry ela se refere, isso pode ser feito na forma de um FQDN: url.do.registry:porta/repositorio/imagem:tag.

No nosso caso o registry está rodando localmente na porta 5000, o repositório seria como se fosse um grupo, então vamos criar um grupo chamado official e, por fim, a imagem é a do Debian. Logo, nossa imagem terá o nome:

localhost:5000/official/debian:latest

Vamos taggear a nossa imagem com o comando docker tag para poder alterar o nome dela:

docker tag debian localhost:5000/official/debian:latest

O comando não vai dar nenhuma saída, mas poderemos ver a imagem em docker image ls, bem como agora podemos enviar a imagem para o registry com docker push localhost:5000/official/debian:latest.

Se buscarmos os logs do container com docker logs registry veremos que o nosso registry recebeu a imagem:

Veja a última linha com o PUT

Se removermos nossa imagem local com docker rmi localhost:5000/official/debian:latest, vamos poder utilizar o comando docker pull para poder buscar a imagem do nosso registry local e baixá-la novamente:

docker pull localhost:5000/official/debian

Conclusão

O uso do registry é bastante simples, nos próximos artigos vamos explorar como podemos amplificar o uso e deixar nosso registry ainda mais seguro!

Este foi um artigo curto porém que demonstra uma funcionalidade que poucas pessoas sabem sobre o funcionamento do Docker.

Não esqueça de assinar a newsletter e me seguir nas redes sociais para mais conteúdo 😍

]]>
<![CDATA[ Integrando o Azure Active Directory no AKS ]]> https://blog.lsantos.dev/azure-ad-aks/ 6052603c1bc18e145bb41ffc Ter, 06 Abr 2021 10:00:00 -0300 Já passamos por artigos sobre como criar usuários e também como atribuir permissões a estes usuários usando RBAC. Porém, utilizar o Kubernetes para poder realizar gerenciamentos de usuários, apesar de simples, não é muito prático justamente por conta da natureza distribuída dos clusters.

Quando criamos um cluster, temos duas boas práticas de segurança que devemos considerar. A primeira é o controle que você tem sobre os recursos da própria Azure, como o próprio cluster e todos os objetos que estão dentro do resource group dele, a outra boa prática é o controle sobre o que seus usuários podem fazer e ver dentro desse cluster.

Azure AD

Uma opção sensacional, provavelmente a melhor para o AKS, para centralizar o gerenciamento de usuários é o chamado Azure Active Directory (AAD). Essencialmente esta é uma integração com o Active Directory nativo da Azure de forma, centralizando o controle.

Utilizar esta opção pode ser uma das melhores práticas porque temos todo o controle sobre tanto os usuários quanto os seus roles e bindings dentro do mesmo local, o portal da Azure.

O AAD funciona sempre que o usuário requisita o arquivo de configuração através do comando az aks get-credentials. Quando isso acontece, existem dois principais roles que são enviados de volta:

  • Role administrativo: Acesso completo a todos os recursos
  • Role de usuário: Acesso restrito à recursos

Quando temos a integração com o AAD habilitada, durante o primeiro download de configurações, o usuário vai ter que logar no portal, a partir daí o perfil correspondente será baixado.

Como funciona

Para entendermos melhor como o AAD funciona, o ideal é entendermos o fluxo completo de como uma integração com active directory funciona em si.

De acordo com a própria documentação da Microsoft, a integração com o AAD provê a usuários e grupos o acesso aos recursos do cluster independentemente se os mesmos estiverem em um namespace ou não.

Quando um usuário faz o login e baixa o arquivo kubeconfig com o comando que vimos anteriormente, o primeiro passo do fluxo é pedir que o usuário se autentique no portal.

Após a autenticação no portal, o cluster verifica no servidor de AD da azure se o usuário existe e pede um token de acesso para o mesmo, este token é utilizado para preencher o arquivo kubeconfig.

Depois do download do arquivo config, o usuário não vai ter as permissões direto no token, mas vai estar integrado no webook do AAD e no servidor de API. Sempre que o usuário executar qualquer comando no Kubectl, um interceptador de chamadas vai usar este token para validar o usuário contra os servidores da Azure e verificar se ele tem permissão para executar o comando.

Se tanto o token JWT quanto o resultado da query na API do MS Graph mostrarem que o usuário tem existe e tem permissão, então a chamada procede para a API do kubernetes que utiliza os próprios Roles e Bindings para verificar se o usuário tem acesso.

O fluxo completo seria mais ou menos assim:

Integrando seu cluster com o AAD

Antes de junho de 2020, precisávamos criar todas as etapas de um servidor AD padrão, isso incluia a criação de uma aplicação de servidor e um client dentro do portal para integrar com a aplicação do AAD.

Agora tudo ficou muito mais simples com a chegada do Azure Managed AAD Integration (Managed AAD). Que, basicamente, abstrai todos os comandos que você teria que executar.

Por mais que seja mais simples, essa facilidade inclui algumas limitações

O primeiro passo para habilitar o Managed AAD no seu cluster é instalar as duas ferramentas mais famosas, o kubectl e o kubelogin, por sorte ambas podem ser instaladas usando o comando az aks install-cli.

Vamos precisar ter um grupo e um usuário iniciais para podermos ter privilégios administrativos para o primeiro usuário do cluster, então podemos usar um grupo existente ou então criar um novo com o seguinte comando:

az ad group create --display-name AKSAdminGroup --mail-nickname AKSAdminGroup

Isso vai te retornar uma série de informações, porém a mais importante delas é o ObjectID. Copie essa informação e guarde, porque vamos precisar dela para nos adicionar ao grupo. Mas para isso vamos precisar descobrir o nosso próprio ID:

az ad user show --id email@delogin.com --query objectId -o tsv

A informação que voltar é o seu ID de usuário. Guarde-o para podermos nos adicionar ao grupo. Mas temos um problema, o email de login não é o mesmo email que o AD pode estar usando, então precisamos achar esse email. Para isso vamos usar o comando de listagem de usuários:

az ad user list --query "[*].{name: displayName, id: userPrincipalName, objectId: objectId}" -o json

Isso te retorna uma lista de todos os usuários e seus IDs de login, você poe então filtrá-los através da chave --id ou então só copiar o ObjectID, porque é só o que é preciso para poder se adicionar no grupo. Então vamos fazer isso:

az ad group member add --group AKSAdminGroup --member-id seuobjectid

Agora já temos nosso usuário dentro do grupo de permissão geral, vamos criar o nosso cluster habilitado:

az aks create \
  -g aks-aad \
  -n aad-cluster \
  --enable-aad \
  --aad-admin-group-object-ids objectIdDoGrupoAdmin

Pegamos as credenciais com az aks get-credentials -g aks-aad -n aad-cluster. Agora que temos o arquivo de configuração, tente executar qualquer comando no kubectl, como kubectl get nodes, veja que você vai receber uma mensagem pedindo para logar no portal.

O AAD já está funcionando, partindo sempre do princípio de Zero Trust, você nunca vai ter nenhuma permissão, somente as que você adicionar.

Adicionando novos usuários e grupos

Agora que temos todas as permissões, vamos adicionar novos usuários e grupos assim como fizemos nos outros artigos. Primeiro precisamos do ID do nosso cluster AKS:

AKSID=$(az aks show -g aks-aad -n aad-cluster --query id -o tsv)

Agora vamos criar um grupo somente leitura chamado AKSReadOnlyGroup e vamos salvar o ObjectID:

GROUPID=$(az ad group create \
  --display-name AKSReadOnlyGroup \
  --mail-nickname \
  --query objectId -o tsv)
 

Esses usuários precisam se logar como usuários e não como administradores. Isso pode ser feito adicionando este grupo a um role existente no AD chamado "Azure Kubernetes Service Cluster User Role". Como vimos antes, isso vai fazer com que os usuários sejam logados como usuários e não como administradores.

O objeto que liga um grupo a um role na Azure é chamado de Role Assignment. Vamos criar um para podermos linkar os dois:

az role assignment create \
  --assignee $GROUPID \
  --role "Azure Kubernetes Service Cluster User Role" \
  --scope $AKSID

Vamos adicionar um novo usuário chamado Alice a este grupo que acabamos de criar, vamos então copiar o ObjectID quando criarmos:

ALICE=$(az ad user create \
  --display-name "Alice Doe" \
  --user-principal-name alice@dominio.com \
  --password S3gr3d0 \
  --query objectid -o tsv)
Lembre-se: Quando criarmos um usuário precisamos de um domínio válido. Para obter este domínio podemos executar o seguinte comando e copiar tudo o que vier depois do "@": az ad user list --query "[*].userPrincipalName" -o json

Adicionamos o usuário ao grupo:

az ad group member add --group AKSReadOnlyGroup --member-id $ALICE

Agora que temos o usuário já criado no AD, vamos criar o role e o binding dele no kubernetes. Como vimos antes, o AAD e o RBAC do kubernetes trabalham juntos para poder prover a solução completa de autenticação.

Dentro do Kubernetes

Primeiro vamos criar o role chamado ReadOnlyRole:

kubectl create clusterrole ReadOnlyRole --verb=get,list,watch --resource="*"

Vamos criar agora o binding deste role ao nosso ID do grupo no AD:

kubectl create clusterrolebinding ReadOnlyBinding \
  --clusterrole=ReadOnlyRole \
  --group=$GROUPID
Podemos também vincular o role a um usuário utilizando o ObjectID do usuário ao invés do ObjectID do grupo no comando acima.

Agora podemos testar essas roles baixando o arquivo de configuração novamente e sobrescrevendo o atual:

az aks get-credentials -g aks-aad -n aad-cluster --overwrite-existing

Na primeira chamada, você precisará fazer o login novamente com email e senha. Vamos usar o email da Alice para fazer isso (o email que você usou quando criou o usuário da Alice), assim como a senha definida.

Depois de um login bem sucedido, tente executar uma lista de pods do namespace kube-system. Você vai conseguir porque o usuário da Alice tem permissão para listar todos os pods em todos os namespaces, porém se você tentar criar qualquer coisa, como:

kubectl run nginx-dev --image nginx --namespace default

Você vai receber uma mensagem de "proibido".

Conclusão

Aprendemos como podemos utilizar ainda mais do poder da Azure para integrar autenticações e mais facilidades no nosso cluster AKS. Com esse tipo de integração podemos deixar nosso cluster ainda mais seguro.

Até mais!

]]>
<![CDATA[ Armazenando seus Helm Charts no Azure Container Registry ]]> https://blog.lsantos.dev/armazenando-seus-helm-charts-no-azure-container-registry/ 6049329ed3e9b63aad1b9447 Ter, 30 Mar 2021 10:00:00 -0300 Uma das grandes vantagens de entendermos como funciona o ecossistema de containers é que podemos entender quando podemos utilizar o mesmo padrão para diversas especificações diferentes.

No ano passado, o Helm anunciou que estaria dando suporte aos OCI Artifacts, que nada mais são do que uma especificação aberta da OCI para distribuição de imagens de containers e outros tipos de dados, chamados de artefatos. Essa especificação, assim como todas as demais especificações OCI são agnósticas de provedores, ferramentas ou clouds, o que as torna uma ferramenta fantástica para se trabalhar.

Registros de Containers

Um container registry, ou um registro, é algo que todo mundo que já teve que se envolver com containers teve que utilizar. O CR é o local onde armazenamos nossas imagens dos containers para que possamos buscá-las de qualquer lugar quando quisermos.

Em essência, uma imagem é basicamente um conjunto de arquivos que segue uma estrutura mais ou menos parecida com esta:

       ├── blobs
       │   └── sha256
       │       ├── 1b251d38cfe948dfc0a5745b7af5ca574ecb61e52aed10b19039db3...
       │       ├── 31fb454efb3c69fafe53672598006790122269a1b3b458607dbe106...
       │       └── 8ec7c0f2f6860037c19b54c3cfbab48d9b4b21b485a93d87b64690f...
       ├── index.json
       └── oci-layout

O arquivo index.json é a lista de todos os manifestos disponíveis ali, ou seja, é a lista de todas as imagens disponíveis naquele local. No nosso caso é uma lista de todos os charts do helm que estão armazenados lá.

Cada arquivo dentro de blobs/sha256 é um JSON que identifica um artefato, seja ele uma imagem ou um chart. Este JSON é compatível com a especificação da OCI para os arquivos SHA. Em suma, eles são uma lista de configurações descrevendo as características do blob, suas configurações, propriedades, camadas do sistema de arquivos e também os comandos iniciais. No caso de um Chart do Helm temos o seguinte arquivo:

{
  "schemaVersion": 2,
  "config": {
    "mediaType": "application/vnd.cncf.helm.config.v1+json",
    "digest": "sha256:8ec7c0f2f6860037c19b54c3cfbab48d9b4b21b485a93d87b64690fdb68c2111",
    "size": 117
  },
  "layers": [
    {
      "mediaType": "application/tar+gzip",
      "digest": "sha256:1b251d38cfe948dfc0a5745b7af5ca574ecb61e52aed10b19039db39af6e1617",
      "size": 2487
    }
  ]
}

Perceba que temos uma diferenciação de mediaType, enquanto uma imagem Docker comum tem um tipo application/vnd.oci.image.config.v1+json, temos aqui um tipo application/vnd.cncf.helm.config, o mesmo vale para os layers, cada layer de uma imagem OCI é do tipo application/vnd.oci.image.layer.v1.tar+gzip, enquanto aqui temos apenas o formato .tar.gz.

Armazenando Charts Localmente

Armazenar charts em um registro local é bastante simples, assumindo que você já tenha o Helm 3 instalado, vamos primeiramente habilitar a flag que diz que podemos utilizar os artefatos OCI, uma vez que o suporte a eles ainda é experimental. Para isso é só exportar uma variável no seu shell:

export HELM_EXPERIMENTAL_OCI=1

Agora, vamos executar um registry do Docker localmente com:

docker un -dp 5000:5000 --name docker-registry registry

A partir daí já temos um repositório oficial rodando em nossa máquina e podemos enviar nossos charts para ele. Como exemplo, vou utilizar um chart de um repositório meu chamado Zaqar. Depois de clonar o repositório para poder ter mais controle, vou entrar na pasta helm e vou rodar o comando helm chart save zaqar localhost:5000/zaqar/zaqar:2.1.3.

Isso vai fazer meu chart ser salvo localmente, da mesma forma que fazemos com o docker pull. Agora posso enviar meu chart salvo para o repositório especificado pelo nome com helm chart push localhost:5000/zaqar/zaqar:2.1.3.

Se quisermos instalar o chart que baixamos, primeiramente temos que exportá-lo do cache usando helm chart export localhost:5000/zaqar/zaqar:2.1.3 -d <destino>, e então rodar o comando helm install <nome> ./<destino>.

Hospedando charts no Azure Container Registry

Agora que já sabemos como é hospedar um chart em um repositório local, hospedar no Azure CR é quase a mesma coisa. Temos que ter um acesso ao azure via Azure CLI, depois que fizermos o login, temos quase o mesmo conjunto de comandos.

Vou assumir que você já tem o Azure CLI, então vamos criar o nosso ACR. Primeiro temos que criar o nosso resource group com az group create -n helm-reg -l eastus, e então o ACR com o comando az acr create -n chartregistry$RANDOM -g helm-reg --sku Basic -o tsv --query loginServer.

Uma dica é guardar o nome do repositório em uma variável:

export ACR=$(az acr create -n chartregistry$RANDOM -g helm-reg --sku Basic -o tsv --query loginServer)

Agora vamos fazer o login no nosso registry usando as chaves gerenciadas da Azure, porém precisamos habilitar o controle administrativo com az acr update -n $ACR --admin-enabled true. Agora podemos executar dois comandos para buscar as credenciais de login e salvá-las em nosso shell:

export ACRUSER=$(az acr credential show -n $ACR --query username -o tsv)
export ACRPASS=$(az acr credential show -n $ACR --query 'passwords[0].value' -o tsv)

Agora podemos logar no nosso registry com o Helm usando helm registry login $ACR --username $ACRUSER --password $ACRPASS, e a partir daqui já temos nosso registry configurado, vamos criar outro artifact com helm chart save zaqar $ACR/zaqar:2.1.3. Depois vamos dar um push com helm chart push $ACR/zaqar:2.1.3.

Uma vez que ele estiver lá, vamos poder listar tudo que existe no repositório com um comando do Azure CLI:

az acr repository show -n $ACR --repository zaqar

Perceba que vamos ter uma saída que é exatamente o que enviamos:

{
  "changeableAttributes": {
    "deleteEnabled": true,
    "listEnabled": true,
    "readEnabled": true,
    "writeEnabled": true
  },
  "createdTime": "2021-03-16T20:56:49.6118202Z",
  "imageName": "zaqar",
  "lastUpdateTime": "2021-03-16T20:56:49.7812323Z",
  "manifestCount": 1,
  "registry": "chartregistry23657.azurecr.io",
  "tagCount": 1
}

Podemos também pegar mais detalhes com o comando show-manifests, adicionando um --detail:

az acr repository show-manifests -n $ACR --repository zaqar --detail

Isso vai nos dar exatamente a definição de um artefato OCI que vimos no primeiro exemplo:

[
  {
    "changeableAttributes": {
      "deleteEnabled": true,
      "listEnabled": true,
      "quarantineState": "Passed",
      "readEnabled": true,
      "writeEnabled": true
    },
    "configMediaType": "application/vnd.cncf.helm.config.v1+json",
    "createdTime": "2021-03-16T20:56:49.7213057Z",
    "digest": "sha256:4780713fa23d7144d356c353795b5b84e66ad2b8bbd47c7118b4b85435d50bbc",
    "imageSize": 1378,
    "lastUpdateTime": "2021-03-16T20:56:49.7213057Z",
    "mediaType": "application/vnd.oci.image.manifest.v1+json",
    "tags": [
      "2.1.3"
    ]
  }
]

Para instalar podemos realizar os mesmos passos que tínhamos feito no registro local, depois de executar helm char remove $ACR/zaqar:2.1.3, para remover localmente já que temos ele aqui do nosso exercício anterior:

  1. helm chart pull $ACR/zaqar:2.1.3
  2. helm chart export $ACR/zaqar:2.1.3 -d ./destino
  3. helm install zaqar-acr ./destino

Conclusão

Utilizar o Helm já era fácil, um dos grandes problemas que tínhamos era que não havia uma forma "simples" de hospedar um chart em algum lugar quando queríamos algum tipo de registro privado. Embora o Helm possua excelentes ferramentas como o Chart Museum, elas ainda não são completamente padrões e, para um desenvolvimento distribuído de forma fácil, é essencial que tenhamos padrões abertos que o mercado possa seguir como um todo.

Lembrando que esta funcionalidade ainda é experimental (pelo menos enquanto escrevo este artigo), ela deverá ser publicada em breve e então você não precisará mais utilizar a variável de ambiente para controlar o comportamento.

Ser experimental não significa que você não pode usá-la para armazenar seus charts, apenas significa que a API poderá mudar drasticamente tendo até mesmo alguns de seus comandos completamente alterados, mas este é um forte indício de que o ecossistema de containers está, cada vez mais, caminhando para um local único.

Até mais!

]]>
<![CDATA[ #DockerSP - Entendendo o ecossistema de containers além do Docker ]]> https://blog.lsantos.dev/dockersp-entendendo-o-ecossistema-de-containers-alem-do-docker/ 605d24a43fb9b227b4274025 Qui, 25 Mar 2021 21:20:12 -0300 Hoje tive o grande prazer de estar com a galera do DockerSP para falar um pouco mais sobre o ecossistema de containers! Neste vídeo vamos dar uma pincelada mais profunda sobre o que falei neste post, dando um pouco mais de contexto e história!

Veja os slides

Logo mais estarei colocando mais conteúdo aqui sobre como podemos conectar os mundos de containers e desenvolvimento!

]]>
<![CDATA[ Gerencie seus dotfiles de qualquer lugar com Git ]]> https://blog.lsantos.dev/dotfiles-git/ 603f9e917ceb302965135bc6 Ter, 23 Mar 2021 10:00:00 -0300 Realizar a configuração de um computador novo é uma das tarefas que, ao mesmo tempo, são super legais e super chatas. Todos nós provavelmente já tivemos que trocar de computador e reconfigurar todos os nossos arquivos.

Isto é um pouco trabalhoso para todas as pessoas, porém é ainda mais trabalhoso para devs. Temos uma série de arquivos de configuração de ambiente, variáveis, configurações de binários e os nossos preciosos shells que precisam estar configurados da maneira como queremos para que possamos ser os mais produtivos possíveis!

Vamos entender o que são os dotfiles e como você vai ser muito mais produtivo (e sua sanidade vai melhorar demais) quando você começar a versioná-los usando nosso querido Git.

Dotfiles? É de comer?

Dotfiles é o nome que foi dado ao conjunto de arquivos ocultos utilizados para armazenar a configuração de estado ou então a configuração de preferências de uma ferramenta.

O termo "dotfiles" vem, como a maioria das coisas em computação, dos antigos kernels do unix  que adotava como prática a adição do prefixo . na frente do nome do arquivo para transformar, por padrão, aquele arquivo em um arquivo oculto, ou seja, que não era mostrado na listagem do ls. Por exemplo, /home/.hushlogin é um arquivo comum do Linux hoje em dia para remover a mensagem de login quando se faz um acesso via SSH.

Como qualquer sistema baseado no unix tem uma relação muito grande com arquivos, já que a maioria dos módulos e até mesmo dispositivos ou interfaces de rede são mostrados como arquivos dentro do sistema de arquivos, eles acabaram tendo uma grande importância para o desenvolvimento de software.

Hoje em dia, a grande maioria das ferramentas utilizam dotfiles para manter arquivos de configuração:

  • Bash: usa o .bashrc
  • Git: .gitconfig, .gitexcludes
  • Vim: .vimrc

E muitos outros.

O problema dos dotfiles

O grande problema dos dotfiles é que eles são locais. Ou seja, sempre que você troca de máquina fisicamente você acaba tendo que configurar tudo de novo.

Existem vários projetos para solucionar o problema de como podemos manter os nossos dotfiles sincronizados em máquinas que possuem o mesmo sistema operacional. Pessoalmente, eu não gosto de nenhuma e prefiro realizar as minhas configurações manualmente, porém pode ser uma ótima ideia ter uma ferramenta para poder auxiliar a realizar não só o armazenamento dos dotfiles, mas também a execução e o link de todos eles na hora de colocá-los para funcionar.

Gerenciando seus dotfiles

Para começar, crie um novo repositório no seu GitHub, geralmente chamamos de dotfiles. Você também pode utilizar alguns repositórios de outras pessoas como inspiração, os meus dotfiles estão neste repositório, embora um pouco desatualizados.

É importante dizer que, se seus arquivos contém algum tipo de dado sensível, como senhas, chaves e etc, então talvez você precise fazer o gerenciamento deles em um repositório privado, ou então utilizar tutoriais como este para fazer com que eles sejam criptografados.

Mas então caímos na pergunta: Quais dotfiles devemos salvar? A resposta é todos. Todas as configuraçòes que puderem ser transformadas em arquivos e salvas no Git devem ser feitas. No meu caso eu tenho grande parte dos arquivos de configuração do Git, Vim e ZSH salvos como configurações no meu repositório.

Além disso, em computadores com sistema MacOS, é possível salvar as configurações utilizando um arquivo .macos, que ficou super famoso através dos dotfiles do Mathias Bynens, estes arquivos permitem que você tenha um setup consistente entre diversos sitemas MacOS.

Ainda para usuários Mac, temos o Mackup que é uma pequena ferramenta que armazena suas configurações de aplicações em um local seguro e depois permite o restore das mesmas em um novo computador. Bem como o BrewBundle que é uma ferramenta que permite que você descreva de forma declarativa quais são os apps instalados através do HomeBrew.

Para os próximos passos, vamos precisar realizar alguns links, então você não pode ter seus arquivos nos locais originais. Portanto se você já criou o repositório, é melhor mover os arquivos do que copiá-los.

Depois de mover todos os arquivos para uma estrutura que você se sinta feliz no seu repositório, você pode utilizar a funcionalidade de hard links dos sistemas baseados em Unix para podermos criar uma conexão entre o nosso arquivo original e o local aonde o dotfile vai existir.

Aqui entramos em uma discussão sobre se devemos criar um soft link (ou symbolic link) ou então um hard link, pessoalmente (e também nos meus dotfiles) eu crio um hard link entre os arquivos.

A maior diferença entre eles, no entanto, é o fato de que hard links vão ser um ponteiro para o arquivo em si, ou seja, é um nome diferente para o mesmo arquivo original que independe de qualquer outro recurso do sistema, é como se estivéssemos falando que um arquivo tem múltiplos nomes.

Isso cria uma facilidade grande quando temos que fazer o backup desses arquivos, porque hard links são ponteiros diretos para o conteúdo do arquivo original, então se alterarmos o hard link, vamos alterar o arquivo original também, você pode realizar um teste fazendo o seguinte:

export temp=$(mktemp -d)
touch $temp/original
ln $temp/original ~/hardlink

Agora edite o arquivo presente em ~/hardlink, que é o link em si, e execute cat $temp/original, veja que o conteúdo está presente lá também.

Sempre ao deletar o arquivo original, você também precisará deletar o link, então não adianta executar somente rm -rf $temp, é necessário rodar rm -rf ~/hardlink também.

Soft links não permitem que isso seja feito, uma vez que eles são apenas arquivos que apontam para outros arquivos. Portanto, para dotfiles eu prefiro muito mais ter um hardlink que me permite editar diretamente o meu dotfile aonde quer que ele esteja e essas mudanças são refletidas no meu repositório Git.

Agora que você já tem o repositório com os arquivos, o que falta é somente fazer o link deles para as localizações originais.

Uma das coisas que eu gosto de fazer quando organizo meus dotfiles é manter a estrutura de pastas original para que eu saiba aonde colocá-los depois, você pode fazer isso se quiser.

Para isso vamos usar o comando ln, a sintaxe deste comando é:

ln <arquivo original> <localização do link>

É importante dizer que a localização do link talvez precise ser um caminho absoluto, ou seja, não poderá ser um caminho do tipo ../ ou ~/, em alguns sistemas.

Faça um hard link para cada um dos seus arquivos dotfiles para a localização original deles, por exemplo:

ln ~/meus-dotfiles/home/.gitconfig ~/.gitconfig

O comando não exibe nenhuma saída se tudo correu bem, então a forma de identificar é verificando se, ao executar um comando ls -la no seu diretório de destino, o arquivo está presente lá também.

No caso de links simbólicos, ao executar uma listagem, a saída do comando mostra um caminho do tipo arquivo -> arquivo original

Conclusão

Agora, quando você estiver em uma nova máquina, basta clonar o repositório com seus dotfiles e executar o linking de cada um deles.

Você pode facilitar isso com um script de bootstrap, ou até mesmo ferramentas de bootstrap que podem inclusive baixar e instalar programas dependentes (como o HomeBrew) para que você só precise ligar a nova máquina, executar um único comando e nunca mais terá de configurar suas preferências novamente.

Se você quiser saber mais sobre dotfiles, de uma olhada neste site que contém uma série de ferramentas para colocar seus dotfiles seguramente no ar com o GitHub, também veja artigos como este, este e este para ter uma ideia de como você pode organizar e preparar os seus repositórios para receber os dotfiles da melhor forma possível.

]]>
<![CDATA[ Como ter controle do seu cluster Kubernetes com affinity e tolerations ]]> https://blog.lsantos.dev/kubernetes-affinity-taints-tolerations/ 5f510efa82fe718273265c4b Ter, 02 Mar 2021 09:00:00 -0300 Quando estamos trabalhando com clusters Kubernetes, é comum termos aplicações que precisam estar em nós específicos. Isso é ainda mais comum quando temos uma série de nós que fazem parte do nosso cluster através de Node Pools.

Neste artigo vamos aprender a controlar nossos nós com seletores, vamos aprender o conceito de Node Pools e também o conceito de Node Affinity. Por fim vamos entender todo a ideia por trás de taints e tolerations. E, com isso, vamos aprender a ter o controle completo do nosso cluster e de onde queremos nossas aplicações.

Como este artigo já assume que você conhece um pouco mais de Kubernetes, não vou dar as bases para entendê-lo, mas se você quiser entender um pouco mais, veja meu curso gratuito de Kubernetes no Channel9 e, para se aprofundar mais, dê uma olhada no meu livro de Kubernetes da Casa do Código.

Por que precisamos de controle?

Por padrão, o Kubernetes tem um scheduler que faz uma separação muito boa do seus pods e containers, por exemplo, ele vai sempre tentar fazer uma distribuição por igual de todas as aplicações pelo cluster, evitando colocar aplicações que precisam de mais recursos do que o nó atualmente possui.

Em geral, essa é uma boa separação e uma boa estratégia, porém muitas vezes temos a necessidade de ter mais de um tipo de máquina para nossas aplicações, e é ai que temos que ter o controle exato de onde precisamos colocar nossas aplicações. Por exemplo, para aplicações de Machine Learning, por exemplo, as melhores máquinas são as que possuem uma GPU integrada, dessa forma podemos ter mais de uma node pool com máquinas diferentes.

Porém não podemos colocar todas as nossas aplicações dentro de um nó com GPU, porque se não esgotaríamos o nó sem ter as aplicações que realmente precisam dele. Da mesma forma, não podemos ter só nós de GPU porque essas máquinas são muito caras.

Então é ai que entra o conceito de seletores.

Node Selectors

Todo a forma de restringir uma aplicação a algum nó é chamado de node constraint. A forma mais simples de criar uma restrição é através de um seletor de nós. Com esta técnica, você essencialmente obriga um Pod a ser registrado para rodar dentro de um nó específico.

Como qualquer outro recurso do Kubernetes, os nós também permitem o agrupamento e taggeamento através de labels. Uma label é um par de chave e valor que pode ser criado a sua escolha. Será através destes pares que você vai restringir uma aplicação.

Criando uma label para um nó

Quando criamos um nó em um cluster Kubernetes gerenciado, como o AKS, podemos ver que a própria cloud já coloca algumas labels nestes nodes, além destas, existem outras labels conhecidas e comuns que são adicionadas a todos os nós por padrão em um cluster.

Podemos obter essa informação com o comando kubectl describe nodes {nome}, como podemos ver neste nó em um cluster que criei:

Labels:             agentpool=nodepool1
                    beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/instance-type=Standard_B2s
                    beta.kubernetes.io/os=linux
                    failure-domain.beta.kubernetes.io/region=eastus
                    failure-domain.beta.kubernetes.io/zone=0
                    kubernetes.azure.com/cluster=MC_keda_keda_eastus
                    kubernetes.azure.com/mode=system
                    kubernetes.azure.com/node-image-version=AKSUbuntu-1804-2021.01.06
                    kubernetes.azure.com/role=agent
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=aks-nodepool1-24389357-vmss000000
                    kubernetes.io/os=linux
                    kubernetes.io/role=agent
                    node-role.kubernetes.io/agent=
                    node.kubernetes.io/instance-type=Standard_B2s
                    storageprofile=managed
                    storagetier=Premium_LRS
                    topology.kubernetes.io/region=eastus
                    topology.kubernetes.io/zone=0

Vamos criar uma nova label para este nó, vamos dizer que ele possui uma GPU através de uma label processingtype=gpu, para isso vamos usar o comando label:

kubectl label nodes {nome do nó} processingtype=gpu
Se quisermos alterar uma label já existente podemos adicionar a flag --overwrite com o nome da label, por exemplo, se quisermos alterar o valor da label processingtype para CPU podemos rodar kubectl label nodes {nome} processingtype=cpu --overwrite

É importante notar que labels só podem ter um valor por chave, ou seja, não podemos ter duas chaves processingtype com dois valores diferentes.

Usando Node Selectors

Agora que temos a aplicação das labels em um nó, vamos criar um Pod simples.

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx

Agora podemos adicionar uma outra chave dentro de spec que especifica que queremos um seletor para que a aplicação execute somente em nós que possuam a chave processingtype com o valor gpu:

apiVersion: v1
kind: Pod
metadata:
  name: nginx
  labels:
    env: test
spec:
  containers:
  - name: nginx
    image: nginx
  nodeSelector:
    processingtype: gpu

Agora esta aplicação sempre será enviada para este nó.

Node Affinity

A Node Affinity é um outro conceito muito próximo do que acabamos de falar como Node Selectors. A diferença é a sintaxe um pouco mais expressiva que permite que você crie seletores mais complexos com modelos label in (valor, valor) ou label=valor.

A principal diferença entre os dois é que temos uma outra chave específica para a affinity dentro de uma spec de um Pod. Nesta spec vamos ter dois tipos de affinity:

  • requiredDuringSchedulingIgnoredDuringExecution: Pense nisso como um node selector, os pods com essa chave vão ter que estar obrigatoriamente em um nó que seja compatível com as descrições dessa affinity.
  • preferredDuringSchedulngIgnoredDuringExecution: Esta é uma forma mais "soft" do anterior. Ela diz que, basicamente, o pod tentará ser executado em um nó com estas labels, mas se não houver nenhum disponível, então ele será executado em outro nó.

Os dois podem coexistir no mesmo pod, e é importante se atentar para a parte IgnoredDuringExecution, isso significa que, se um nó perder uma determinada label que permite que uma série de pods rodem nele, estes pods continuam existindo até que eles sejam recriados.

Veja um exemplo de Pod que vai obrigatoriamente criar os containers dentro das zonas 1 ou 2 e vai preferir ambientes Linux, mas se não houverem, então podem ser executados em outros SOs:

apiVersion: v1
kind: Pod
metadata:
  name: node-affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - 1
            - 2
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 1
        preference:
          matchExpressions:
          - key: kubernetes.io/os
            operator: In
            values:
            - Linux
  containers:
  - name: affinity
    image: khaosdoctor/go-vote-api
Você pode usar os operadores In, NotIn, Exists, DoesNotExist, Gt, Lt. Usando o NotIn e DoesNotExist podemos alcançar o que é chamado de anti-affinity, que é para repelirmos um Pod de executar em determinados nós

Peso

A chave weight é uma opção bastante interessante porque podemos utilizar o peso para que um nó tenha mais prioridade do que outros. O que acontece é que, quando o scheduler passar por estas regras, ele irá somar os weight de todas as regras de um nó que são satisfeitas pelas labels deste nó. Os nós que obtiverem a soma mais alta, serão os preferidos para ter este container.

Taints e Tolerations

Sabemos que affinity é uma propriedade de um Pod que atrai este pod para um grupo de nós. Temos também o oposto dessa propriedade, uma outra propriedade que afasta os Pods de um grupo de nós, chamamos estas propriedades de taints.

Taints são aplicados a nós, então um nó possui um ou mais taints que vão repelir pods de serem enviados para ele. Do outro lado, temos as tolerations que são aplicadas a Pods. Um pod que tem uma toleration que é compatível com um taint vai poder ser agendado para iniciar neste nó, caso contrário ele será permanentemente repelido.

Pense em taints e tolerations como uma forma permanente de repelir Pods de nós. Enquanto um NodeSelector ou NodeAffinity funcionam quando você explicitamente especifica uma série de labels, caso você não especifique essas propriedades, o Pod vai continuar sendo agendado em outro nó. Com uma taint em um nó, você repele permanentemente todos os pods que não possuam tolerations.

Lembre-se: Taints e Tolerations funcionam juntas, podemos ter um sem o outro, mas elas simplesmente não funcionariam. Toda toleration precisa de uma taint.

Criando uma taint

Da mesma forma como criamos labels, podemos criar taints com o comando kubectl taint nodes {nome}, mas diferentemente de labels, taints possuem um efeito. Vamos a um exemplo:

kubectl taint node {nome} processingtype=gpu:NoSchedule

Veja que temos o template chave=valor:efeito, o efeito NoSchedule vai proibir que qualquer pod que não tenha uma toleration compatível de ser criado neste nó. Além deste efeito, temos outros dois:

  • PreferNoSchedule: uma versão soft do NoSchedule, o sistema irá tentar evitar qualquer pod que não tenha uma toleration a isso seja criado neste nó.
  • NoExecute: É uma forma ainda mais radical, significa que se um taint deste tipo for adicionado a um nó, todos os Pods que não suportarem esta taint com uma toleration compatível, serão imediatamente removidos do nó. Este tipo de efeito possui uma outra propriedade chamada tolerationSeconds, que diz quanto tempo vai demorar para que o nó possa remover os pods que não tem uma toleration compatível.

Vamos a exemplos de tudo isso. Primeiramente, vamos imaginar que temos 3 nós, cada um terá uma taint diferente:

kubectl taint nodes node1 processingtype=cpu:PreferNoSchedule && \
kubectl taint nodes node2 processingtype=gpu:NoSchedule && \
kubectl taint nodes node3 processingtype=tpu:NoExecute
Uma taint pode possuir mais de um efeito ao mesmo tempo, embora isso não faça tanto sentido

O que temos aqui são:

  • node1 vai permitir a execução de pods dentro dele se não houver nenhum tipo de toleration nos pods. Já que ele é um nó simples de CPU
  • node2 não vai permitir que nenhum Pod que não tenha uma toleration compatível seja executado, já que só queremos pods de GPU rodando ali
  • node3 além de não permitir nenhum tipo de agendamento para ele, também vai automaticamente remover todos os pods que já estão sendo executados que não possuam a determinada toleration. Este é um nó caro já que usa TPUs, então queremos o máximo de controle por ali

Tolerando Taints

Agora que criamos o taint no nosso node, vamos criar uma série de pods que toleram determinadas taints.

Primeiramente vamos criar o pod que tolera a taint de TPU:

apiVersion: v1
kind: Pod
metadata:
  name: tpu
  labels:
    env: test
spec:
  containers:
  - name: tpu
    image: khaosdoctor/go-vote-api
  tolerations:
  - key: "processingtype"
    operator: "Equals"
    value: "tpu"
    effect: "NoExecute"
c
Diferentemente de uma affinity, as tolerations podem possuir os operadores Equals e Exists

Este Pod poderá ser executado no nó que contém TPUs. Agora vamos fazer o mesmo para o setup de CPU e GPU:

apiVersion: v1
kind: Pod
metadata:
  name: gpu
  labels:
    env: test
spec:
  containers:
  - name: gpu
    image: khaosdoctor/go-vote-api
  tolerations:
  - key: "processingtype"
    operator: "Equals"
    value: "gpu"
    effect: "NoSchedule"
---
apiVersion: v1
kind: Pod
metadata:
  name: cpu
  labels:
    env: test
spec:
  containers:
  - name: cpu
    image: khaosdoctor/go-vote-api
  tolerations:
  - key: "processingtype"
    operator: "Equals"
    value: "cpu"
    effect: "PreferNoSchedule"

Além disso, quando estamos trabalhando com tolerations, a chave value não é obrigatória, podemos ter apenas uma taint de chave sem precisar de um valor.

Para estes dois pods, teremos que eles podem ser incluídos em cada um dos nós que eles toleram. No entanto, um novo Pod que for criado sem nenhuma toleration será provavelmente criado no nó de CPU.

Taints padrões

Quando um nó do Kubernetes entra em determinados estados, como "sem disco", "sem memória" ou qualquer outro estado que o impeça de ter novos Pods sendo agendados para ele, o sistema automaticamente adiciona taints padrões que removem os Pods de acordo com cada uma dessas taints.

Você pode criar tolerations para estas taints, por exemplo, quando um nó fica incomunicável, podemos dizer para seus Pods que eles esperem pelo menos 5 minutos antes de serem retirados e realocados com a chave tolerationSeconds:

apiVersion: v1
kind: Pod
metadata:
  name: gpu
  labels:
    env: test
spec:
  containers:
  - name: gpu
    image: khaosdoctor/go-vote-api
  tolerations:
  - key: "node.kubernetes.io/unreachable"
    operator: "Exists"
    effect: "NoExecute"
    tolerationSeconds: 300

Dessa forma os nós não vão imediatamente remover os Pods que não forem compatíveis na esperança de que a sua rede volte antes disto. Você pode também aplicar esse tempo de toleration a qualquer outra taint.

Boas práticas para labels

Existem algumas práticas para nomear suas labels para que elas fiquem eficientes e você consiga realizar suas tarefas de forma prática

Prefixos

Como você pôde perceber no exemplo anterior, algumas das labels parecem um nome de domínio completo (FQDN) separado por um recurso, como em topology.kubernetes.io/region, a parte do DNS é chamada de prefixo.

Prefixos são utilizados para poder dar a intenção de que as labels prefixadas são públicas, qualquer label sem nenhum prefixo é considerara uma label privada apenas ao usuário, embora possam ser vistas por outros usuários.

Como a documentação oficial diz, qualquer aplicação que adiciona labels a objetos de usuários (como é o caso da própria Azure), deve obrigatoriamente possuir uma label com um prefixo.

Prefixos reservados

Todas as labels com os prefixos que terminem em kubernetes.io ou k8s.io são geralmente reservadas para objetos do core do Kubernetes, isso não é uma obrigação ou algo que é forçado ao usuário, mas é uma convenção que deve ser seguida.

Labels recomendadas

A documentação oficial recomenda uma série de labels para serem colocadas em todos os recursos criados dentro do cluster. Elas são apenas recomendadas e não são obrigatórias em todas as aplicações. Além destas labels, algumas outras labels que eu particularmente gosto de colocar são:

  • Labels que definem o time responsável pela aplicação como team=campaign
  • Labels para definir os responsáveis pela aplicação com owner=lucas_santos
  • Definição do ambiente da aplicação com env=production
  • Último commit da versão atual com sha=5acffe34d, apenas para controle de versionamento

Conclusão

Com taints, tolerations, affinity e selectors damos um passo a frente no controle dos nossos clusters de forma que podemos controlar ainda melhor o nosso ecossistema!

Até mais!

]]>
<![CDATA[ Dando permissões a usuários com Kubernetes ]]> https://blog.lsantos.dev/dando-permissoes-a-usuarios-com-kubernetes/ 601ac5eb073577468f928b15 Ter, 23 Fev 2021 09:00:00 -0300 No último artigo, conversamos sobre como podemos criar usuários no Kubernetes para que não tenhamos mais o problema de todo mundo ter o mesmo grau de acesso em todos os namespaces. Porém, não chegamos a elaborar sobre como podemos, de fato, dar essas permissões.

Vamos entender um pouco mais sobre como funciona o RBAC (Role Based Access Control) e como podemos tirar vantagem disso para podermos usar nosso cluster com mais segurança.

RBAC

O Kubernetes suporta vários métodos de autorização, o mais famoso deles, de longe, é o RBAC que significa Role Based Access Control. Basicamente o que o RBAC faz é limitar o acesso a recursos do cluster através de quatro resources do Kubernetes: Roles, RoleBindings, ClusterRoles e ClusterRoleBindings.

Ter um acesso baseado em Roles ao invés de um acesso baseado em perfis permite que você compartilhe esses Roles com vários usuários, distribuindo as permissões ao longo do cluster para que todos possam ter o acesso correto quando precisam. Um caso de uso básico desse modelo é permitir, por exemplo, que coordenadores e gerentes de áreas possam ter acesso aos seus próprios namespaces para gerenciar seus próprios funcionários, evitando assim que haja uma escalação de problemas para a equipe de operação do cluster.

Para criar esses acessos, vamos utilizar um grupo de APIs muito especial no Kubernetes, o rbac.authorization.k8s.io. Isso significa que você pode criar e configurar essas políticas de acesso dinamicamente através da API do Kubernetes e do kubectl.

A maioria dos clusters já vem com o RBAC ativado por padrão, porém é possível iniciar um novo kube-apiserver com a flag --authorization-mode=RBAC em casos de clusters manuais e, no caso da Azure, você pode habilitar o RBAC em um cluster AKS diretamente do portal:

Nomenclaturas

No RBAC, usuários são chamados de subjects, as APIs e recursos que os usuários poderão ou não ter acesso são chamados de resources. Temos também os verbs, que são as ações e operações que podem ser executadas em um resource por um subject.

Os verbos são basicamente as chamadas que podem ser feitas pelo usuário a um determinado recurso. Por exemplo, a criação de um Pod é um verb create sobre um resource Pod feito pelo usuário.

Roles e ClusterRoles

A base de todas as políticas de acesso do RBAC é um objeto nativo chamado Role. Os Roles são as regras que definem o acesso a um resource, ou seja, eles não são as regras aplicadas a um subject mas sim o conjunto de regras que pode ser reutilizado para vários usuários.

Além dos Roles, existem os ClusterRoles que, da mesma forma que os Roles, são regras de acesso para um ou mais resources, porém, enquanto o objeto Role é limitado ao seu próprio namespace, o ClusterRole é aplicado para todo o cluster, independente do namespace onde ele esteja. Isso permite que você defina políticas globais que são aplicadas para todo o cluster, e a definição de políticas que são aplicadas a recursos do Kubernetes que não dependem de namespaces, como os Nodes.

Existem algumas regras importantes para saber antes de começarmos a colocar a mão na massa:

  • Não existem regras para "negar" o acesso de um usuário a um recurso. Como já falamos antes, assim como os Ingresses, o Kubernetes trabalha em um modelo deny-all, ou seja, todas as permissões são negadas por padrão e tudo que você criar será uma exceção à lista de negação.
  • Roles sempre dependem de um namespace, então quando você cria um novo Role, você precisa setar qual é o namespace ao qual ele pertence.
  • De forma oposta, os ClusterRoles não precisam de um namespace porque eles estão acima dessa separação.

Definindo nossas permissões

No artigo anterior criamos um usuário chamado Lucas, que faz parte da equipe de desenvolvimento, vamos imaginar que também criamos outros usuários no sistema para completar nosso time:

  • Ana, que é a coordenadora da área de desenvolvimento (líder do grupo devs, do qual Lucas faz parte)
  • Thiago, que é o gerente da área de BI (líder do grupo bi)
  • Fernanda, que é uma analista de BI (parte do grupo bi)
  • Amanda, que é uma das coordenadoras da área de DevOps e uma das operadoras do cluster responsável por manter vários projetos de desenvolvimento. (parte dos grupos devs e devops)

Para criarmos nosso cenário, vamos imaginar que Lucas trabalha em um dos muitos projetos que existem dentro do cluster da empresa, portanto, para permitir que o trabalho seja feito, ele precisa ter acesso ao seu namespace. E, pela empresa ter uma cultura de DevOps mais evoluída, todos os devs são responsáveis pelo deploy de suas aplicações, então ele precisa ter o acesso completo a criação de recursos.

Ana, sendo a líder de desenvolvimento, precisa ter acesso completo a todos os projetos da área de desenvolvimento.

Thiago, da mesma forma, precisa ter acesso de leitura completo a todos os objetos do cluster, porém Fernanda está trabalhando no mesmo projeto que Lucas, então ela só pode ter acesso de leitura neste namespace.

Amanda é a coordenadora do cluster, então ela precisa ter acesso completo a todos os objetos do cluster para poder realizar ações sobre eles.

Criando os Roles

Agora que temos nossas histórias definidas, vamos partir para a elaboração das nossas permissões. Para isso vamos criar um objeto base chamado developer, que vai se aplicar a todo dev dentro do namespace projeto1, que é o projeto do time do Lucas e da Fernanda:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer
  namespace: projeto1

Até aqui estamos definindo que este Role estará dentro do namespace do projeto, então todas as permissões serão restritas a este namespace. Agora vamos definir as regras:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer
  namespace: projeto1
rules:
- apiGroups: ["", "autoscaling", "apps", "networking.k8s.io"]
  verbs: ["get", "list", "create", "watch", "update"]
  resources: ["*"]

O que estamos fazendo aqui é criando um Role que vai dar acesso a todos os recursos que estão presentes nas seguintes APIs:

  • "": Significa a core api do Kubernetes, ou seja, quando não utilizamos nenhum tipo de FQDN antes do / quando criamos um novo workload, por exemplo, Pods fazem parte dessa api, quando criamos um novo Pod utilizamos apiVersion: v1.
  • autoscaling: É o grupo que controla a escalabilidade de aplicações, os HorizontalPodAutoscalers fazem parte deste grupo
  • apps: É o grupo de Deployments, DaemonSets e outros
  • networking.k8s.io: É o grupo de Ingresses
Você pode encontrar todos grupos de API na documentação oficial e também utilizando o comando kubectl api-resources -o wide, que irá mostrar não só os grupos, mas também os nomes e os verbos disponíveis.

Além disso, estamos dando acesso a quase todos os verbos, exceto o delete, a todos os resources descritos por estas APIs através do wildcard *.

Para o Role de leitura, vamos fazer uma cópia deste Role e chamá-lo de developer-readonly:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer-readonly
  namespace: projeto1
rules:
- apiGroups: ["*"]
  verbs: ["get", "list"]
  resources: ["*"]

Para o Role de gerencia da área de desenvolvimento, temos que criar uma permissão que permita o acesso completo a todos os recursos e verbos dentro dos namespaces específicos da área de desenvolvimento, vamos supor que a esta área tenha apenas dois projetos chamados projeto1 e projeto2. Neste caso vamos ter que criar dois Roles idênticos, porém um para cada namespace e vamos nomeá-los como developer-admin:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer-admin
  namespace: projeto1
rules:
- apiGroups: ["*"]
  verbs: ["*"]
  resources: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer-admin
  namespace: projeto2
rules:
- apiGroups: ["*"]
  verbs: ["*"]
  resources: ["*"]
Podemos também criar Roles de forma interativa com kubectl create role <nome> -n <namespace> --verb=verb1,verb2,verb3 --resource=resource1,resource2

Criando ClusterRoles

Para o Role que será aplicado ao Thiago, temos que permitir que ele possa ler e listar qualquer recurso em qualquer namespace do cluster, isso seria complicado se fossemos criar um Role comum, então vamos criar um ClusterRole para que ele possa ser aplicado automaticamente, este ClusterRole vai ser chamado de readonly:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: readonly
rules:
- apiGroups: ["*"]
  verbs: ["get", "list", "watch"]
  resources: ["*"]

Veja que ClusterRoles não possuem a chave namespace.

Para a última permissão, vamos criar o objeto que será dado à Amanda, como ela é a operadora do cluster, ela precisa ter acesso completo a todos os recusos de todos os namespaces do cluster, vamos chamar esse ClusterRole de cluster-operator:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cluster-operator
rules:
- apiGroups: ["*"]
  verbs: ["*"]
  resources: ["*"]
Da mesma forma dos Roles, podemos criar ClusterRoles com o kubectl através do comando kubectl create clusterrole <nome> --verb=verb1,verb2 --resource=resource1,resource2

Aplicando as permissões com bindings

Como falamos antes, os Roles e ClusterRoles são apenas definições de permissões, estas definições precisam ser aplicadas a usuários através de outros objetos "irmãos" chamados de RoleBindings e ClusterRoleBindings.

Bindings dão as permissões definidas em Roles e ClusterRoles para subjects e groups. No nosso caso, temos alguns grupos mas também temos alguns usuários individuais que queremos dar o acesso. Além disso, podemos também garantir o acesso a uma ServiceAccount.

As diferenças entre os dois são basicamente as mesmas do Role e ClusterRole, enquanto um RoleBinding pode ser aplicado em um Role ou a um ClusterRole – embora, quando feito dessa maneira, vai aplicar as regras do ClusterRole somente ao namespace a qual aquele RoleBinding pertence – enquanto um ClusterRoleBinding pode ser apenas aplicado em um ClusterRole.

Todos os bindings precisam de uma referência a um Role ou ClusterRole existente. RoleBindings só podem referenciar Roles dentro do mesmo namespace, enquanto ClusterRoleBindings podem referenciar quaisquer ClusterRoles.

Para criarmos nosso primeiro binding, vamos olhar para o usuário Lucas, que terá o Role developer associado a ele:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developer
  namespace: projeto1
subjects:
  - kind: Group
    name: devs
    apiGroup: rbac.authorization.k8s.io
roleRef:
  - kind: Role
    name: developer
    apiGroup: rbac.authorization.k8s.io

O que estamos fazendo aqui é criando um RoleBinding que será aplicado a todos os subjects no array de subjects, neste caso um subject pode ter vários kinds, como User, Group e ServiceAccount.

Além disso, na chave roleRef, temos o nome do Role que vamos aplicar a estes subjects. Aqui temos dois kind, ou Role ou ClusterRole. E estamos essencialmente falando que queremos que o Role chamado developer seja aplicado ao subject cujo kind é um Group, ou seja, um grupo de usuários, chamado devs.

Agora, para criarmos o da Fernanda, vamos fazer a mesma coisa, porém trocando o nome da roleRef:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: business-intelligence
  namespace: projeto1
subjects:
  - kind: Group
    name: bi
    apiGroup: rbac.authorization.k8s.io
roleRef:
  - kind: Role
    name: readonly
    apiGroup: rbac.authorization.k8s.io

Vamos agora criar o binding para Ana, da mesma forma que criamos dois roles diferentes, vamos criar outros dois bindings para podermos garantir o acesso diretamente a ela:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developer-admin
  namespace: projeto1
subjects:
  - kind: User
    name: ana
    apiGroup: rbac.authorization.k8s.io
roleRef:
  - kind: Role
    name: developer-admin
    apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developer-admin
  namespace: projeto2
subjects:
  - kind: User
    name: ana
    apiGroup: rbac.authorization.k8s.io
roleRef:
  - kind: Role
    name: developer-admin
    apiGroup: rbac.authorization.k8s.io

E agora temos que criar os últimos dois bindings, que são ClusterRoleBindings, porque vamos estar garantindo o acesso a todo o cluster. No caso do Thiago, temos que tomar cuidado porque não podemos aplicar o ClusterRole para o grupo bi, uma vez que Fernanda também é parte do grupo, então vamos aplicar somente para o usuário:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: readonly
subjects:
  - kind: User
    name: thiago
    apiGroup: rbac.authorization.k8s.io
roleRef:
  - kind: ClusterRole
    name: readonly
    apiGroup: rbac.authorization.k8s.io

E da mesma forma, vamos criar o binding da Amanda:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cluster-operator
subjects:
  - kind: User
    name: amanda
    apiGroup: rbac.authorization.k8s.io
roleRef:
  - kind: ClusterRole
    name: cluster-operator
    apiGroup: rbac.authorization.k8s.io
Podemos criar RoleBindings e ClusterRoleBindings também de forma interativa com `kubectl create rolebinding <nome> --[user|group|serviceaccount] <subject> --[role|clusterrole] <roleRef>

Agora com os bindings criados, poderemos executar os comandos como cada usuário e cada um deles vai ter as permissões necessárias para seus times.

Conclusão

Agora já sabemos como criar usuários e como podemos atribuir as permissões a estes usuários, nos próximos artigos, vamos ver como podemos melhorar ainda mais o modelo de autenticação usando o Azure Managed AD com AKS!

Vejo vocês por lá!

]]>
<![CDATA[ Entendendo runtimes de containers ]]> https://blog.lsantos.dev/entendendo-runtimes-de-containers/ 6014475b073577468f928992 Qui, 18 Fev 2021 09:37:00 -0300 Nós comentamos no último post sobre o assunto sobre como os runtimes de containers funcionam, além disso falamos sobre o que é o OCI, o que é um CRI e também criamos um container usando ContainerD diretamente através da integração com a aplicação.

Porém, o ContainerD não é o único runtime existente, vamos entender um pouco sobre o que é o ContainerD, quais as suas vantagens e desvantagens sobre o seu par CRI-O e como o Docker se encaixa nisso tudo.

Docker

Como eu comentei no meu artigo sobre a história do Docker, antes da versão 1.11, o Docker era considerado um único monolito. Tudo que podia ser feito por um runtime de containers estava sendo feito pelo Docker em um único local, o download das imagens, rede, gerenciamento de ciclo de vida e tudo mais em um único processo rodando como root.

Porém, como qualquer sistema que cresce ao longo do tempo, o Docker não podia se manter monolítico para sempre, isso ia começar a pesar na arquitetura e estava começando a dificultar a integração com sistemas operacionais como o Linux. Até que alguém teve uma ideia de separar o Docker em diversos pedaços.

Em uma nota de lançamento da versão 1.11, a empresa disse que iria quebrar o daemon do Docker e começar a utilizar o runC juntamente com o containerd. Isso significava muita coisa para o ecossistema, pois agora o Docker estaria de fato utilizando o mesmo runtime que foi doado por eles em 2015 para a CNCF, criando a OCI. Dessa forma não só teríamos um modelo de containers padronizado, mas também que poderia ser evoluído ao longo dos anos de forma independente.

Ter uma ferramenta dividida em seções mais especializadas faz com que outras pessoas se especializem em manter essas seções, de forma que a aplicação como um todo se torna melhor por ser mais bem desenvolvida. Na mesma nota eles mostraram o novo modelo de funcionamento do Docker:

Modelo de funcionamento do Docker (Fonte: Docker)

Podemos ver que o engine (dockerd) principal do Docker foi separado do runtime, e agora ele se concentrava somente em receber os inputs do usuário e comunicar os mesmos ao containerd, que por sua vez iria iniciar os runtimes do runC ou qualquer outro runtime que fosse compatível com o OCI.

Em suma, dando nome a todas as partes:

  • Docker Engine (ou Docker Daemon, dockerd): É o responsável por receber os comandos do usuário pelo CLI e passar essas requisições para o containerd.
  • containerd: Interface compatível com o OCI capaz de executar, baixar e extrair qualquer imagem que seja também compatível com a OCI e executar processos do runC.
  • runC: Lida com todo o gerenciamento e criação dos containers de acordo com a especificação do OCI.

Isso criou uma série de aberturas para que outras pessoas e outras empresas também fizessem seus próprios runtimes de containers, e agora vamos entender a diferença entre os dois principais.

containerd

Conforme falamos nos artigos citados, o containerd é um daemon que executa e controla instancias do runC. Gerenciando seu ciclo de vida, bem como toda a parte de transferência e extração de imagens, armazenamento, rede e etc. Isso é o que a gente chama de container engine.

O containerd é a ferramenta principal que abstrai grande parte da funcionalidade que o Docker acumulava como um todo, se fossemos simplificar, podemos dizer que o containerd é o conjunto de funções mínimas que um container runtime precisa para executar qualquer container.

O containerd ajuda a abstrair as chamadas de kernel (syscalls) para que containers possam executar da mesma forma em qualquer sistema operacional, independente do que está acontecendo embaixo deles. Isso é chamado de supervisão e é um padrão muito comum quando estamos executando VMs.

Sempre que rodamos um SO dentro de outro, o SO convidado não sabe que está rodando dentro de uma VM, então ele continua chamando as syscalls necessárias para se comunicar com o hardware, e é ai que entra o trabalho do hypervisor, que é abstrair essas chamadas de sistema para que o SO convidado não precise implementar cada kernel individual se ele estiver rodando dentro, por exemplo, de uma máquina Linux.

O containerd é essa implementação, de forma que outras ferramentas podem também construir suas implementações de runtime sobre ele sem se preocupar em qual sistema operacional estão rodando, já que o containerd abstrai toda essa funcionalidade.

Vantagens do containerd

  • Você tem completo controle de imagens, podendo baixar e enviar imagens para registros
  • Permite a utilização via API dentro da sua própria linguagem de programação
  • Quando dentro do Kubernetes, pode ser usado como CRI
  • Totalmente configurável
  • Permite gerenciar ciclos de vida de containers através de uma API
  • Gerenciamento de armazenamento e snapshots
  • Extensível
  • Abstrai completamente o SO que está rodando

CRI-O

Além do containerd, outro runtime famoso é o CRI-O. O CRI vem de Container Runtime Interface, que é um plugin que expõe uma interface que permite que um Kubelet (agent que roda dentro de cada nó dentro de um cluster do Kubernetes) use diferentes tipos de runtime compatíveis com a especificação da OCI sem precisar de recompilação ou reinicialização. O runC é o runtime mais famoso, porém temos outros como o crun, railcar e o kata.

Não vamos entrar em detalhes sobre o porquê do Kubernetes precisar de um CRI, pois já falamos sobre tudo isso no post anterior

Tendo em vista tudo isso, o CRI-O foi criado especificamente para permitir que o Kubernetes execute containers sem muito código ou ferramentas externas. Isto porque o CRI-O é construído em diversas bibliotecas diferentes, veja alguns de seus componentes:

  • Container Runtime compatível com a OCI, por padrão é o runC mas pode ser qualquer um
  • containers/storage: Módulo responsável por lidar com os layers de armazenamento e filesystems
  • containers/image: Módulo responsável por baixar imagens de registries
  • networking: Usado para criar a camada de rede dos pods, existem vários plugins chamados de CNI que podem ser utilizados
  • conmon: Um utilitário chamado de "container monitoring" que serve para monitorar o que está acontecendo dentro dos containers

A imagem abaixo ilustra todo o processo de vida do CRI-O dentro de um cluster Kubernetes:

Processo de uso do CRI-O dentro do Kubernetes (Fonte: CRI-O)

Conclusão

Apesar de estarmos acostumados com o Docker, existem ainda muitas outras ferramentas e ideias que estão pairando sobre nossas cabeças quando o assunto é containers. Usei este artigo como base para poder escrever este conteúdo e aconselho fortemente que vocês vejam os demais artigos relacionados.

Até mais!

]]>
<![CDATA[ Criando e gerenciando usuários no Kubernetes ]]> https://blog.lsantos.dev/criando-e-gerenciando-usuarios-no-kubernetes/ 60141cb5073577468f928885 Qui, 11 Fev 2021 10:00:00 -0300 O Kubernetes ficou bastante famoso pela sua capacidade de gerenciar aplicações distribuídas e fazer com que a orquestração de containers se tornasse fácil. Porém, com o uso mais amplo do sistema, esquecemos de coisas primordiais como a segurança do nosso cluster.

O problema do acesso comum

Uma das facilidades que o Kubernetes nos proporciona é o kubectl, sua padronização da linha de comando e a capacidade de conseguir gerenciar múltiplos clusters de uma única vez faz com que ele seja tentador para ser utilizado em uma equipe de desenvolvimento como a única ferramenta disponível.

Para dar um pequeno contexto e nivelar o conhecimento de todos os leitores, o kubectl funciona com um arquivo chamado config que, geralmente, fica na pasta ~/.kube e chamamos ele de kubeconfig. Este arquivo contém as credenciais e instruções de como o kubectl deve se comportar para conectar aos clusters que estão listados por lá. Em suma, ele é a chave para todos os clusters que você tem acesso.

Durante os meus anos usando o Kubernetes e também prestando consultoria a empresas, por mais de uma vez observei situações onde o acesso do cluster é compartilhado entre toda a equipe de desenvolvimento, ou seja, o mesmo kubeconfig sendo repassado entre todo mundo e até mesmo sendo usado em ferramentas automatizadas de CI.

Mas qual é o problema disso?

Assim como é péssimo você dar a chave da sua casa para qualquer pessoa que passa na rua, é péssimo que todas as pessoas do time tenham o mesmo acesso, porque por mais que o Kubernetes possua uma ferramenta de auditoria que permite saber o que foi feito e quem fez as alterações, se todos os usuários forem admin, fica um pouco difícil entender o que aconteceu.

Além disso, dar permissão total como administrador para qualquer pessoa poder manipular o cluster é uma receita para um desastre em pouquíssimo tempo. Muitas pessoas gerenciando todas as ferramentas e todos os namespaces cria a tendência da falta de controle pelo administrador do cluster.

Autorização e autenticação no Kubernetes

Felizmente, o Kubernetes permite que tenhamos um sistema de autenticação e autorização através de usuários, que permite que criemos acessos específicos para cada pessoa.

O sistema de autenticação e autorização são diferentes, o Kubernetes não gerencia os usuários propriamente ditos, mas possui ferramentas nativas para gerenciar suas permissões – chamadas de Role, ClusterRole, RoleBinding e ClusterRoleBinding que vamos ver em um artigo futuro. Ou seja, a autenticação de usuários (saber quem é quem) e a autorização (saber quem pode fazer o que) são ferramentas separadas que podem ser gerenciadas de forma separada.

Mas por que fazer isso?

Imagine que você trabalha em uma grande empresa, como a Microsoft, por exemplo. Dentro de empresas deste porte (ou até mesmo em empresas que são bem menores), temos uma divisão muito clara de áreas e times. Cada área é responsável por uma tarefa ou então cuida de uma ou uma série de aplicações específicas.

Por exemplo, a equipe que cuida do Microsoft Teams não é a mesma que cuida do Windows, apesar de elas precisarem compartilhar recursos, uma não deveria ter permissão de acessar ou manipular os recursos da outra.

Trazendo esta analogia para empresas menores, que possuem menos produtos, se tivermos dois times distintos usando o mesmo cluster, cada time só pode ter permissões dentro de seu próprio namespace, além disso, nem todas as pessoas do time devem ter permissões para alterar determinados recursos. Por exemplo, a equipe de gerencia pode modificar todos os recursos em um namespace, mas uma equipe de BI não deveria poder escrever ou deletar recursos, apenas lê-los.

Tudo isso é chamado de RBAC ou (Role Based Access Control), e essa é apenas uma das possibilidades que temos dentro do AKS para poder gerenciar usuários, vamos explorar outras possibilidades no futuro.

Como funciona a autorização no Kubernetes

O Kubernetes expõe uma API ReST. E é ela que temos que controlar para evitar que pessoas não autorizadas acessem o cluster, ou seja, da mesma forma que protegemos nossas APIs de acessos externos, temos que proteger nosso cluster.

Para isso, o Kubernetes usa esse servidor para autorizar as requests, avaliando os atributos daquela requisição contra uma série de políticas criadas pelo administrador e dá uma resposta booleana sim ou não caso o usuário possa ou não executar a ação. Por padrão, o Kubernetes adota uma prática deny all, ou seja, ninguém tem permissão para nada, elas precisam ser adicionadas.

O kubectl possui um comando chamado auth can-i para verificar se um determinado usuário pode ou não executar alguma ação dentro da API, por exemplo:

$ kubectl auth can-i create pods --namespace production

E, se você for um administrador, pode combinar esse comando com a flag --as para poder personificar um usuário e testar permissões para ele.

$ kubectl auth can-i create pods -n production --as lucas

RBAC com AKS

Por padrão, quando criamos um cluster AKS, o RBAC está habilitado, ou seja, não precisamos fazer nada de diferente para podermos criar um cluster já com essa capacidade. No painel da Azure, temos uma opção que nos diz se podemos ou não utilizar o RBAC para autorização:

Neste artigo, vamos apenas criar os usuários, não vamos trabalhar com a parte de autorização ainda. Isto vai ficar para um próximo artigo

Para criarmos um cluster usando a linha de comando podemos fazer assim:

az aks create \
  -n nome \
  -g rg \
  --enable-rbac \
  --generate-ssh-keys \
  --node-count 1

Criando usuários no Kubernetes

No Kubernetes, temos dois conceitos de usuários, pois eles podem ser pessoas como eu e você, mas também podem ser outros serviços não humanos – como um CI, por exemplo – por isso, temos a diferenciação entre um User e uma ServiceAccount.

Essencialmente, um User é um conceito que não existe dentro do K8S como um recurso válido, ou seja, não temos um arquivo manifesto que tenha um Kind: User, porque User não é uma resource definition válida. Usuários são processos ou humanos que existem fora do cluster, por isso não são gerenciados.

No caso de ServiceAccounts estamos falando de processos que são dependentes do seus namespaces e não são humanos em essência – embora podemos criar SAs para humanos também – e estes recursos existem como uma RD válida, ou seja, temos um Kind: ServiceAccount e podemos criar uma a partir de um arquivo manifesto do Kubernetes. Estes são processos internos ao cluster e geralmente são mais comum atrelados a pods.

Em suma, ambos usam a API de autorização, mas podemos fazer isso:

kubectl create serviceaccount minhaconta

Mas não podemos fazer isso:

kubectl create user lucas

Isso significa que o Kubernetes não armazena nem gerencia informações de usuários. Todo esse gerenciamento de identidade é feito através dos meios comuns fora do cluster, a forma mais comum é através de certificados digitais no formato X509.

Usando certificados

O modo mais comum e mais seguro, como falamos antes, é usar um certificado X509 para criar um usuário. Este certificado é assinado pela CA do cluster e permite que o usuário se autentique na API usando essa validação, ou seja, o Kubernetes verifica se a request está autenticada com a chave do certificado, se sim, é um usuário confiável, pois ninguém além do próprio cluster pode emitir tal certificado.

Porém, além de toda a segurança criptográfica do certificado, o processo é bastante seguro porque exige que tanto a pessoa que está pedindo a autorização quanto o autorizador precisem participar do processo de criação do usuário, que é mais ou menos assim:

  1. Usuário gera uma chave privada (ou usa a sua própria)
  2. O usuário gera uma nova CSR (Certificate Signing Request) com esta chave e manda para o administrador
  3. O administrador assina a CSR com a CA do cluster, tornando ela um certificado X509 válido no formato CRT, e devolve o arquivo CRT para o usuário
  4. O usuário pode usar os comandos do próprio kubectl para criar seu usuário na sua máquina

Criando um usuário

Vamos fazer o processo completo, simulando um cluster criado para podermos autenticar nosso usuário.

Primeiramente, vamos gerar uma nova chave privada usando o OpenSSL:

openssl genrsa -out ./lucas-k8s.key 4096

Agora podemos gerar o arquivo CSR:

openssl req \
  -new 
  -key ./lucas-k8s.key \
  -out ./lucas-k8s-csr \
  -subj "/CN=lucas/O=devs"

Aqui temos que notar algumas coisas importantes. Primeiramente estamos gerando a chave para um usuário chamado lucas, isso fica claro quando setamos o Common Name (CN) na CSR, será esse campo que vai nomear o nosso usuário. Além disso, temos os campos de Organization (O), neste caso estamos criando um usuário chamado lucas que faz parte da organização devs.

Para o Kubernetes, a CN é o nome do usuário e o O são os grupos nos quais este usuário está inserido. Vamos usar essas informações quando criarmos os Roles e RoleBindings nos próximos artigos.

Agora, nosso usuário já tem tanto a chave quando o CSR necessário, vamos enviar estes arquivos para o administrador do nosso cluster, que vai assinar este certificado.

O administrador pode realizar um login via SSH no control plane do Kubernetes e assinar o certificado manualmente, o que é mais difícil, porém mais seguro, ou então criar um objeto chamado CertificateSigningRequest dentro do Kubernetes, que diz ao cluster que ele possui um CSR para assinar com sua CA.

Todo o objeto CSR precisa de um arquivo CSR válido em formato base64, então vamos transformar nosso lucas-k8s.csr em um novo encoding:

$ cat ./lucas-k8s.csr | base64 | tr -d '\n'

Copie a saída do terminal e vamos criar um arquivo manifesto chamado csr.yaml:

apiVersion: certificates.k8s.io/v1beta1
kind: CertificateSigningRequest
metadate:
  name: lucas-csr
spec:
  request: <cole o base64 aqui>
  usages:
    - digital signature
    - key encipherment
    - client auth

Agora criamos este objeto no cluster chamando kubectl apply -f csr.yaml e vamos poder ver o que criamos através do comando kubectl get csr:

NAME        AGE    REQUESTOR      CONDITION
lucas-csr    34s   masterclient   Pending

Veja que o certificado está como Pending, isto porque toda CSR precisa da aprovação do operador para poder ser criada. Vamos aprovar essa criação com o comando kubectl certificate lucas-csr approve. Isso vai gerar uma mensagem de confirmação e ai podemos rodar novamente o comando kubectl get csr, que vai ter uma saída um pouco diferente:

NAME        AGE    REQUESTOR      CONDITION
lucas-csr   5m3s   masterclient   Approved,Issued

Veja que além de Approved ele também já foi emitido (Issued) então estamos prontos para mandar este certificado de volta para nosso usuário. Para obter o certificado, vamos executar um elegante oneliner:

kubectl get csr lucas-csr \
  -o jsonpath='{.status.certificate}' \
  | base64 -d > lucas-k8s.pem

Teremos um novo arquivo chamado lucas-k8s.pem, vamos nos certificar de que ele é válido com o comando openssl x509 -in ./lucas-k8s.pem -text -noout. Isso vai nos dar, além de outras informações, o nome e as datas de validade do certificado.

Cadastrando o usuário no kubeconfig

Agora vamos mandar novamente o certificado de volta para o usuário para que ele possa criar o seu usuário. Para isso, vamos precisar mexer com nosso kubeconfig, já que não temos duas máquinas, então faça uma cópia do seu arquivo atual para não perder seus dados com mv ~/.kube/config ~/.kube/config.bkp.

Vamos buscar os dados base do AKS novamente usando az aks get-credentials -n cluster -g rg e vamos usar o kubectl para criar novas credenciais:

kubectl config set-credentials lucas \
  --client-key /caminho/para/lucas-k8s.key \
  --client-certificate /caminho/para/lucas-k8s.pem \
  --embed-certs=true

Agora vamos juntar a nossa configuração de usuário que acabamos de criar com a configuração do cluster:

kubectl config set-context lucas \
  --cluster=nome_do_cluster \
  --user=lucas

Como clonamos a imagem base do AKS, ela está com o acesso administrativo por padrão, vamos remover o acesso de administrador antes de enviar para o nosso usuário:

kubectl config delete-context <nome-do-contexto> 
kubectl config unset users.nome_do_resource-group_nome-do-cluster
Para saber os nomes corretos, dê uma lida no seu arquivo kubeconfig para poder achar estas chaves.

Agora você pode testar com kubectl config use-context lucas, tente executar qualquer comando, como não criamos nenhum esquema de permissão ele sempre te devolverá uma mensagem:

Error from server (Forbidden): pods is forbidden: User "lucas" cannot ...

Usando Tokens

Outra forma de criarmos usuários é dando a eles tokens JWT para autenticação. Um uso comum desta forma de autenticação é para serviços externos ou então usuários que são temporários, visto que é mais fácil remover um token do que um certificado.

Isto porque criamos tokens usando ServiceAccounts, da mesma forma que criamos para serviços não humanos. Cada SA vai criar um novo Secret com um token JWT que é válido mesmo fora do cluster.

A criação é bem simples:

kubectl create serviceaccount lucas-sa

Agora, vamos buscar a SA e ver qual é o nome do Secret que ela criou usando kubectl get sa lucas-sa -o yaml. Veja que temos uma chave chamada secrets, esta chame vai ser um array de objetos cuja propriedade name é o nome do secret que estamos procurando, no meu caso estava assim:

secrets:
  - name: lucas-sa-token-6dfl4

Vamos agora buscar o token com este simples comando:

TOKEN=$(kubectl get secret lucas-sa-token-6dfl4 -o jsonpath='{.data.token}')

Para criarmos um novo usuário no kubectl com um token, podemos usar a seguinte linha de comando:

kubectl config set-credentials lucas-token \
  --token=$TOKEN && \
kubectl config set-context lucas-token-context \
  --cluster=nome-do-cluster \
  --user=lucas-sa

Conclusão

Vimos como podemos criar usuários no Kubernetes através de certificados digitais e de tokens, nos próximos artigos vamos explorar como podemos completar o conjunto dando a eles níveis de permissionamentos diferentes com roles. Não deixe de acompanhar a continuação dessa série

Com isso, a segurança do seu cluster irá aumentar muito e você se tornará capaz de entender e gerenciar os usuários para que ninguém possa fazer algo diferente do que você está permitindo. Melhorando a auditoria no processo.

Até mais!

]]>
<![CDATA[ Métricas customizadas com AKS ]]> https://blog.lsantos.dev/metricas-customizadas-com-aks/ 60086ce9ea0c31179b22d1de Ter, 09 Fev 2021 10:00:00 -0300 Quando trabalhamos com microsserviços, sempre ouvimos falar que o monitoramento e a observabilidade são métricas chave para que possamos ter sucesso em manter nosso ecossistema coeso, funcional e não ficarmos loucos com o que está acontecendo. Falamos isto no podcast #FalaDev que participei junto de vários convidados.

Afinal, em sistemas distribuídos, a complexidade não está na unidade, mas sim em como essas unidades interagem umas com as outras. E, se não soubermos o que está acontecendo no nosso ecossistema, não podemos diagnosticar, entender e muito menos responder a incidentes em tempo hábil.

Mas como podemos resolver estes problemas? A resposta é bem simples, temos que começar o monitoramento de nossas aplicações.

Azure Monitor For Containers

Para esse tipo de situação temos ferramentas como o Azure Monitor. Essa é uma ferramenta disponibilizada pela Azure para o monitoramento de diversos de seus produtos, um deles é o AKS.

A ideia deste artigo é entender um pouco mais do Azure Monitor primeiro, depois vamos criar uma pequena aplicação que irá extrair algumas métricas customizadas de alguns de nossos serviços. Então vamos começar entendendo como ele funciona!

Como funciona o Azure Monitor For Containers?

Conforme este artigo incrível do Thomas Stringer diz, o Azure Monitor é uma solução que inclui várias facetas, uma delas é a visualização de dados que é obtida através da captura de métricas pelo que chamamos de agent.

Diagrama de funcionamento do Azure Monitor (Fonte: Microsoft Docs)

Um agent é um pequeno serviço que roda dentro do nosso cluster como um daemonset, ou seja, um serviço que cria um pod em cada nó do nosso cluster para poder capturar as métricas de recursos de máquina, como CPU, RAM e etc.

Por padrão, este recurso vem desabilitado quando criamos um novo cluster do AKS, então temos que habilitá-lo – vamos aprender como fazer isso no próximo capítulo.

Uma vez que o recurso está habilitado, o DaemonSet chamado omsagent é instalado no cluster e um deployment chamado omsagent-rs é criado em cada um dos nós do cluster. Este deployment é o responsável por agregar métricas e enviá-las ao que chamamos de Log Analytics workspace, ou seja, o local onde todas as nossas métricas vão ficar armazenadas para podermos lê-las.

Fluxo de métricas do Azure Monitor (Fonte: Thomas Stringer)

Uma vez que temos todos os serviços rodando, vamos ser capazes de obter as métricas do nosso cluster acessando ou o próprio painel da Azure, ou então uma ferramenta chamada Azure Data Explorer.

Tela do Azure Monitor for Containers

Monitorando um cluster

Primeiramente, precisamos criar um cluster AKS para que possamos monitorar, para isso, precisamos registrar duas extensões em nosso Azure CLI (se você ainda não possui o Azure CLI instalado, então instale ele na sua máquina).

Vamos verificar se já temos os providers instalados com os seguintes comandos:

az provider show -n Microsoft.OperationsManagement -o table && \
az provider show -n Microsoft.OperationalInsights -o table
Verificando se já temos os providers instalados

Se tivermos saídas dese tipo:

Namespace                       RegistrationPolicy    RegistrationState
------------------------------  --------------------  -------------------
Microsoft.OperationsManagement  RegistrationRequired  Registered

Namespace                      RegistrationPolicy    RegistrationState
-----------------------------  --------------------  -------------------
Microsoft.OperationalInsights  RegistrationRequired  Registered

Significa que os provedores estão instalados e funcionando (veja o Registered), porém, se precisarmos instala-los, vamos ter que rodar os seguintes comandos:

az provider register --namespace Microsoft.OperationsManagement && \
az provider register --namespace Microsoft.OperationalInsights

O processo pode demorar alguns minutos para completar, rode o primeiro comando novamente para ter certeza de que o provedor foi registrado. Vamos agora criar um novo Resource Group e armazenar o valor tanto dele quando o nome do nosso novo cluster em uma variável:

export RESOURCE_GROUP=aksmonitor
export CLUSTER_NAME=aksmonitor
az group create -n $RESOURCE_GROUP -l eastus

Então podemos criar um novo cluster do AKS habilitado para monitoramento através do comando:

az aks create \
  -g $RESOURCE_GROUP \
  -n $CLUSTER_NAME \
  --node-count 1 \
  --generate-ssh-keys \
  --enable-addons monitoring,http_application_routing
Se você já tem um cluster AKS criado, pode habilitar o monitoramento através do comando az aks enable-addons -a monitoring -n $CLUSTER_NAME -g $RESOURCE_GROUP

Criando um teste

Vamos criar alguns pods de teste para que possamos monitorar o uso do sistema, primeiramente vamos criar um deployment simples que vai expor uma pequena API em Node.js. Para isso vamos criar um novo arquivo simple_api.yaml e criar nossas instruções:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: simple-api
spec:
  selector:
    matchLabels:
      app: simple-api
  template:
    metadata:
      labels:
        app: simple-api
    spec:
      containers:
      - name: simple-api
        image: khaosdoctor/scalable-node-api:2.0.0
        resources:
          limits:
            memory: "128Mi"
            cpu: "100m"
        ports:
        - containerPort: 8080
          name: http
        env:
          - name: PORT
            value: "8080"
---
apiVersion: v1
kind: Service
metadata:
  name: simple-api
spec:
  type: LoadBalancer
  selector:
    app: simple-api
  ports:
  - port: 80
    targetPort: http

Este serviço vai nos dar um endereço externo de IP que poderá ser obtido usando kubectl get svc na coluna EXTERNAL-IP:

NAME         TYPE           CLUSTER-IP    EXTERNAL-IP     PORT(S)        
simple-api   LoadBalancer   10.0.50.182   xxx.xxx.xxx.xxx   80:30287/TCP

Acesse o endereço após alguns instantes para ver o pod sendo executado. Nesta versão teremos um delay de 2 segundos gerando uma pequena carga para podermos ver o consumo de recursos. Vamos ao nosso portal da Azure e selecionaremos nosso cluster AKS, então vamos no menu "Insights":

Vamos utilizar o comando scale do kubectl para poder escalar os pods e ver que temos monitoramento constante:

Numero de pods pendentes e executando

Se rodarmos um teste de stress, veremos que teremos um aumento gradativo do uso de memória e CPU:

Além disso temos diversos workbooks que são dashboards já prontos que nos permitem ver alguns dados:

Workbook de deployments

Podemos também obter os logs dos nossos containers através do menu logs no canto esquerdo do painel:

Aqui iremos ter uma visão inicial com um modal que irá nos perguntar se queremos ver algumas queries prontas, vamos para a aba "Audit" e então vamos clicar em "Run" abaixo de "List containers logs per namespace":

Veja um exemplo de uma query no formato Kusto:

// List container logs per namespace 
// View container logs from all the namespaces in the cluster. 
ContainerLog
| join(KubePodInventory
    | where TimeGenerated > startofday(ago(1h)))//KubePodInventory Contains namespace information
    on ContainerID
| where TimeGenerated > startofday(ago(1h))
| project TimeGenerated, Namespace, LogEntrySource, LogEntry

Vamos ter uma lista de todos os logs gerados pela nossa aplicação:

Mas e se quisermos gerar mais informações e buscar mais dados? Como fazemos para criar uma métrica customizada no Azure Monitor?

Criando métricas customizadas

Para podermos entender um pouco do que vamos fazer a seguir, precisamos entender sobre como o Kubernetes trabalha com métricas.

Prometheus

Atualmente, o Prometheus é o líder de mercado quando se trata de armazenamento e coleta de métricas para aplicações distribuídas. O Prometheus funciona em um modelo de scrapping, ou seja, de tempos em tempos ele acessa uma URL definida em seus endpoints cadastrados e busca os dados em um formato específico. A camada de abstração que roda entre um serviço e o Prometheus é chamado de exporter.

Os exporters buscam as métricas das APIs e aplicações e as formatam para que sejam consumidas pelo Prometheus de forma correta, por isso eles geralmente são executados dentro do mesmo pod, em outro container, acessando a aplicação localmente. Como esta imagem do blog do Thomas Stringer nos mostra:

Diagrama de funcionamento de um exporter (Fonte: Thomas Stringer)

Exporters

Quando estamos trabalhando com uma tecnologia que ainda não tem um exporter pronto, ou então quando queremos fazer o scrapping de métricas de aplicações que nós mesmos escrevemos (que é o nosso caso aqui) nós podemos escrever o nosso próprio através de uma lista de clientes disponíveis.

O trabalho de um exporter é basicamente rodar um loop onde ele:

  1. Inicia um servidor HTTP
  2. Busca as métricas da aplicação alvo
  3. Trata as métricas e as formata
  4. Devolve as métricas para o prometheus quando forem necessárias
  5. Dorme por um determinado tempo antes de começar de novo

A aplicação que vamos estar utilizando é uma simples aplicação de votação feita em Go, você pode ver o código fonte neste repositório. Temos uma imagem hospedada em meu Docker Hub pessoal.

Como criei a aplicação do zero, construi um pequeno exporter em Node.js para ela que você pode checar neste repositório com esta imagem. Basicamente a aplicação só possui um arquivo index.js que inicia um servidor do Koa usando uma biblioteca de cliente do Prometheus.

const Koa = require('koa')
const app = new Koa()
const axios = require('axios').default

const prometheus = require('prom-client')
const PrometheusRegistry = prometheus.Registry
const registry = new PrometheusRegistry()

const PREFIX = `go_vote_api_`
const pollingInterval = process.env.POLLING_INTERVAL_MS || 5000
registry.setDefaultLabels({ service: 'go_vote_api', hostname: process.env.POD_NAME || process.env.HOSTNAME || 'unknown' })

// METRICS START

const totalScrapesCounter = new prometheus.Counter({
  name: `${PREFIX}total_scrapes`,
  help: 'Number of times the service has been scraped for metrics'
})
registry.registerMetric(totalScrapesCounter)

const scrapeResponseTime = new prometheus.Summary({
  name: `${PREFIX}scrape_response_time`,
  help: 'Response time of the scraped service in ms'
})
registry.registerMetric(scrapeResponseTime)

const localResponseTime = new prometheus.Summary({
  name: `${PREFIX}exporter_response_time`,
  help: 'Response time of the exporter in ms'
})
registry.registerMetric(localResponseTime)

const totalVotes = new prometheus.Gauge({
  name: `${PREFIX}total_votes`,
  help: 'Total number of votes computed until now',
  async collect () {
    const total = await scrapeApplication()
    this.set(total)
  }
})
registry.registerMetric(totalVotes)

// --Utility Function-- //

async function scrapeApplication () {
  const id = Date.now().toString(16)
  console.log(`Scraping ${process.env.SCRAPE_URL}:${process.env.SCRAPE_PORT}/${process.env.SCRAPE_PATH} [scrape id: ${id}]`)
  const start = Date.now()
  const metrics = await axios.get(`${process.env.SCRAPE_URL}:${process.env.SCRAPE_PORT}/${process.env.SCRAPE_PATH}`)
  scrapeResponseTime.observe(Date.now() - start)
  totalScrapesCounter.inc()
  console.log(`Scraped data [scrape id: ${id}]`)
  return metrics.data.total
}

// --Servers start-- //

app.use(async (ctx, next) => {
  console.log(`Received scrape request: ${ctx.method} ${ctx.url} @ ${new Date().toUTCString()}`)
  const start = Date.now()
  await next()
  localResponseTime.observe(Date.now() - start)
})

app.use(async ctx => {
  ctx.set('Content-Type', registry.contentType)
  ctx.body = await registry.metrics()
})

// start loop
if (pollingInterval > 0) {
  setInterval(async () => {
    const total = await scrapeApplication()
    totalVotes.set(total)
  }, pollingInterval)
}

console.log(`Listening on ${process.env.SCRAPER_PORT || 9837}`)
app.listen(process.env.SCRAPER_PORT || 9837)

O que esta aplicação está fazendo é registrar uma série de métricas em um registrador padrão do Prometheus, as métricas que estamos pegando são:

  • Número total de votos
  • Quantidade vezes que buscamos as métricas
  • Tempo de resposta do exporter e também da API na rota /total
Obviamente que estas métricas não são tão importantes quanto outras métricas que podem ser obtidas através da instrumentação direta na aplicação, para isso uma boa prática é ter uma rota /metrics que serve as métricas de instrumentação da aplicação, como CPU, RAM e outras, além do exporter.

Se acessarmos o exporter, teremos uma saída como esta:

Extraindo métricas da aplicação

Para podermos extrair as métricas da aplicação, o que vamos fazer é rodar os dois containers lado a lado, dessa forma não oneramos a aplicação original com a busca e parsing de métricas e ainda mantemos a velocidade de conexão por estarem na mesma rede. Nosso arquivo de deployment anterior vai mudar um pouco:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vote-api
spec:
  selector:
    matchLabels:
      app: vote-api
  template:
    metadata:
      labels:
        app: vote-api
    spec:
      containers:
      - name: vote-api
        image: khaosdoctor/go-vote-api
        resources:
          limits:
            memory: "128Mi"
            cpu: "200m"
        ports:
        - containerPort: 8080
          name: http
      - name: vote-api-exporter
        image: khaosdoctor/go-vote-api-exporter
        resources:
          limits:
            memory: "128Mi"
            cpu: "100m"
        ports:
        - containerPort: 9837
          name: exporter
        env:
          - name: SCRAPE_PORT
            value: "8080"
          - name: SCRAPE_PATH
            value: total
          - name: SCRAPE_URL
            value: "http://localhost"
      - name: vote-api-voter
        image: curlimages/curl
        command: ["/bin/sh"]
        args: [
          "-c",
          "while true; do wget -O- http://localhost:8080/votes/Lucas; sleep 3; done"
        ]
        resources:
          limits:
            memory: "128Mi"
            cpu: "100m"
---
apiVersion: v1
kind: Service
metadata:
  name: vote-api
spec:
  type: LoadBalancer
  selector:
    app: vote-api
  ports:
  - port: 80
    targetPort: http
---
apiVersion: v1
kind: Service
metadata:
  name: vote-api-exporter
spec:
  selector:
    app: vote-api
  ports:
  - port: 9837
    targetPort: exporter

O que estamos fazendo é subir três containers junto com a aplicação, um deles é o exporter e o outro é uma simples aplicação que ficará votando a cada 3 segundos para simular o aumento dos votos. Note que estamos definindo localhost como a url de scrapping porque todos os containers estão na mesma rede local.

Podemos verificar os logs de cada container criado depois com o comando kubectl  logs deploy/vote-api -c <nome-do-container>, se quisermos ver nosso exporter em ação basta executarmos kubectl port-forward svc/vote-api-exporter 9837:9837 e acessarmos localhost:9837 em nossa máquina:

Veja que agora temos mais labels, como o hostname que antes não era buscado.

Preparando o Azure Monitor

Agora que temos nossa API pronta, vamos preparar o Azure Monitor para poder buscar as métricas. Para isso, vamos criar um simples ConfigMap que irá configurar o nosso agente dentro do Node. A própria Microsoft tem um modelo padrão de configuração que podemos baixar com o comando abaixo

$ curl -Lo agent-config.yaml https://aka.ms/container-azm-ms-agentconfig

Salve o arquivo e veja que ele está bastante comentado, é um arquivo longo, mas a parte que nos interessa é esta aqui:

        # When monitor_kubernetes_pods = true, replicaset will scrape Kubernetes pods for the following prometheus annotations:
        # - prometheus.io/scrape: Enable scraping for this pod
        # - prometheus.io/scheme: If the metrics endpoint is secured then you will need to
        #     set this to `https` & most likely set the tls config.
        # - prometheus.io/path: If the metrics path is not /metrics, define it with this annotation.
        # - prometheus.io/port: If port is not 9102 use this annotation
        monitor_kubernetes_pods = false

Vamos alterar monitor_kubernetes_pods para true, isto fará com que quaisquer deployments que tiverem as annotations prometheus.io/scrape e prometheus.io/scheme sejam buscados pelo agent como se fosse o Prometheus buscando métricas. Agora vamos criar a configuração com kubectl apply -f agent.yaml.

Vamos agora adicionar as annotations no nosso arquivo de deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vote-api
spec:
  selector:
    matchLabels:
      app: vote-api
  template:
    metadata:
      labels:
        app: vote-api
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/path: /
        prometheus.io/port: "9837"
    spec:
      containers:
      - name: vote-api
        image: khaosdoctor/go-vote-api
        resources:
          limits:
            memory: "128Mi"
            cpu: "200m"
        ports:
        - containerPort: 8080
          name: http
      - name: vote-api-exporter
        image: khaosdoctor/go-vote-api-exporter
        resources:
          limits:
            memory: "128Mi"
            cpu: "100m"
        ports:
        - containerPort: 9837
          name: exporter
        env:
          - name: SCRAPE_PORT
            value: "8080"
          - name: SCRAPE_PATH
            value: total
          - name: SCRAPE_URL
            value: "http://localhost"
      - name: vote-api-voter
        image: curlimages/curl
        command: ["/bin/sh"]
        args: [
          "-c",
          "while true; do wget -O- http://localhost:8080/votes/Lucas; sleep 3; done"
        ]
        resources:
          limits:
            memory: "128Mi"
            cpu: "100m"
---
apiVersion: v1
kind: Service
metadata:
  name: vote-api
spec:
  type: LoadBalancer
  selector:
    app: vote-api
  ports:
  - port: 80
    targetPort: http
---
apiVersion: v1
kind: Service
metadata:
  name: vote-api-exporter
spec:
  selector:
    app: vote-api
  ports:
  - port: 9837
    targetPort: exporter

Agora podemos buscar as métricas através do nosso portal, vamos clicar em Logs que nem fizemos antes, e agora podemos buscar dentro da tabela de InsightsMetrics, pela seguinte query:

InsightsMetrics
| where Namespace == "prometheus"
| where Name in ("go_vote_api_total_votes")
| summarize sum(Val) by TimeGenerated, Name
| order by TimeGenerated asc

Isso vai nos dar todos os votos que foram computados separados por horário que eles foram gerados:

Se clicarmos em Chart teremos um grafico que pode ser configurado para podermos ver o crescimento da métrica:

Conclusão

Extrair métricas é importante e necessário para que possamos ter uma melhor visão do nosso sistema. Com o Azure Monitor fica muito mais fácil de fazermos essas medições porque não precisamos instalar nada externo como o Prometheus e também não precisamos gerenciar bancos de dados ou qualquer outra coisa.

]]>
<![CDATA[ #ImersãoFullstack - Escalando infinitamente com Kubernetes ]]> https://blog.lsantos.dev/aprenda-kubernetes-no-aks/ 601c7892073577468f928c75 Qui, 04 Fev 2021 22:00:00 -0300 Olá pessoal! Mais uma vez estou com o grande Wesley Willians para conversarmos sobre Kubernetes e containers! Nesta live falamos sobre o que é o Kubernetes, como ele está funcionando hoje em dia na Azure e tivemos exemplos práticos de como podemos rodar nossas aplicações com os serviços de containers que a Azure oferece!

Neste vídeo comentamos diversos links uteis, aqui estão a lista deles:

OCI, CRI, Runtimes? Entendendo o ecossistema de containers.
Recentemente o Kubernetes iniciou a depreciação do Docker. O que isso significa para você? O que é essa sopa de letrinhas de OCI, CRI, RunC? Vamos mergulhar e aprender mais sobre o ecossistema de containers!
Que tal aprender AKS com este curso GRATUITO?
Neste treinamento gratuito de 8h, mostro como podemos aprender Kubernetes do zero até sermos capazes de cuidar das nossas próprias aplicações!
Deploy de imagens Docker: Do VSCode para a Azure
A integração mais esperada entre o Docker e a Azure chegou às mãos de todos, e de uma forma muito mais fácil!
Conheça o GitHub Container Registry
Você já pensou nas facilidades que teria se pudesse armazenar suas imagens Docker no mesmo local onde você versiona seu código?
Executando Containers no Azure Container Instancies com Docker
Já imaginou como seria se você pudesse executar diretamente um container em uma infraestrutura cloud sem precisar fazer nada novo?
What is Docker Used For? A Docker Container Tutorial for Beginners
As a developer, you have probably heard of Docker at some point in yourprofessional life. And you’re likely aware that it has become important tech forany application developer to know. If you have no idea of what I’m talking about, no worries – that’s what thisarticle is for. We’ll go on a jo…
]]>
<![CDATA[ Controlando containers de dentro sua aplicação com ContainerD ]]> https://blog.lsantos.dev/integrando-containers-na-sua-aplicacao-com-containerd/ 5fff3b2868fe9d21e9e66b46 Qui, 04 Fev 2021 11:00:00 -0300 Como já falamos no artigo anterior, o Kubernetes recentemente marcou o Docker como depreciado, ou seja, não poderemos mais usar a integração com o Docker diretamente de dentro de um Pod a não ser que instalemos ele manualmente.

Neste mesmo artigo falei sobre o que isso significa para o ecossistema e apresentei a Open Container Initiative (OCI), que é a responsável por criar o padrão que seguimos para que os runtimes de containers possam executar o mesmo tipo de imagem. Todas as imagens compatíveis com o OCI poderão ser executadas por qualquer runtime também compatível, isso abre portas para a criação de diferentes runtimes.

Agora, vamos entender como podemos utilizar o ContainerD para integrar com as nossas aplicações e executar containers sem a necessidade do Docker ou de qualquer comunicação com ele.

ContainerD

Assim como o CRI-O, Docker Engine e o RKT, o ContainerD é um runtime de containers, ou seja, ele é a ferramenta que gerencia todo o ciclo de vida de um container, desde o download de imagem até a criação das interfaces de rede, supervisão e armazenamento.

Overview do ecossistema do ContainerD (Fonte: ContainerD)

Isso significa que podemos pegar qualquer imagem que seja compatível com a especificação da OCI e rodar usando o ContainerD. Então se é tudo compatível, por que não usamos o Docker direto?

Pelo mesmo motivo que o Kubernetes depreciou o suporte ao Docker. Por mais que possamos querer uma aplicação como o Docker, ele ainda é totalmente voltado aos usuários e a interação dos usuários com a ferramenta, ou seja, o Docker é uma ferramenta feita para ser usada por pessoas, não por máquinas.

Com o ContainerD podemos integrar a manipulação de containers no código, porque ele não possui uma interface de usuário. E é exatamente o que vamos fazer aqui.

Preparativos

Antes de podermos executar a nossa imagem usando o ContainerD, vamos ter que preparar uma máquina para executar a ferramenta. O ctr (CLI do ContainerD) só executa em ambientes que possuam a implementação runc da OCI instalada e, infelizmente, essa implementação só existe para Linux.

Criando uma VM

No meu caso, estou usando um Mac, se você estiver em qualquer outro ambiente que não seja Linux (como o Windows), você precisará de uma máquina virtual – ou você pode usar o WSL2 no Windows. Eu decidi ir pela primeira opção e criei uma máquina virtual usando o VirtualBox e a imagem netboot do Ubuntu 18.4 (só porque ela é mais leve e mais rápida de baixar).

Rodei uma pequena VM usando Linux em meu computador
Nota: Se você quiser instalar o Ubuntu usando a mesma imagem que eu utilizei, selecione sua arquitetura de sistema (x86, amd64, arm, etc.) no link acima e baixe o arquivo chamado mini.iso. A partir daí, execute a imagem no VirtualBox.

Se você estiver utilizando o Linux ou uma de suas distribuições então esse passo não é necessário, você pode pular diretamente para a instalação do runc.

Nota 2: Não vou entrar em detalhes de como fazer a instalação da máquina virtual nem como executar o Ubuntu neste artigo, existem vários artigos e tutoriais incríveis na Internet sobre o mesmo tópico, eles são bem simples de se encontrar.

Depois de criada a máquina virtual, vamos instalar o a linguagem Go na máquina.

Instalando o Go

Neste exemplo vamos integrar a nossa aplicação em Go com o ContainerD e já vamos aproveitar para compilar o runc (outra dependência necessária) direto da fonte.

Estou usando a distribuição do Linux Ubuntu 18.4, então a instalação pode ser feita usando o Snap ou o arquivo Tar. Você pode achar todas as opções na documentação. No meu caso, instalei usando o Snap com o seguinte comando:

sudo snap install go --classic

Para este tutorial estou usando a versão 1.15.6:

Vamos criar uma pasta em qualquer local – eu escolhi ~/gopath – para criar o nosso $GOPATH, depois, vamos abrir o nosso arquivo .bashrc e adicionar a seguinte linha:

export GOPATH=~/gopath
export PATH=$PATH:$GOPATH/bin

Depois salvaremos e iremos rodar o comando source .bashrc para poder carregar as alterações. Então vamos criar os diretórios corretos usando o comando a seguir:

mkdir -p $GOPATH/src/github.com

Mas eu preciso saber Go?

Não obrigatoriamente, o ContainerD possui um CLI que você pode utilizar para fazer a comunicação via linha de comando.

Além disso, o ContainerD também possui uma API em gRPC que permite que você se comunique diretamente com o Socket do serviço e chame os RPC's necessários, como Mark Kose fez aqui com o browser (mas ele usou o Envoy para se comunicar com o socket) e por aqui usando Java com o gRPC.

Então, extrapolando um pouco o conceito (mas nem tanto assim), é possível utilizar qualquer linguagem suportada pelo gRPC para poder se conectar com o socket do ContainerD no arquivo containerd.sock. Muito semelhante ao que a gente faz com a integração usando o docker.sock. Porém, infelizmente, não há um client nativo exceto o escrito em Go.

Instalando o runc

O runc é um projeto open source feito pela OCI, você pode achar o repositório oficial aqui. Podemos fazer a instalação de algumas formas:

  1. Baixando uma release da lista de releases e colocando em uma pasta que esteja na sua variável $PATH
  2. Clonando o repositório e executando make, como descrito no README.
  3. Utilizando o go get

O meio mais fácil sem dúvida é utilizar a opção 3, pois a 1 exige que saibamos algumas informações sobre nosso sistema e a 2 pode dar alguns problemas dependendo da arquitetura. Já que temos o Go instalado, vamos somente instalar o runc como um novo pacote.

Execute o comando a seguir para baixar e instalar o runc:

go get github.com/opencontainers/runc

Após um tempo, verifique se há um binário chamado runc na pasta $GOPATH/bin. Tente executar o comando runc --version para ter uma saída parecida com esta:

runc instalado e pronto para ser executado

Caso contrário, gere o binário você mesmo indo até a pasta de download com cd $GOPATH/src/github.com/opencontainers/runc e executando o comando make && sudo make install.

Instalando o ContainerD

Para instalarmos o ctr, o CLI do ContainerD, na nossa máquina virtual. No caso do ubuntu é simplesmente executar o comando sudo apt install containerd -y, para outros sistemas veja a página de downloads.

Se tudo correu bem, você poderá executar o comando ctr version para mostrar o número da versão do CLI.

Se você estiver tendo problemas em executar o comando ctr version com uma mensagem de "Permission Denied" ao ler o arquivo containerd.sock em /run/containerd, execute o comando como sudo.

Além disso, o ContainerD também pode ser usado com o Systemd como um serviço Daemon, para verificar se está tudo correto, utilize o comando sudo systemctl status containerd, você deve obter uma saída informando que o daemon do ContainerD está instalado e executando.

Se você quiser ter mais certeza, execute o comando ps -fC containerd e veja os processos aparecendo na lista de processos do sistema:

UID        PID  PPID  C STIME TTY          TIME CMD
root     23133     1  0 14:23 ?        00:00:02 /usr/bin/containerd
root     23666 23643  0 14:42 ?        00:00:00 containerd

Usando sem precisar de sudo

Para remover a necessidade do uso de sudo para a execução do ctr, podemos alterar o arquivo de configuração do daemon que fica localizado em /etc/containerd/config.toml, por padrão o arquivo não é gerado, então temos que gerar um arquivo base. Para isso vamos criar o diretório e executar o comando nativo do containerd para gerar um arquivo base.

sudo mkdir -p /etc/containerd
sudo containerd config default > /etc/containerd/config.toml

Este comando irá gerar um arquivo próximo a este em /etc/containerd:

version = 2
root = "/var/lib/containerd"
state = "/run/containerd"
plugin_dir = ""
disabled_plugins = []
required_plugins = []
oom_score = 0

[grpc]
  address = "/run/containerd/containerd.sock"
  tcp_address = ""
  tcp_tls_cert = ""
  tcp_tls_key = ""
  uid = 0
  gid = 0
  max_recv_message_size = 16777216
  max_send_message_size = 16777216

[ttrpc]
  address = ""
  uid = 0
  gid = 0

[debug]
  address = ""
  uid = 0
  gid = 0
  level = "debug"

[metrics]
  address = ""
  grpc_histogram = false

[cgroup]
  path = ""

[timeouts]
  "io.containerd.timeout.shim.cleanup" = "5s"
  "io.containerd.timeout.shim.load" = "5s"
  "io.containerd.timeout.shim.shutdown" = "3s"
  "io.containerd.timeout.task.state" = "2s"

[plugins]
  # Omitido

Vamos buscar o ID do nosso usuário e do nosso grupo, para isso digite o comando id na linha de comando e copie os IDs uid e gid, no meu caso, ambos são 1000:

uid=1000(khaosdoctor) gid=1000(khaosdoctor) groups=1000(khaosdoctor),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lpadmin),124(sambashare),999(vboxsf)

Agora abra o arquivo e vamos editar as seções [grpc], [ttrpc] e [debug] do arquivo TOML. Substitua as propriedades uid e gid de 0 para o número do seu usuário e grupo, ficando assim (lembrando que os meus eram 1000):

[grpc]
  address = "/run/containerd/containerd.sock"
  tcp_address = ""
  tcp_tls_cert = ""
  tcp_tls_key = ""
  uid = 1000
  gid = 1000
  max_recv_message_size = 16777216
  max_send_message_size = 16777216

[ttrpc]
  address = ""
  uid = 1000
  gid = 1000

[debug]
  address = ""
  uid = 1000
  gid = 1000
  level = "debug"

Salve o arquivo e feche o editor. Agora execute sudo systemctl restart containerd e depois sudo ls -l /run/containerd e verifique se o arquivo containerd.sock está sob o seu nome de usuário e grupo.

Usando o ctr

O primeiro passo para utilizar o ContainerD é entender o ctr, da mesma forma como o Docker trabalha com linhas de comando, temos a capacidade de criar e gerenciar containers de uma forma mais controlada com o ctr.

Primeiramente, temos que criar um namespace. Namespaces são separações lógicas no sistema que permitem que usuários diferentes no mesmo sistema possam trabalhar sem conflitar. Para isso vamos executar o comando ctr namespaces create:

ctr namespaces create lsantos # Estou criando um namespace chamado "lsantos"

Podemos ver os namespaces criados com o comando ctr namespaces ls:

NAME    LABELS 
lsantos 

Finalmente, podemos baixar nossa primeira imagem, como teste, vou baixar uma imagem própria que contém uma pequena API em Node.js que responde um "Hello World" para todo mundo que acessar uma determinada porta. Vamos executar o comando abaixo para poder baixar a imagem:

ctr images pull docker.io/khaosdoctor/simple-node-api:latest

E ai podemos listar as imagens com ctr images ls:

REF                                          TYPE                                                 DIGEST                                                                  SIZE      PLATFORMS   LABELS 
docker.io/khaosdoctor/simple-node-api:latest application/vnd.docker.distribution.manifest.v2+json sha256:587747676c8aa6e26e2c7f3adf8c76c5653e63e96af6510fbf12357be4fcd0f3 254.1 MiB linux/amd64 -  

Agora que temos nossa imagem baixada, vamos executá-la. Os comandos do ctr são bem parecidos com os próprios comandos do Docker. Vamos executar a o seguinte comando:

sudo ctr run \
  --net-host \
  --rm \
  --env PORT=8080 \
  docker.io/khaosdoctor/simple-node-api:latest \
  simple-api

E temos um container em execução:

Vamos passar parte por parte do comando:

  • sudo ctr run: É o comando que diz para o ContainerD criar um container a partir de um FS ou uma imagem
  • --net-host: Permite que acessemos a rede do container através do host (assim podemos acessar nossa API)
  • --rm: Assim como no Docker, remove o container após executar
  • --env PORT=8080: Criamos uma variável de ambiente dentro do container chamada PORT com o valor 8080, como a documentação da imagem diz
  • docker.io/khaosdoctor/...: Falamos qual é a imagem que queremos executar
  • simple-api: Damos um nome ao container, este nome pode ser qualquer coisa

Agora podemos entrar no browser no endereço localhost:8080 e ver a mágica acontecer!

Parabéns! Você acabou de criar o seu primeiro container sem precisar do Docker!

Integrando com a API do ContainerD

Agora, vamos para a segunda parte, onde fazemos tudo isso só que sem nenhum tipo de linha de comando ou CLI para ajudar. Vamos escrever uma aplicação em Go para podermos integrar diretamente com o containerd.sock e dar os comandos através da interface gRPC dele.

A grande vantagem de se utilizar o Go para este tipo de ação é que temos o client nativo direto da fonte, pois o ContainerD é escrito em Go. Então tudo fica muito mais fácil!

O exemplo que vamos fazer aqui é muito parecido com o exemplo do site da lib, porém vamos simplificar um pouco mais para podermos executar o que fizemos antes através do ctr.

Primeiramente, vou criar um diretório em qualquer local da minha VM (se você estiver usando o VirtualBox, dê uma olhada na opção "Shared Folders"), resolvi chamar meu diretório de containerd, dentro dele criei uma outra pasta chamada src.

Criando um client

Vamos iniciar um novo módulo executando go mod init containerd e depois baixar o pacote do client do ContainerD com go get github.com/containerd/containerd, isso vai criar uma nova pasta pkg com os arquivos necessários dentro.

O código que você verá abaixo pode ser encontrado neste repositório em meu GitHub

Dentro da pasta src vou criar um novo arquivo chamado main.go e vou criar o client do ContainerD:

package main

import (
	"log"

	"github.com/containerd/containerd"
)

func main() {
	if err := createAPI(); err != nil {
		log.Fatal(err)
	}
}

func createAPI () error {
	client, err := containerd.New("/run/containerd/containerd.sock")
	defer client.Close()

	if err != nil {
		return err
	}

	return nil
}

Aqui o que estamos fazendo é basicamente criar o client do ContainerD passando o caminho do arquivo .sock que vamos nos comunicar.

Criando um contexto

Já que estamos usando o socket para nos comunicar via gRPC, vamos ter que criar um contexto para as chamadas. Para isso, vamos importar o pacote github.com/containerd/containerd/namespaces no topo do nosso arquivo e criar um novo contexto e um novo namespace, muito parecido com o que já fizemos antes com o crt.

Nossos imports ficarão assim:

import (
	"context"
	"log"

	"github.com/containerd/containerd"
	"github.com/containerd/containerd/namespaces"
)

Depois adicionaremos uma outra linha dentro da função createAPI:

func createAPI () error {
	client, err := containerd.New("/run/containerd/containerd.sock")
	defer client.Close()
	if err != nil {
		return err
	}

	ctx := namespaces.WithNamespace(context.Background(), "lsantos")

	return nil
}

Aqui estamos criando um novo namespace chamado lsantos e vamos passar um contexto vazio.

Baixando uma imagem

Vamos dar o pull para a nossa imagem da mesma forma que fizemos com o comando ctr image pull. Nossa função final ficará assim:

func createAPI () error {
	client, err := containerd.New("/run/containerd/containerd.sock")
	defer client.Close()
	if err != nil {
		return err
	}

	ctx := namespaces.WithNamespace(context.Background(), "lsantos")

	image, err := client.Pull(ctx, "docker.io/khaosdoctor/simple-node-api:latest", containerd.WithPullUnpack)
	if err != nil {
		return err
	}
	log.Printf("Imagem %q baixada", image.Name())

	return nil
}

Na sua VM execute o comando go build ./src/main.go e depois ./main, você deverá ver uma saída dizendo que a imagem foi baixada.

Criando um container

Para podermos executar um container através da interface programática, temos que criar um runtime OCI válido. Este runtime pode ter várias configurações, mas o ContainerD já tem um runtime padrão muito bom e muito útil, então vamos utilizá-lo.

Para isso vamos criar uma nova função chamada createContainer, ela vai ter a seguinte assinatura:

func createContainer (
	ctx context.Context,
	client *containerd.Client,
	image containerd.Image,
) (containerd.Container, error) { }

Para iniciarmos o container sem problemas de nomenclatura, vamos criar automaticamente um hash único baseado no horário para cada container. Vamos importar as bibliotecas crypto/sha256, encoding/hex e time e fazer o seguinte código:

func createContainer (
	ctx context.Context,
	client *containerd.Client,
	image containerd.Image,
) (containerd.Container, error) {
	
	hasher := sha256.New()
	hasher.Write([]byte(time.Now().String()))
	salt := hex.EncodeToString(hasher.Sum(nil))[0:8]
    
	containerName := "simple-api-" + salt
	log.Printf("Criando um novo container chamado %q", containerName)

Agora podemos criar o nosso spec do OCI, para isso vamos importar o módulo OCI do ContainerD, nossos imports ficarão assim:

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"log"
	"time"

	"github.com/containerd/containerd"
	"github.com/containerd/containerd/namespaces"
	"github.com/containerd/containerd/oci"
)

E agora criamos o spec em uma variável a parte:

func createContainer (
	ctx context.Context,
	client *containerd.Client,
	image containerd.Image,
) (containerd.Container, error) {

	hasher := sha256.New()
	hasher.Write([]byte(time.Now().String()))
	salt := hex.EncodeToString(hasher.Sum(nil))[0:8]

	containerName := "simple-api-" + salt
	log.Printf("Criando um novo container chamado %q", containerName)

	imageSpecs := containerd.WithNewSpec(
    	oci.WithImageConfig(image),
        oci.WithEnv([]string{"PORT=8080"}),
        oci.WithHostNamespace(specs.NetworkNamespace),
        oci.WithHostHostsFile,
        oci.withHostResolvconf,
    	)

Perceba que os specs são, na verdade, as configurações da imagem que estamos querendo executar, por isso estamos passando uma nova configuração chamada oci.WithEnv, onde passamos a string da variável de ambiente.

Além disso temos WithHostNamespace que seta o namespace do container para ser o mesmo que o nosso, também temos WithHostHostsFile e WithHostResolvconf que monta o nosso arquivo /etc/hosts e /etc/resolv.conf no container para que possamos acessar o container de fora, como fizemos com o --net-host.

Inclusive, o código fonte do crt faz a mesma coisa que estamos fazendo agora quando um container é inicializado com a flag --net-host

Após isto, vamos finalizar a função criando o container. A função final ficaria assim:

func createContainer (
	ctx context.Context,
	client *containerd.Client,
	image containerd.Image,
) (containerd.Container, error) {

	hasher := sha256.New()
	hasher.Write([]byte(time.Now().String()))
	salt := hex.EncodeToString(hasher.Sum(nil))[0:8]

	containerName := "simple-api-" + salt
	log.Printf("Criando um novo container chamado %q", containerName)

	imageSpecs := containerd.WithNewSpec(
    	oci.WithImageConfig(image),
        oci.WithEnv([]string{"PORT=8080"}),
        oci.WithHostNamespace(specs.NetworkNamespace),
        oci.WithHostHostsFile,
        oci.withHostResolvconf,
        )

	container, err := client.NewContainer(
		ctx,
		containerName,
		containerd.WithNewSnapshot(containerName + "-snapshot", image),
		imageSpecs,
	)
	if err != nil {
		return nil, err
	}
    
	log.Printf("Criado novo container %q", containerName)
	return container, nil
}

Então chamamos a função na nossa função principal, logo após baixar a imagem:

container, err := createContainer(ctx, client, image)
if err != nil {
	return err
}
defer container.Delete(ctx, containerd.WithSnapshotCleanup)

Estamos realizando a remoção do container logo após a sua execução, similar ao --rm que utilizamos, a função completa fica assim:

func createAPI () error {
	client, err := containerd.New("/run/containerd/containerd.sock")
	defer client.Close()
	if err != nil {
		return err
	}

	ctx := namespaces.WithNamespace(context.Background(), "lsantos")

	image, err := client.Pull(ctx, "docker.io/khaosdoctor/simple-node-api:latest", containerd.WithPullUnpack)
	if err != nil {
		return err
	}
	log.Printf("Imagem %q baixada", image.Name())

	container, err := createContainer(ctx, client, image)
	if err != nil {
		return err
	}
	defer container.Delete(ctx, containerd.WithSnapshotCleanup)

	return nil
}

Você pode ver tudo em ação através dos mesmos comandos go build ./main.go e sudo ./main:

Tasks e containers

Uma segregação importante que é feita no ContainerD é entre os containers e as tasks.

Enquanto um container é um objeto com vários metadados e recursos alocados, uma task é um processo real que está rodando no sistema. Toda a taks deve ser removida após sua execução, mas containers podem ser reutilizados e atualizados múltiplas vezes.

Vamos criar uma nova função createTask para que possamos buscar todo o IO do container e exibí-lo no nosso terminal:

func createIOTask (ctx context.Context, container containerd.Container) (containerd.Task, error) {
	task, err := container.NewTask(ctx, cio.NewCreator(cio.WithStdio))
	if err != nil {
		return nil, err
	}
	return task, nil
}

O que estamos fazendo aqui é importando a biblioteca github.com/containerd/containerd/cio para criar um link que permitirá que toda a saída de informações do nosso container vá para nosso arquivo main.go, vamos chamá-la na nossa função principal logo abaixo da criação do container:

task, err := createIOTask(ctx, container)
if err != nil {
	return err
}
defer task.Delete(ctx)

No momento, a nossa task está no status created, ou seja, está criada mas não iniciada. Vamos inicia-la, mas temos que tomar cuidado para sempre esperar ela finalizar antes de podermos matar a mesma. Vamos adicionar essas linhas na nossa função principal, abaixo de onde chamamos defer task.Delete:

	exitStatus, err := task.Wait(ctx)
	if err != nil {
		log.Println(err)
	}

	if err := task.Start(ctx); err != nil {
		return err
	}

Isso vai garantir que vamos esperar a task finalizar antes de podermos removê-la.

Matando o processo

Como estamos executando um processo que executa sem final (long-running process) vamos dar um tempo para ele executar e mostrar seus logs, assim como o tempo necessário para podermos entrar na nossa API e verificar tudo.

Até agora a nossa função está assim:

func createAPI () error {
	client, err := containerd.New("/run/containerd/containerd.sock")
	defer client.Close()
	if err != nil {
		return err
	}

	ctx := namespaces.WithNamespace(context.Background(), "lsantos")

	image, err := client.Pull(ctx, "docker.io/khaosdoctor/simple-node-api:latest", containerd.WithPullUnpack)
	if err != nil {
		return err
	}
	log.Printf("Imagem %q baixada", image.Name())

	container, err := createContainer(ctx, client, image)
	if err != nil {
		return err
	}
	defer container.Delete(ctx, containerd.WithSnapshotCleanup)

	task, err := createIOTask(ctx, container)
	if err != nil {
		return err
	}
	defer task.Delete(ctx)

	exitStatus, err := task.Wait(ctx)
	if err != nil {
		log.Println(err)
	}

	if err := task.Start(ctx); err != nil {
		return err
	}

	return nil
}

Vamos adicionar as seguintes linhas antes do return nil:

time.Sleep(10 * time.Second)

if err := task.Kill(ctx, syscall.SIGTERM); err != nil {
	return err
}

status := <-exitStatus
exitCode, _, err := status.Result()
if err != nil {
	return err
}

log.Printf("%q foi finalizado com status: %d\n", container.ID(), exitCode)

Estamos esperando 10 segundos (pode aumentar este tempo se for necessário) para poder enviar um comando task.Kill, depois estamos esperando o status da chamada ser retornado através de um Channel para podermos pegar o resultado e exibir na tela.

Concluindo

Podemos agora executar nosso container normalmente, primeiro podemos usar o go build ./main.go e depois sudo ./main.go para poder executar o comando e rodar os containers:

Fluxo de execução completo

Se tentarmos acessar a API pelo browser dentro dos 10 segundos vamos obter o mesmo resultado que tivemos anteriormente:

E é assim que podemos manipular containers usando o runc e o containerd de forma programática e ainda entender um pouco mais sobre como o ecossistema de containers funciona!

Nosso arquivo final ficou assim:

package main

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"log"
	"syscall"
	"time"

	"github.com/containerd/containerd"
	"github.com/containerd/containerd/cio"
	"github.com/containerd/containerd/namespaces"
	"github.com/containerd/containerd/oci"
	"github.com/opencontainers/runtime-spec/specs-go"
)

func main() {
	if err := createAPI(); err != nil {
		log.Fatal(err)
	}
}

func createAPI () error {
	client, err := containerd.New("/run/containerd/containerd.sock")
	defer client.Close()
	if err != nil {
		return err
	}

	ctx := namespaces.WithNamespace(context.Background(), "lsantos")

	image, err := client.Pull(ctx, "docker.io/khaosdoctor/simple-node-api:latest", containerd.WithPullUnpack)
	if err != nil {
		return err
	}
	log.Printf("Imagem %q baixada", image.Name())

	container, err := createContainer(ctx, client, image)
	if err != nil {
		return err
	}
	defer container.Delete(ctx, containerd.WithSnapshotCleanup)

	task, err := createIOTask(ctx, container)
	if err != nil {
		return err
	}
	defer task.Delete(ctx)

	exitStatus, err := task.Wait(ctx)
	if err != nil {
		log.Println(err)
	}

	if err := task.Start(ctx); err != nil {
		return err
	}

	time.Sleep(10 * time.Second)

	if err := task.Kill(ctx, syscall.SIGTERM); err != nil {
		return err
	}

	status := <-exitStatus
	exitCode, _, err := status.Result()
	if err != nil {
		return err
	}
	log.Printf("%q foi finalizado com status: %d\n", container.ID(), exitCode)

	return nil
}

func createContainer (
	ctx context.Context,
	client *containerd.Client,
	image containerd.Image,
) (containerd.Container, error) {

	hasher := sha256.New()
	hasher.Write([]byte(time.Now().String()))
	salt := hex.EncodeToString(hasher.Sum(nil))[0:8]

	containerName := "simple-api-" + salt
	log.Printf("Criando um novo container chamado %q", containerName)

	imageSpecs := containerd.WithNewSpec(
																			oci.WithDefaultSpec(),
																			oci.WithImageConfig(image),
																			oci.WithEnv([]string{"PORT=8080"}),
																			oci.WithHostNamespace(specs.NetworkNamespace),
																			oci.WithHostHostsFile,
																			oci.WithHostResolvconf,
																			)

	container, err := client.NewContainer(
		ctx,
		containerName,
		containerd.WithNewSnapshot(containerName + "-snapshot", image),
		imageSpecs,
	)
	if err != nil {
		return nil, err
	}

	log.Printf("Criado novo container %q", containerName)
	return container, nil
}

func createIOTask (ctx context.Context, container containerd.Container) (containerd.Task, error) {
	task, err := container.NewTask(ctx, cio.NewCreator(cio.WithStdio))
	if err != nil {
		return nil, err
	}
	return task, nil
}
]]>
<![CDATA[ Kubernetes sem Docker? – Entendendo OCI, CRI e o ecossistema de containers ]]> https://blog.lsantos.dev/oci-cri-docker-ecossistema-de-containers/ 5fff0ec568fe9d21e9e66b04 Ter, 02 Fev 2021 10:00:00 -0300 Imagem de capa por ItsVit

Recentemente o time do Kubernetes anunciou que estaria descontinuando o suporte ao Docker a partir da versão 1.22. Neste artigo vamos falar desta alteração e vou explicar o que são todas as ferramentas que temos disponíveis hoje para rodar nossos containers! Então vamos entrar nessa sopa de letrinhas que é o CRI, OCI, ContainerD e muito mais!

Docker e Kubernetes

O Docker e o Kubernetes têm andado de mãos dadas por um bom tempo. Todos os nós de um cluster Kubernetes são equipados com uma implementação do Docker, que torna possível o uso do socket do Docker para várias implementações. Em suma, você pode montar o socket dentro do seu pod e utilizar o "Docker" que está disponível no nó dentro do seu container.

A partir da versão 1.22, o time anunciou que não estaria mais utilizando o Docker como runtime padrão, ou seja, se você está usando o socket do Docker ou então o está montando dentro do pod de alguma forma, então você terá de buscar outras opções. Mas não se preocupe, imagens do docker (produzidas pelo docker build continuarão sendo aceitas pelo cluster normalmente, e já vamos entender por quê. A razão para isto é que, na realidade, o Docker estava causando mais problemas do que soluções.

CRI - Container Runtime Interface

Desde versões mais antigas do Kubernetes, como a 1.3 e a 1.5, já existiam conversas para que o Kubernetes pudesse suportar diversos tipos de runtimes de execução de containers (como o Docker e o RKT). Até aquele momento, ambos os runtimes já eram suportados porém eles estavam tão intimamente integrados com a ferramenta que estavam deixando de ser uma abstração e se tornando parte do que o Kubelet (o daemon de gerenciamento de containers) era.

Abaixo temos um diagrama muito legal feito pelo Michael Brown, da IBM, sobre como podemos imaginar a arquitetura do Kubernetes e aonde o Kubelet se encaixa nisso tudo.

O Kubernetes, entre todas as coisas, é uma ferramenta que inicia e para containers, para isso acontecer, o Kubelet precisa se comunicar com o runtime que está gerenciando os containers. Isto era feito diretamente no código, ou seja, o Kubelet possuía uma camada de código específica para lidar com cada um dos runtimes, sempre que o runtime era trocado, a aplicação precisava ser recompilada e reexecutada.

Além disso, implementar um runtime diretamente abre uma imensa possibilidade de que as ferramentas implementadas (como o Docker, por exemplo), que estão em constante mudança e evolução, acabem quebrando o próprio Kubernetes por conta de suas atualizações.

Para ter a capacidade de trocar de runtime sem precisar compilar toda o cluster novamente, a equipe do Kubernetes criou o que foi chamado de CRI, ou Container Runtime Interface.

O CRI é um plugin que permite que um Kubelet se distancie da camada de runtimes utilizando uma API feita utilizando gRPC. Em suma, a equipe desacoplou a aplicação do Kubelet do runtime, fazendo com que eles sejam inseridos como plugins, bastando que eles implementem a interface gRPC correspondente. Agora, ao invés de o Kubelet ter de se adaptar as mudanças de interface dos runtimes, os runtimes teriam que criar um plugin que seguiria uma interface obrigatória do Kubelet, desta forma a manutenção e a contribuição ficariam muito mais fáceis.

Diagrama de funcionamento do CRI

Não vamos entrar em detalhes da implementação do gRPC, mas a lista de serviços suportados está descrita em um protofile bastante intuitivo:

service RuntimeService {

    // Sandbox operations.

    rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {}  
    rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {}  
    rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {}  
    rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {}  
    rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {}  

    // Container operations.  
    rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {}  
    rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {}  
    rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {}  
    rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {}  
    rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {}  
    rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {}

    ...  
}

Agora basta que o Kubelet acesse a API em gRPC do CRI como um cliente e ele poderá se comunicar com qualquer runtime que implemente essa mesma funcionalidade, desacoplando assim a lógica de lidar com o runtime diretamente do Kubernetes e passando para o CRI responsável que pode ser executado como um plugin.

Para complementar a lista de runtimes suportados, uma série de CRIs foi desenvolvido:

  • CRI-O: um runtime que conforma com a especificação OCI (que já vamos ver)
  • rktlet: runtime para RKT
  • dockershim: O CRI para o Docker (que causou todo esse problema)

O problema

O grande problema é que o Docker foi construído para ser uma aplicação a parte, mantida por uma empresa própria e não foi feita para ser inclusa dentro de um cluster como o Kubernetes (tanto é que o Docker tem seu próprio "Kubernetes" com o Docker Swarm).

Isto acontece porque o Docker em si não é apenas uma única aplicação, mas uma stack inteira: um CLI, APIs, Sockets, o Docker Engine e um runtime chamado ContainerD (mais detalhes neste artigo). O que o Docker fez pela sociedade e pelo mundo dev foi transformar a forma como devs utilizam containers em ambientes Linux (como eu já expliquei neste meu artigo). Porém, isso tudo não serve de nada para o Kubernetes, porque ele não é um humano...

Então, para utilizar só a parte importante do Docker (o containerd), a equipe do Kubernetes precisou desenvolver o que é chamado de Dockershim, um shim (uma biblioteca que modifica chamadas para a API do docker de forma transparente). Isso é péssimo porque adiciona uma nova ferramenta que precisa ser mantida pelo time e outro ponto de falha que pode quebrar todo o ecossistema. E foi o Dockershim que teve sua depreciação na versão 1.20 e vai ser descontinuado na 1.23.

A questão por trás disso tudo é que o Docker não é compatível com o CRI, nunca foi e provavelmente nunca será. Como o próprio time diz:

Se ele fosse, ele não precisaria do shim, e isso não estaria acontecendo.

E agora?

Agora o que resta é encontrar um novo runtime para poder rodar os containers dentro do Kubernetes. Não é uma tarefa difícil, já que o próprio Docker estava utilizando o ContainerD, então basta extrair o ContainerD e utilizá-lo de forma independente!

Muitas pessoas estão se perguntando se esta mudança irá quebrar todo o ecossistema e ninguém mais conseguirá rodar containers no Kubernetes, isso não é verdade. Como falamos nos primeiros parágrafos, o runtime que usamos nas nossas máquinas para construir containers Docker é uma instalação voltada para usuários humanos que prioriza a UX, porém produz uma imagem que é compatível com o OCI (Open Container Initiative) e, para o Kubernetes, toda imagem OCI é a mesma imagem e pode ser executada por um runtime compatível com a mesma especificação.

O que é o OCI?

OCI significa Open Container Initiative, ela é uma estrutura de governança open source formada nos mesmos moldes da Linux Foundation. O objetivo principal da OCI é criar um padrão de mercado seguido por todas as empresas que trabalham com containers de forma que todos sigam as mesmas interfaces para os formatos de containers e imagens. Isto facilita muito a interoperabilidade entre ferramentas e runtimes de forma que uma imagem que seja compatível com a OCI possa ser executada por qualquer runtime também compatível.

Ela foi criada em 2015 pela própria Docker, a CoreOS e outros líderes do mercado de containers.

Em essência, a OCI possui duas especificações, uma para as imagens (chamada de image-spec) e outra para os runtimes (chamada de runtime-spec). A especificação das imagens diz, entre outras coisas, como uma imagem deve ser formada, qual deve ser a estrutura de seu manifesto e quais são as informações específicas da arquitetura do sistema que uma imagem precisa ter para que ela seja executada em qualquer implementação de um runtime OCI. A especificação completa do image-spec pode ser encontrada aqui.

Da mesma forma, a `runtime-spec` descreve como um runtime deve se comportar, incluindo como o filesystem deve gerenciar e descompactar imagens no disco. Também específica algumas regras de UX que devem ser esperadas de qualquer runtime que siga a especificação como, por exemplo, executar uma imagem sem nenhum argumento – como em docker run nginx:latest. Toda a especificação pode ser encontrada aqui.

RunC

Além de criar a especificação, a OCI também mantém uma implementação de sua própria especificação chamada runc, que foi doada pela Docker no início do projeto.

O RunC é o runtime padrão por trás tanto do ContainerD quanto do CRI-O e é atualmente uma das mais abrangentes implementações existentes. Podemos ver a implementação do ContainerD e do RunC diretamente na arquitetura do Docker na imagem abaixo presente neste artigo do Docker:

Kubernetes depois do Docker

Depois de depreciar a interface do Dockershim, o uso do ContainerD diretamente faz com que a arquitetura geral do Kubernetes seja muito mais simples de entender.

Isso vem em partes porque o próprio ContainerD já possui uma integração com o CRI via plugin que é habilitado por padrão, então o Kubelet se comunica com o plugin do CRI do ContainerD que, por sua vez, executa a implementação do runc e então os containers propriamente ditos, conforme o diagrama abaixo, também do Michael Brown:

Diagrama de funcionamento do ContainerD com o Kubelet

O daemon do ContainerD lida com todas as chamadas do Kubelet através do plugin do CRI, chamando os shims necessários para modificação (que são mantidos pelo proprio time do ContainerD) e executando os containers através do  runc.

Conclusão

Neste artigo não mostramos tanto sobre o ContainerD quanto eu gostaria, mas estou quebrando ele em duas partes para que possamos ter um artigo específico onde podemos falar um pouco mais só sobre o ContainerD!

Espero que vocês tenham gostado! Não se esqueçam de se inscrevem no newsletter para receber conteúdos, promoções e novidades!

]]>
<![CDATA[ Podcast #FalaDev - Vamos falar de Microsserviços ]]> https://blog.lsantos.dev/podcast-faladev-vamos-falar-de-microsservicos/ 5ff8955368fe9d21e9e66ab5 Sex, 08 Jan 2021 14:42:39 -0300 Tive o enorme prazer de ser convidado para fazer parte do incrível podcast #FalaDev, da RocketSeat, juntamente com o Wesley Willians para podermos falar sobre Microsserviços!

Neste podcast falamos sobre como podemos entender microsserviços, como eles funcionam, quais são os casos clássicos, como podemos melhorar nossas arquiteturas e como podemos ser muito mais eficientes em termos de custo e também de projetos usando microsserviços!

]]>
<![CDATA[ Somos 4k! Muito obrigado! ]]> https://blog.lsantos.dev/somos-4k-muito-obrigado/ 5fc9622b82fe71827326670f Qua, 09 Dez 2020 14:33:25 -0300 Eu gostaria de agradecer a todas as pessoas que fizeram parte desta conquista! Somos 4.000 pessoas no Twitter que adoram tecnologia e gostam de conversar e descobrir cada vez mais conteúdo!

Muito obrigado!

Quando comecei a escrever, por volta de 2016, não esperava que fosse atrair ninguém, eu nem sequer imaginava que um dia alcançaria tantas pessoas que se interessariam pelo meu conteúdo. Agora, trabalhando integralmente com conteúdo e aprendendo cada vez mais para poder levar mais conhecimento a todos eu entendo que esta é de fato a profissão que eu quero seguir!

Vai ter bolo?

Para comemorar este marco, eu vou estar realizando um sorteio! Para cada mil seguidores vou sortear um livro da casa do código a sua escolha, ou seja, vou sortear 4 livros físicos para 4 pessoas além de um pack de 5 stickers aleatórios! E mais um pack de 10 stickers para outras 4 pessoas sorteadas!

Para participar é muito simples, você so precisa ser uma das pessoas inscritas na newsletter! Para fazer isso é muito simples:

  1. No canto inferior direito da tela existe um botão "Assine a Newsletter" é só clicar nele!
  2. Coloque seu nome e e-mail
  3. Clique em "Enviar"

É simples assim! Se você já é assinante, então não precisa fazer mais nada!

Como funciona?

O sorteio vai acontecer no dia 15 de Janeiro de 2021, assim todos vão ter tempo para poder se cadastrar na lista e receber os emails! Você pode se cadastrar na newsletter até o dia do sorteio!

Os resultados do sorteio serão postados no meu Twitter e em todas as minhas outras redes sociais, bem como em uma newsletter especial. Assim que as pessoas ganhadoras forem anunciadas, vou enviar um email pessoal para cada uma delas pedindo informações sobre o livro que quer ganhar e também o endereço para entrega!

Se a pessoa não foi sorteada para os livros mas sim para o pack de stickers, vou entrar em contato da mesma forma para requisitar o endereço de entrega!

Assim que os livros forem enviados, eu estarei entrando em contato novamente para informar os códigos de rastreio e as últimas novidades :D

E se eu não ganhei :(

Não tem problema! Aqui todos vão ganhar! Todas as pessoas inscritas na newsletter vão receber códigos de descontos surpresas para facilitar ainda mais o aprendizado!

Vai ter mais?

Claro! A cada marco de 1000 novos seguidores vou fazer uma pequena ação para podermos comemorar o crescimento da nossa comunidade! A cada vez tentarei conseguir novas parcerias e novos sorteios!

Se você é parte ou conhece alguém que poderia ajudar a fazer os sorteios ainda mais legais! É só entrar em contato comigo através do meu email!

Obrigado!

Eu gostaria de agradecer a todas as pessoas que me acompanharam desde o início até aqui, este blog, todo o meu conteúdo e tudo que eu faço não seria nada sem alguém para discutir e ler minhas ideias. Então

MUITO OBRIGADO!

]]>
<![CDATA[ gRPC com Node.js no EnterJS 2020 ]]> https://blog.lsantos.dev/grpc-com-node-js-no-enterjs-2020/ 5fc945b782fe7182732666e3 Qui, 03 Dez 2020 17:55:44 -0300 Em setembro tive o prazer de participar de uma conferência online feita na Alemanha onde falei um pouco mais sobre gRPC com Node.js na parte teórica e dei alguns exemplos de como podemos codar a nossa aplicação utilizando o modelo gRPC de comunicação.

Aproveitando que o DoWhile vem ai! Veja a parte teórica antes da parte prática nessa palestra!

]]>
<![CDATA[ Entenda a comunicação entre serviços com gRPC no DoWhile 2020 ]]> https://blog.lsantos.dev/entenda-a-comunicacao-entre-servicos-com-grpc-no-do-while-2020/ 5fc5044482fe718273266672 Seg, 30 Nov 2020 11:52:51 -0300 Estou super feliz de anunciar que fui convidado pela RocketSeat para fazer parte do grupo de palestrantes no maior evento deles, o DoWhile 2020!

O evento é totalmente gratuito e acontece nos dias 14 e 15 de Dezembro, então já sabe, deixa sua agenda pronta para não esquecer! O evento é totalmente online e tem como objetivo reunir todo o ecossistema de desenvolvimento em busca do aprendizado contínuo!

Quem quiser se inscrever, lembrando que é gratuito, é só acessar o link abaixo.

Lucas Santos | DoWhile 2020
Junte-se a Lucas Santos no DoWhile, um evento online que vêm com a missão de reunir todo o ecossistema de programação em busca de um mesmo propósito: o aprendizado contínuo.

Junto comigo vão estar outros profissionais incríveis do mundo da tecnologia, entregando conteúdos em forma de palestras, workshops, painéis e muito mais!

Neste evento vou estar ensinando a fazermos comunicações entre microsserviços utilizando gRPC de forma simples e prática em um workshop totalmente prático e hands on, então quem estiver curioso para saber como você pode parar de usar o ReST em suas APIs e começar a tipar todo o seu ecossistema, então esta é a talk!

Vejo vocês por lá!

]]>
<![CDATA[ Por Que Black Fridays Dão Errado? - Hipsters.talks ]]> https://blog.lsantos.dev/por-que-black-fridays-dao-errado/ 5fc4647482fe718273266646 Seg, 30 Nov 2020 00:21:41 -0300 Tive o enorme prazer de ser o convidado surpresa neste quadro do Hipsters que adoro! O Gabs Ferreira me chamou para participar de um papo super descontraído para discutirmos junto com o Mário e a Patrícia sobre histórias de Black Friday.

Foi um papo super bacana e ele está totalmente disponível de graça neste link! Assista e dê seus comentários!

E você? Tem alguma história de black friday para contar?

]]>
<![CDATA[ Entendendo Threading no Node.js com Rodrigo Branas ]]> https://blog.lsantos.dev/entendendo-threading-no-node-js-com-rodrigo-branas/ 5fb7e5fa82fe71827326661f Sex, 20 Nov 2020 13:31:32 -0300 No dia 9 de Novembro tive o prazer de ser convidado pelo excelente Rodrigo Branas para participar de uma live super animada no canal dele.

Nesta live, ficamos mais de 2 horas falando sobre como funcionam os processos em threading no Node.js e também como podemos tirar vantagem da API de clusters e Worker Threads!

Mostramos códigos, falamos da nossa experiência e também tiramos muitas duvidas sobre como threading funciona nos computadores em geral! Veja a live na íntegra logo abaixo!

]]>
<![CDATA[ Atualização Automática de SO com Unattended Upgrades ]]> https://blog.lsantos.dev/atualizacao-automatica-de-so-com-unattended-upgrades/ 5fabfbb382fe7182732665b8 Qua, 11 Nov 2020 13:10:23 -0300 Quando estamos criando máquinas virtuais, um dos maiores problemas que temos é manter os nossos sistemas operacionais atualizados e livres de bugs e falhas de segurança.

Na maioria dos casos, o SO já possui um sistema interno para atualização automática, porém, quando estamos usando um sistema operacional baseado em pacotes como, por exemplo, o Linux, digamos um Ubuntu 18.4 Server, temos que constantemente rodar comandos como apt update e apt upgrade para instalar as últimas versões dos pacotes do sistema e assim atualizar para a última versão e corrigir possíveis falhas de segurança.

O problema

O grande problema com estas abordagens é que precisamos constantemente entrar na máquina, rodar o comando e sair. Para solucionar isso, temos as famosas crontabs. Podemos por exemplo executar o seguinte comando:

crontab -e

Para editar a nossa crontab, e ai podemos cadastrar a seguinte linha:

0 3 */5 * * sudo apt update && sudo apt upgrade -y

Esta linha vai fazer com que executemos os comandos de atualização a cada 5 dias as 3 da manhã. É uma boa prática realizar a atualização sempre fora dos horários de utilização da máquina, então esta hora pode ser definida por você sem problemas.

Da mesma forma, a frequência de execução fica a critério de quem está configurando a máquina, eu costumo executar o processo a cada 5 dias porque geralmente não temos grandes atualizações diariamente.

Porém, se tivermos outras atualizações que necessitam de um reboot, por exemplo, atualizações de Kernel, então vamos ter que entrar e reiniciar a máquina manualmente... Deve haver um jeito mais simples de fazer isso, não?

Unattended Upgrades

O Ubuntu (e acredito que a maioria dos sistemas baseados no Debian), possuem um pacote chamado unattended-upgrades, que pode ser combinado com alguns outros pacotes para prover uma funcionalidade sensacional em termos de segurança e atualização de SO.

Para começar, vamos remover a nossa crontab criada anteriormente e deixar o sistema limpo de novo. Depois, vamos instalar os seguintes pacotes:

sudo apt install -y unattended-upgrades apt-listchanges bsd-mailx

O bsd-mailx irá pedir algumas configurações iniciais para setar o seu email, estas configurações são específicas de máquina para máquina, mas o ideal é que você escolha a opção Internet Site para poder configurar o FQDN do seu próprio domínio. Se você precisar realizar alguma reconfiguração do pacote pois ele não está funcionando, use o comando a seguir:

sudo dpkg-reconfigure -plow postfix

Isto fará com que a janela de configuração seja aberta novamente, se você prefere reconfigurar utilizando o arquivo de configuração, basta editar o arquivo /etc/postfix/main.cf, não se esqueça de executar um restart do postfix depois de salvar o arquivo com o comando:

sudo systemctl restart postfix

Agora, vamos ativar o pacote para as atualizações estáveis usando o seguinte comando:

sudo dpkg -plow unattended-upgrades

Então podemos abrir o arquivo de configuração, use o seu editor preferido para editar o arquivo /etc/apt/apt.conf.d/50unattended-upgrades. Este arquivo contém todas as configurações necessárias para definirmos a funcionalidade de atualização do pacote, primeiro, vamos configurar nosso email para recebermos notificações de atualizações importantes, para isso vamos setar a chave Unattended-Upgrade::Mail com o nosso email escolhido, ficando Unattended-Upgrade::Mail "hello@lsantos.dev";.

Agora vamos configurar uma série de outras chaves para podermos obter o máximo do pacote:

Unattended-Upgrade::Automatic-Reboot "true";  # Para reiniciar o sistema após uma atualização de Kernel
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00"; # Que horas queremos que o sistema reinicie

Agora podemos rodar o comando sudo unattended-upgrades --dry-run para poder testar se as nossas configurações estão corretas. Este comando não deve ter nenhuma saída, se este é o resultado então está tudo certo!

Conclusão

Com a instalação destes pacotes podemos ficar mais tranquilos em relação a atualizações de sistemas operacionais e também podemos manter as nossas VMs atualizadas de forma mais concisa.

Se você leu o último artigo sobre criar uma VPN própria, a aplicação desta técnica e também 2FA usando SSH podem ser uma boa pedida para deixar a sua VM rodando tranquilamente!

Até mais!

]]>
<![CDATA[ Aplicando Two Factor Authentication no SSH ]]> https://blog.lsantos.dev/aplicando-two-factor-authentication-no-ssh/ 5fa45a4f82fe7182732664fa Qui, 05 Nov 2020 19:07:56 -0300 Já estamos muito acostumados com SSH para logarmos em máquinas virtuais, como fizemos no nosso post onde criamos uma VPN. Porém, sabemos que o SSH aceita diversos níveis de segurança quando se trata de acesso local.

O primeiro nível de segurança – e o mais fraco – é uma senha alfanumérica, este é o meio mais fraco porque a comunicação entre o seu computador e o servidor, mesmo sendo pelo protocolo do SSH, ainda transmite sua senha em texto plano, um ataque bem direcionado poderia capturar a sua senha e utilizá-la, se você não estiver utilizando nenhum túnel.

O segundo nível de segurança, e também o que mais utilizamos, é a combinação de chaves RSA, este tipo de segurança permite que você tenha mais facilidade para gerenciar acessos, por exemplo:

  • Podemos permitir o acesso de várias pessoas à mesma máquina sem ter uma senha trafegada por elas.
  • Podemos revogar as credenciais de uma única pessoa sem precisar alterar as demais.
  • Sendo sincero, é muito melhor não ter que lembrar uma senha do que ter que lembrar uma, não é?

A grande vantagem das chaves assimétricas é que você pode provar que você é o dono de uma chave privada sem precisar, de fato, mostrar a chave para ninguém, ou seja, você pode dar uma chave pública ao servidor e, quando você for logar, o servidor irá perguntar se você possui a chave privada que faz a contraparte daquela chave pública, se você possuir, então você está dentro.

Apesar disso, muitas pessoas ainda tem um grande problema porque precisam gerenciar estas chaves em algum lugar, manter a sua chave privada em segredo e segura é uma tarefa muito complicada. Então podemos adicionar uma nova camada de segurança para aumentar ainda mais a resiliência de um servidor importante. O 2FA, ou, Two-Factor Authentication.

Two Factor Authentication

O TFA (ou 2FA) é uma técnica de segurança que exige que você utilize um outro dispositivo (geralmente um celular) para confirmar um código de uso único, chamado de OTP, o One-Time Password.

Este código é gerado seguindo algumas implementações matemáticas que não vamos discutir aqui, mas ele está baseado em tempo, ou seja, desde que o seu relógio e o relógio do servidor estejam sincronizados, vocês dois irão gerar o mesmo código que valerá por um determinado tempo. Se você digitar o mesmo código que o servidor gerar, então você é o dono do OTP.

Por que usar TFA?

O uso de senhas se tornou algo comum e a principal forma de autenticação. Utilizar algo que só você sabe de forma que só você tenha acesso a um determinado tipo de dado é um conceito muito simples e todos podem entender. Porém, com o tempo, o uso de senhas se tornou muito comum com a Internet, tudo que temos pede uma conta com uma senha ou então algum tipo de autenticação.

O ideal seria que você gerasse senhas diferentes para serviços diferentes, porém é impossível lembrar todas essas senhas,  por isso temos serviços como o 1Password, Dashlane ou LastPass, para que possamos gerar senhas aleatórias que não precisamos lembrar, pois eles se encarregarão de logar para a gente no momento correto e também de manter os dados seguros.

O que acontece é que muitas pessoas não sabem ou não tem como utilizar estes serviços pelos mais variados motivos e acabam utilizando a mesma senha para diversos serviços. Desta forma, se você usa a sua senha em um serviço que tem uma boa segurança, digamos a Azure, mas também usa a mesma senha para serviços de qualidade duvidosa, quando o elo mais fraco se romper, os hackers só precisarão de uma senha para acessar todas as suas contas.

E é por isso que o TFA é tão importante. No geral, o TFA tenta melhorar a segurança adicionando uma camada extra de autenticação, que pode ser uma entre três fatores:

  • Algo que você sabe
  • Algo que você tem
  • Algo que você é

Algo que você sabe seria a sua senha, o que não é interessante. Algo que você tem seria um celular ou um outro dispositivo, portanto utilizar a combinação "Algo que eu sei + Algo que eu tenho" se tornou muito popular. Ultimamente estamos vendo também a combinação "Algo que eu sei + Algo que eu sou" com autenticações biométricas ou por identificação de face.

Por que usar TFA em um servidor

Pelo mesmo motivo que você utiliza TFA para proteger suas contas de e-mail: aumentar a segurança.

Imagine que você tenha uma empresa e esta empresa tenha uma tecnologia que valha milhões, ou então possua dados muito sensíveis armazenados em um servidor. Automaticamente este se torna um alvo muito cobiçado por hackers, então é importante adicionar camadas extras de proteção.

Aplicando TFA em um servidor

Chega de conceitos, vamos colocar a mão na massa! Para começar, vou assumir que você já tem um servidor rodando, pode ser uma VM da Azure, uma VM local, um Raspberry Pi, você que escolhe.

Além disso é importante que este servidor já esteja configurado para utilizar chaves SSH como meio de entrada. Desta forma não temos como logar através de uma senha, a maioria dos provedores cloud hoje permitem que você coloque sua chave pública como meio de autenticação na VM.

Configurando o SSH

Antes de começarmos, preciso dizer que não podemos executar os comandos como sudo su , sempre utilize sudo quando for necessário dentro do usuário que você irá logar.

Primeiramente, vamos abrir o arquivo /etc/ssh/sshd_config com o nosso editor de texto preferido. Você vai ver um arquivo com várias configurações. A primeira delas é a configuração da porta, que deve estar como #Port 22, você pode mudar a configuração para aumentar ainda mais a segurança, uma vez que a porta padrão do SSH é bem conhecida, basta remover o #. Esta porta pode ser qualquer uma que você escolher, você so precisa lembrar de abri-la no seu provedor e no seu firewall.

Procure a linha PermitRootLogin, ela deve estar com o valor padrão #PermitRootLogin prohibit-password. Altere este linha para PermitRootLogin no.

Agora, vamos alterar a linha #PubkeyAuthentication yes, removendo o # para ativar a configuração.

Não vamos reiniciar o servidor ainda, vamos primeiro garantir que conseguimos instalar o nosso sistema de TFA corretamente.

Instalando o TFA

Se você estiver utilizando qualquer tipo de sistema baseado em Debian, no meu caso estou usando o Ubuntu Server 18.4, basta instalar o pacote libpam-google-authenticator com o comando:

sudo apt install -y libpam-google-authenticator
Se você não está usando uma distro baseada no Debian, ou não está conseguindo instalar a biblioteca, procure por "libpam-google-authenticator <suadistro>" para obter instruções corretas de instalação.

Execute o script digitando google-authenticator na linha de comando, ele vai te fazer várias perguntas, as quais você pode só responder yes para todas, com exceção de duas.

Para a pergunta:

Do you want to disallow multiple uses of the same authentication
token? This restricts you to one login about every 30s, but it increases
your chances to notice or even prevent man-in-the-middle attacks (y/n)

Responda n. Esta pergunta está perguntando se queremos desativar o login com tokens iguais ao mesmo tempo, se sim, você só poderá logar uma vez que um novo token for gerado a cada 30s, se não, poderá logar a qualquer momento. Se este é um servidor muito importante, recomendo que você digite y, no nosso caso estamos ignorando esta opção.

Para a pergunta:

By default, a new token is generated every 30 seconds by the mobile app.
In order to compensate for possible time-skew between the client and the server,
we allow an extra token before and after the current time. This allows for a
time skew of up to 30 seconds between authentication server and client. If you
experience problems with poor time synchronization, you can increase the window
from its default size of 3 permitted codes (one previous code, the current
code, the next code) to 17 permitted codes (the 8 previous codes, the current
code, and the 8 next codes). This will permit for a time skew of up to 4 minutes
between client and server.
Do you want to do so? (y/n)

Vamos digitar n, ela permite que  utilizemos o mesmo token anterior até alguns segundos depois que ele perdeu a validade para compensar a diferença de tempo entre o servidor e o cliente, enquanto isso ajuda a manter as coisas simples, ele também reduz a segurança.

No final, você terá uma imagem parecida com esta:

Resultado da criação de um autenticador

Basta apontar um aplicativo de TFA como o Authy, 1Password ou até mesmo o Google Authenticator para o QR Code para poder cadastrar a nova senha, ou então utilizar a Secret Key para poder gerar a mesma em aplicativos que não possuem leitura de QR Codes.

Lembre-se de anotar os códigos de emergência e o código de verificação e mantê-los em locais seguros!

Vamos agora editar o arquivo de configuração do pacote para podermos habilitar o uso de TFA no login, vamos abrir o arquivo /etc/pam.d/sshd. Este arquivo vai ter um código já pronto, vamos fazer as seguintes alterações:

  1. Comente a linha que diz @include common-auth, isto significa que o TFA não vai perguntar uma senha além do OTP.
  2. No final do arquivo, adicione a seguinte linha: auth required pam_google_authenticator.so
  3. Salve e feche o arquivo

Agora volte no arquivo de configuração do SSH em /etc/ssh/sshd_config, vamos deixar o SSH a par do novo método de autenticação.

Procure a linha ChallengeResponseAuthentication no e mude-a para ChallengeResponseAuthentication yes. Depois procure a linha UsePAM no e mude para UsePAM yes.

Agora vamos setar os métodos de entrada para o SSH, para isso procure a linha AuthenticationMethods, se ela não existir, adicione-a logo após a linha UsePAM. Nesta linha escreva:

AuthenticationMethods publickey,keyboard-interactive

Vamos reiniciar o serviço do SSH para poder ativar as alterações com o comando a seguir:

sudo systemctl restart sshd

Agora, em um outro terminal, tente fazer login no seu servidor, você deverá ver algo assim:

Agora basta digitar o seu código de autenticação e então você estará logado!

Conclusão

Aplicar TFA nos seus servidores pode parecer uma medida drástica, porém ela ajuda a manter toda a sua infraestrutura mais segura de forma que você poderá ter certeza de que seus dados estarão mais seguros do que somente com uma autenticação simples com chaves ou senhas!

]]>
<![CDATA[ Tenha privacidade total com sua própria VPN hospedada na cloud ]]> https://blog.lsantos.dev/criando-uma-vpn/ 5f92ee1a82fe718273266332 Ter, 27 Out 2020 14:51:53 -0300 Quer estejamos trabalhando ou simplesmente navegando na Internet, um dos principais questionamentos que temos é se o nosso tráfego de rede está realmente seguro ou se estamos sendo espionados pelos nossos próprios provedores de Internet.

Com base nisso, decidi utilizar uma VPN paga há alguns anos, a PIA VPN, porém eu acabei cancelando o uso desta VPN por dois fatores simples:

  • Depois de um tempo eles pararam de ter servidores brasileiros por conta de políticas internas do país com relação a retenção de logs, então o servidor mais próximo era o dos EUA, o que adicionava um ping de 120ms para cada requisição
  • A PIA foi recentemente adquirida por uma companhia israelense que tem um histórico que é, digamos... Um pouco duvidoso em relação a espionagem

Obviamente estes motivos foram totalmente pessoais, a PIA é uma ótima VPN porém, para mim, não estava mais funcionando. Então por um tempo fiquei sem nenhuma VPN até ter uma ideia de tentar construir a minha própria, já que não haviam outros servidores que não guardavam logs aqui no Brasil.

Iniciando o processo

Para começar o processo, eu comecei a pesquisar alguns tipos de tutoriais e ideias de de como começar a criar minha própria VPN, juntando alguns deles consegui um processo bastante rápido e bastante conciso para poder criar a minha própria VPN e ter total certeza que não há alguém espionando os logs por trás.

Aqui estão alguns dos artigos que utilizei para poder criar a VPN, alguns deles explicam muito bem as razões por trás de tudo, outros simplesmente são tutoriais passo a passo:

Por que eu deveria construir minha própria VPN

Assim como já falamos no parágrafo anterior, a principal razão para você querer setar a sua VPN própria é justamente a questão de que você está no controle de todos os logs e todas as ações dentro do servidor. Então não há como ter alguém espionando suas ações no meio do caminho.

Além disso, ter uma VPN própria – principalmente para os brasileiros – implica que, se você estiver utilizando uma VPN por pura segurança, você vai conseguir acesso a redes públicas e até mesmo sem senha sem precisar se preocupar com segurança no geral, porque você vai ter um servidor local que adiciona pouquíssimo de latência a sua conexão, ou seja, essencialmente, o túnel criptografado que você irá criar entre seu computador e a sua VPN vai atuar somente como uma camada de criptografia extra.

Menos importante do que isso também, é a capacidade que você terá de permitir que outras pessoas tenham acesso a sua rede através do mesmo túnel, então você poderá compartilhar arquivos locais ou até mesmo jogar jogos desenhados para a LAN através da Internet.

Problemas de criar sua própria VPN

O maior uso de VPNs atualmente infelizmente não é para se proteger de olhos alheios, mas sim para poder trocar a sua localização geográfica de forma que você aparentará estar em outros países, quebrando assim algumas travas de Geo-IP que alguns sites possuem, que impedem você de consumir algum conteúdo que será destinado a um país específico, por exemplo.

Enquanto é possível criar um servidor utilizando provedores de cloud como a Azure, que providenciam várias máquinas virtuais em diversos locais do mundo, ainda sim o custo que você terá para criar um servidor de VPN em cada região é extremamente alto comparado ao custo de você contratar um serviço de VPN pronto, como a NordVPN ou a TunnelBear. Embora seja completamente possível se você estiver disposto a gastar algumas centenas de reais por mês.

Você também pode criar diversos servidores e deixar os mesmos desalocados (parados), o que consome muito menos recursos e gasta muito menos, ligando o servidor somente quando for utilizar ou agendando um auto desligamento.

Criando sua própria VPN

Com isso tudo dito, vamos partir para a criação da nossa própria VPN, o primeiro passo é criar um servidor onde possamos acessar a VPN a partir da Internet, existem várias maneiras de se fazer isso, uma delas (que vou postar aqui assim que terminar) é utilizar um RaspberryPI como servidor e o serviço do No-IP para poder expor o mesmo para a Internet.

Como vamos criar o nosso próprio servidor e não vamos armazenar logs, não há problema em criarmos em serviços de cloud como a Azure, então vamos começar criando um servidor por lá.

Você pode utilizar outros serviços cloud também, como a DigitalOcean, Linode ou qualquer outro que você se sinta mais a vontade utilizando.

Criando o servidor

Para criarmos o nosso servidor, você primeiro precisa ter uma conta na Azure, se é a sua primeira vez então você vai ganhar alguns créditos que vão te ajudar a não precisar pagar pelo servidor, pelo menos nos primeiros meses.

Ao entrar no portal da Azure, pesquise no topo por "Virtual Machines", clique no ícone e você deverá cair em uma lista de máquinas virtuais, que provavelmente estará vazia, clique no botão "Add" logo abaixo do título:

Adicionando uma máquina virtual

Selecione "Virtual Machine":

Clique em "Add" e então em "Virtual Machine"

Quando você selecionar a opção, vamos ter um formulário para ser preenchido, é importante que você preste atenção no que vamos colocar por aqui. Primeiro, vamos ter uma ideia de como este formulário se divide.

A primeira seção são dados gerais do projeto:

Agora atente-se ao que vamos colocar em cada campo:

  • Subscription: isto vai vir preenchido automaticamente se você só tiver uma conta da Azure, se não, é a conta que você deseja ser cobrado.
  • Resource Group: É uma boa prática criar um novo resource group para armazenar os dados da VPN, você pode clicar no link "Create New" logo abaixo para poder criar o novo RG.
  • Virtual Machine Name: É o nome da sua máquina, aqui você pode ser uma pessoa criativa e escrever o que achar mais conveniente, só é preciso lembrar dele depois.
  • Region: Aqui é a parte mais importante, onde você vai criar o seu servidor, lembrando que você precisa levar em consideração as leis locais de cada país, então se você quer acessar conteúdo disponível nos EUA, crie um servidor nos EUA, se você quer baixar algum torrent, evite países presentes na lista dos 14 eyes, vamos de Brasil mesmo.
  • Availability Options: Neste campo definimos a redundância de rede, não vamos precisar disso então vamos manter como está.
  • Image: Vamos usar o Ubuntu por comodidade em instalar os scripts e a VPN, mas se você tiver habilidade com Linux, pode usar qualquer distro que quiser.
  • Azure Spot Instance: Há uma categoria de VMs na Azure que utilizam a capacidade não utilizada da cloud a um preço bem menor, as chamadas Spot Instances, porém o problema é que elas não tem garantia de disponibilidade, e seu servidor pode ser desalocado se a capacidade for necessária, queremos que o servidor seja o mais disponível possível, certo? Então vamos deixar isso aqui como "No".
  • Size: Essa é a parte onde teremos que decidir o tamanho da nossa máquina, a Azure te dá vários tipos e tamanhos diferentes de máquinas, a mais comum é a DS2_V3, porém ela é um canhão para o que queremos fazer, vamos selecionar então a máquina B1s que possui somente 1 núcleo e 1 Gb de RAM.
Escolha o tamanho apropriado de máquina

Na próxima seção, vamos ter as configurações de autenticação.

Aqui temos um disclaimer importante. Vou utilizar o "password" para que todos saibam o que fazer quando para trocar para um acesso via chave SSH, uma vez que a senha é trafegada em texto puro, portanto pode ser um meio de ataque para hackers.

O que vamos fazer é setar uma senha inicial para fazer o login, porém a primeira alteração no servidor será alterar o SSH para que possamos acessar em portas diferentes e também através de uma chave SSH ao invés da senha.

Se você já é familiarizado com os métodos de login do SSH e já sabe como fazer a alteração, então você pode marcar "SSH Public Key" para evitar um retrabalho.

Por fim temos as configurações de portas. Vamos deixar somente uma das portas habilitadas, a 22 que é a porta padrão do SSH:

Ao clicar em "Next" vamos para a seção de discos, não vamos incluir nenhum disco novo, vamos apenas trocar o disco que vem por padrão de "Premium SSD" para "Standard HDD", pois não precisamos de velocidade e o HD é muito mais barato:

Vamos dar um "Next" e passar batido pela seção de rede, pois não vamos mudar nada, na seção "Management", vamos desativar todas as opções:

Desativamos todas as opções na seção de gerenciamento

Então clicamos no botão azul do lado esquerdo inferior "Review + Create". Após uma rápida revisão de configurações, seu servidor será criado, o processo em si demora alguns minutos, mas assim que estiver finalizado você poderá navegar para a tela do recurso, e verá algo parecido com isto:

Painel de controle da VM

Protegendo o servidor

Como comentamos anteriormente, acessar um servidor de segurança utilizando senha é uma hipocrisia, então vamos gerar as nossas chaves SSH para podermos acessar o servidor de forma segura.

Se você estiver usando o Windows, abra o PowerShell e instale o OpenSSH com o seguinte comando:

PS C:\> Add-WindowsCapability -Online -Name OpenSSH.Client*

Se você estiver em Mac ou Linux, basta abrir o terminal que o OpenSSH deverá estar instalado por padrão. Se não estiver, busque como instalar o OpenSSH para a sua distro antes de continuar.

Vamos usar o seguinte comando para gerar uma chave:

ssh-keygen -t rsa -b 4096

Pressione ENTER quando for perguntado aonde você quer salvar a chave para salvar no diretório padrão (que é geralmente ~/.ssh), se não selecione um local aonde você terá acesso para deixar as suas chaves (você pode ter problemas no futuro se não colocar no diretório padrão).

Você será questionado por uma senha na sua chave, ela é completamente opcional, mas adiciona um nível extra de segurança, se quiser adicionar, pode ficar a vontade.

Vamos logar no servidor para poder realizar as alterações, para isso é só utilizar o comando:

ssh usuario@ip

Lembrando que o usuário é o mesmo usuário que você colocou quando criou o servidor, e o IP é o endereço de IP público que aparece como um link no seu painel da VM na Azure.

Uma vez dentro do servidor vamos atualizar o sistema operacional e todo o sistema com os clássicos:

sudo apt-get update && sudo apt-get upgrade

Depois vamos instalar um editor de texto para que possamos editar os arquivos que vamos precisar, aqui a escolha é pessoal, eu gosto de utilizar o Vim, mas você pode instalar o que for melhor para você.

sudo apt-get install -y vim

Vamos criar um novo usuário não root para podermos utilizar como login:

sudo useradd -G sudo -m nomedousuario -s /bin/bash

Depois vamos criar uma senha para este usuário:

passwd nomedousuario

Agora, não desconecte do SSH do seu servidor e abra um novo terminal local, vamos transferir a nossa chave pública para dentro do servidor para que ele possa realizar o login.

Para isso, no Linux ou Mac vamos executar o seguinte comando:

ssh-copy-id usuario@ip

No Windows você precisará utilizar outro comando

type $env:USERPROFILE\.ssh\id_rsa.pub | ssh seuip "cat >> .ssh/authorized_keys"

Mantenha os dois terminais abertos, vamos agora restringir o acesso a quem está usando senha, e também vamos atualizar a porta do SSH para que ele não fique exposto na porta 22, que é padrão.

A primeira coisa que vamos fazer é abrir o arquivo /etc/ssh/sshd_config dentro do servidor da VPN. Vamos então procurar a linha Port 22 e vamos alterá-la, aqui estou usando a porta 78, mas você pode utilizar qualquer porta que queira e não esteja utilizada por outro serviço:

# Port 22
Port 78

No painel da Azure, vamos entrar nas configurações de rede para abrir a nova porta. Para isso, no painel da VM, vá na barra lateral e clique em "Networking":

Você verá uma lista com todas as portas que estão abertas e todas as regras de rede para o seu IP ordenadas por prioridade. Clique no botão azul "Add inbound port rule":

Preencha as informações alterando o "Destination port ranges" para o número da porta que você escolheu, defina a "priority" como 100 e dê um nome identificável para esta regra de rede. Salve.

Já vamos aproveitar também e abrir a próxima porta que vamos utilizar, que vai ser a porta 443 UDP, que é utilizada pelo OpenVPN para poder realizar a conexão e o tráfego de dados. Clique novamente no botão para adicionar outra regra:

Desta vez, vamos setar o "Destination port ranges" para 443 e selecionar o "protocol" como UDP, deixamos a prioridade do jeito que está e damos um nome identificável. Agora temos o acesso liberado para as duas portas principais, não feche esta aba ainda, nós vamos ter que voltar aqui em instantes.

Voltando ao servidor, vamos continuar alterando o nosso arquivo de configurações. Agora vamos procurar por PasswordAuthentication e vamos desativar o login através de senha:

PasswordAuthentication no

Vamos também desabilitar o login como root:

PermitRootLogin no

Vamos salvar o arquivo e reiniciar o serviço utilizando:

sudo systemctl restart sshd

Não feche o terminal que está logado no servidor ainda, pois se tivermos problemas, não queremos ficar trancados para fora não é mesmo? Abra um novo terminal e vamos tentar logar na máquina com o novo usuário através da nova porta:

ssh -i ~/.ssh/id_rsa novousuario@ip -p porta

Se você conseguir logar sem digitar nenhuma senha, ou então um prompt para digitar a senha da sua chave, então está tudo certo. Você pode tentar fazer um teste para saber se podemos também logar sem uma chave:

ssh novousuario@ip -p porta

Isto deve te dar um "Permission Denied"

Agora sim podemos fechar o terminal anterior que tínhamos logado como root, vamos voltar ao portal da Azure e remover a regra padrão de acesso à porta 22 que foi criada. Para isso, é só clicar nos três pontos no final da linha correspondente e selecionar o botão "Delete".

(Opcional) Criando um alias

O SSH permite que você crie um alias para se conectar mais facilmente ao servidor sem precisar digitar o IP, usuário, chave e porta todas a vezes. Para isso, na sua máquina local, encontre o arquivo config, que está na pasta .ssh no seu diretório $HOME (ou ~/.ssh), abra com o seu editor preferido e crie um registro novo:

Host minhavpn # pode ser qualquer nome
    User novousuario # usuario de login
    Port porta # a porta que você escolheu
    IdentityFile ~/.ssh/id_rsa # Se você salvou a chave em outro local, escolha coloque este local aqui
    HostName ip # endereço de IP do servidor

Agora você pode logar no servidor com o comando ssh minhavpn

Isso é totalmente opcional, você não precisa executar este passo se não quiser.

Criando a VPN

A criação da VPN é um processo bastante complexo, que exige que você instale todos os pacotes do OpenVPN, crie IPTables, configure o firewall, crie as chaves de acesso e os certificados que serão utilizados para acessar o endereço e tudo mais.

Todo o processo é muito complexo e é fácil de errar em algo e ter que começar tudo de novo. Então, graças ao open-source, temos um usuário no GitHub chamado Nyr que criou um script chamado OpenVPN Road Warrior Installation, que será o que vamos utilizar para instalar, ele vai te fazer algumas perguntas simples e, na maioria das vezes, você vai selecionar a resposta padrão.

Vamos instalar o wget primeiro no nosso servidor com:

sudo apt-get install -y wget

Agora vamos baixar o script no nosso caminho atual (que provavelmente será ~):

wget https://git.io/vpn -O openvpn-controller.sh

Vamos dar permissão de execução com chmod +x openvpn-controller.sh e então vamos executar o script com ./openvpn-controller.

Alguns pontos importantes durante a instalação:

  • Porta do servidor: A porta padrão do OpenVPN é a 1194 UDP, porém como é uma porta padrão, vamos escolher outra, no nosso caso é a 443 UDP que abrimos na Azure
  • Servidor de DNS: O servidor de DNS pode ser qualquer um de sua preferência, eu geralmente utilizo 1.1.1.1 ou 8.8.8.8
  • Nome do cliente: Este será o nome do arquivo que você vai gerar com a configuração, eu geralmente separo as configurações por dispositivo, então se você vai usar em um computador Windows, ele pode se chamar VPN_WIN

No final do processo de instalação, você vai obter um arquivo .ovpn de configuração. Este arquivo é o arquivo mais importante de todos porque ele possui as credenciais de acesso para que você possa entrar na sua VPN, bem como os certificados e chaves do cliente.

Removendo Logs

Por fim, vamos fazer o que a maioria dos serviços de VPN não fazem, que é desativar os logs.

Para isso vamos acessar o arquivo de configuração do OpenVPN com:

sudo vim /etc/openvpn/server/server.conf

Lembrando que o vim pode ser qualquer editor. Mude a linha que tem escrito verb 3 para verb 0, salve o arquivo e reinicie o serviço com:

sudo systemctl restart openvpn-server@server.service
É possível que o nome do serviço seja um pouco diferente dependendo da máquina que você instalou o OpenVPN e a sua versão, então talvez você precise encontrar o nome do serviço para reiniciá-lo

Agora não temos nenhum log sendo mantido pela nossa VPN!

Baixando as credenciais

Por padrão o script posiciona o arquivo no diretório do root (porque ele precisa ser executado como administrador), então vamos mover o arquivo para o nosso diretório e mudar o dono para que possamos alterá-lo:

sudo mv /root/nomedoarquivo.ovpn ~
sudo chown novousuario nomedoarquivo.ovpn

Vamos agora baixar o arquivo. Para isso, vá em um terminal local e abra uma conexão sftp com o seu servidor de VPN através do comando sftp minhavpn (ou qualquer que seja o nome que você colocou no seu alias), depois execute os seguintes comandos:

get nomedoarquivo.ovpn pasta/de/destino
exit

Lembrando que você pode acessar por outros meios também, o sftp é só um deles, mas você pode usar o scp ou até mesmo clientes de SFTP como o FileZilla.

Agora o seu arquivo está localizado na sua máquina local e a VPN está instalada, chegou a hora de testar!

Testando a VPN

Para testar a VPN, se você estiver em um Mac e quiser que todo o seu tráfego passe pela sua VPN, que é o que eu faço por aqui, você vai precisar de um software gratuito chamado TunnelBlick.

Painel do TunnelBlick

Basta baixar o software e clicar duas vezes sobre o arquivo .ovpn que você baixou e ele será importado para o sistema. A partir daí você poderá usar a VPN como qualquer outra.

Se você estiver no Windows ou em qualquer outro dispositivo (até mesmo no iOS, Android e afins) você vai precisar do OpenVPN Connect, e a partir daí a configuração é a mesma, basta um duplo clique no arquivo para importar, se isto não funcionar, ambos os programas possuem um botão de importação.

Gerenciando a VPN

A partir da primeira instalação você já vai ter o script de conexão completo, porém, algo que notei é que utilizar o mesmo script para todos os seus dispositivos acaba sendo ruim porque a VPN parece não lidar muito bem com o tráfego vindo do mesmo cliente, então a solução é criar um cliente novo para cada dispositivo que você usa.

Para isso você pode acessar o servidor da VPN novamente e executar o mesmo comando que usou para instalar a VPN, ou seja, utilizamos o mesmo script de instalação porque ele é inteligente o suficiente para saber quando ele já foi instalado e quando você está querendo apenas gerenciar.

Basta o comando sudo ./openvpn-controller.sh (ou qualquer nome que você tenha dado ao arquivo) e ele vai mostrar uma lista de comandos possíveis:

Nele você pode adicionar novos clientes para poder dar a outras pessoas a capacidade de conexão à sua VPN ou então para adicionar novos dispositivos a ela. Assim como você também pode revogar um cliente existente e remover a VPN completamente.

Conclusão

O artigo ficou um pouco longo, porém ele contém tudo que é necessário para você criar a sua própria VPN! Nos próximos artigos vou mostrar como você pode aumentar ainda mais a segurança do seu servidor com Two-Factor Authentication e também adicionar upgrades automáticos para manter o sistema sempre atualizado.

Fiquem ligados para os próximos capítulos!

]]>
<![CDATA[ Que tal aprender AKS com este curso GRATUITO? ]]> https://blog.lsantos.dev/que-tal-aprender-aks-com-este-curso-gratuito/ 5f92e81282fe7182732662f9 Sex, 23 Out 2020 11:34:51 -0300 Na semana passada divulguei em meu Twitter que tinha acabado de lançar um curso gratuito de Kubernetes com AKS como vocês podem ver no tweet aqui a seguir

Não poderia estar mais feliz de divulgar este curso porque, além de ele ser gratuito, ele também é super completo. A grade que criei para o desenvolvimento dele cobre desde Docker até o seu primeiro deploy com Kubernetes passando por quase todos os workloads mais importantes!

Além disso, estou super feliz de que ele tenha sido publicado em um canal tão sensacional como o Channel9! O canal de cursos e vídeos da própria Microsoft!

Maratona AKS: Tudo sobre Kubernetes de A a Z | Channel 9
Neste bootcamp, aprenderemos os conceitos básicos de contêineres, Docker, Kubernetes, Helm e outras ferramentas para permitir que você assuma o controle de seus aplicativos criando, gerenciando e mant

Se você quiser ir lá aprender mais sobre Kubernetes, aproveita e já assiste este curso e me dê os feedbacks através dos comentários deste artigo ou então nas minhas redes sociais (você pode encontrar todas elas no topo do blog ou então no meu site) eu vou ficar super feliz em saber o que está certo e o que posso mudar para continuar produzindo um conteúdo de cada vez mais qualidade para todos!

Muito obrigado pessoal!

]]>
<![CDATA[ Tornando o VSCode o seu único ambiente de desenvolvimento com Docker e Kubernetes ]]> https://blog.lsantos.dev/tornando-o-vscode-o-seu-unico-ambiente-de-desenvolvimento-com-docker-e-kubernetes/ 5f7a64d982fe718273266212 Dom, 04 Out 2020 21:34:43 -0300 No dia 3 de outubro participei da BrazilJS.live() um evento completamente online organizado pela já conhecida equipe da BrazilJS!

Neste dia decidi ser um pouco mais ousado e trazer para vocês uma palestra completamente Hands On sobre como podemos transformar o nosso editor do coração, o VSCode no único ambiente de desenvolvimento quando estamos tratando de Azure, Docker e Kubernetes.

Nesta palestra de mais ou menos 30 minutos, abro meu VSCode e mostro as incríveis extensões do Docker e do Kubernetes que permitem que você faça a gestão completa de qualquer cluster Kubernetes diretamente de dentro do editor, além disso, temos também a capacidade de fazer qualquer modificação e manipular o Docker em si diretamente de dentro do Code!

Dá uma olhada como foi:

Você pode encontrar as duas extensões nestes links:

]]>
<![CDATA[ Será este o fim da carreira em desenvolvimento de software? ]]> https://blog.lsantos.dev/sera-este-o-fim-das-pessoas-desenvolvedoras-chegamos-ao-limite-do-desenvolvimento-de-software/ 5f76112582fe718273266156 Sex, 02 Out 2020 18:47:10 -0300 Recentemente li um post bastante interessante no Medium que dizia que a "Era de Ouro da programação" estava chegando ao fim. Que, assim como todas as coisas, quem hoje sabe escrever código e entende das minúcias do mundo digital, amanhã será somente outra pessoa qualquer. Assim como aconteceu no passado com a leitura e a escrita.

Primeiramente, o que seria a Era de Ouro da programação?

A era de ouro

Se você dissesse para qualquer pessoa em meados de 1600 que daqui há algumas centenas de anos teríamos redes sociais onde todas as pessoas do mundo iriam postar conteúdo escrito, você provavelmente seria taxado de herege ou então de maluco.

Isto porque, como todas as habilidades do mundo, qualquer tipo de nova ciência começa como um conhecimento especializado, vindo de uma necessidade de poucas pessoas e pesquisada por poucas pessoas. Depois, este conhecimento trazido de especialistas é aprofundado e abstraído por outros especialistas para que conceitos novos, mais simples, utilizem esta base sem que as pessoas que os usem sequer saibam das suas origens.

Um exemplo clássico. Você, programador ou programadora, já escreveu ou sequer leu uma linha de código em Assembly? Fortran? As poucas pessoas que já fizeram isso são pessoas que são muito entusiastas do mundo digital ou então tiveram um contato, mesmo que passageiro, com estas tecnologias. Isto porque, hoje, a desenvolvedora comum não precisa saber que MOV ax, cs é uma instrução essencial de um processador moderno.

E esta é considerada a Era de Ouro da programação. A era que estamos vivendo agora. Onde saber codar, entender de código, de "computadores", do digital é algo que está além da capacidade da maioria das pessoas do mundo. Isto porque, para aprender essas capacidades, você precisaria se desviar do caminho "comum" de uma escola padrão. Tanto é que cursos de informática são considerados extracurriculares.

Como Tim O'Reilly - fundador da O'Reilly media, que toda pessoa dev já deve ter visto - citou neste mesmo artigo:

"Eu acho que a era de ouro das últimas duas décadas onde você pode se tornar um programador e você vai ter um emprego está meio que no fim... Programar hoje é mais como saber ler e escrever. Você simplesmente precisa saber isso."

Mas, será que, com o aumento da demanda por devs, saber programar deixará de ser um diferencial e se tornará um requisito? Não só para quem é dev, mas para todas as pessoas do mundo?

E será que, com isso, a quantidade de pessoas que saberiam programar no futuro seria tão alta que a demanda não acompanharia? Então estaríamos ao mesmo tempo ajudando a área de desenvolvimento quanto matando o que nos torna valiosos? Seria esse o fim da carreira em tecnologia como conhecemos?

A viralidade da ciência

Esta não é a primeira vez que vemos uma nova especialidade surgindo com este boom, antigamente as pessoas pensavam exatamente a mesma coisa sobre cientistas. Havia até uma promessa de que a ciência seria um conhecimento de todos o que, claramente, não aconteceu. Hoje vemos pessoas utilizando infinitas tecnologias baseadas em ciências naturais sem entender como elas funcionam.

O mesmo vale para a "arte da programação", muitas pessoas estão usando computadores e não sabem absolutamente nada do funcionamento dos mesmos. E isso é completamente normal... Hoje eu e você utilizamos o Waze, que é um GPS e nenhum de nós sabe a fundo sobre relatividade geral - provavelmente nem os criadores do Waze sabem sobre isso.

O que estou tentando dizer é que, para que possamos tornar a programação plausível para todos, a fim de que as pessoas comecem a entender as máquinas que as cercam, precisamos parar de afastar as pessoas que querem aprender. A área de desenvolvimento, assim como literalmente qualquer outra área, é super tóxica se pegarmos alguns pontos de vista.

Mas não podemos deixar a toxicidade de algumas pessoas afetarem a experiência de todas as demais pessoas que querem entrar na área e serem programadoras. Algo que me surpreendeu bastante está sendo o avanço de empresas como a Microsoft em plataformas no-code.

Como falamos no primeiro parágrafo, as pessoas que são mais versadas e especialistas na área de software conseguem abstrair conceitos mais complexos para conceitos mais simples a fim de formar uma fundação para outras pessoas poderem codar também. No discurso do Satya Nadella no Microsoft Ignite, a empresa apresentou uma proposta muito interessante para o futuro da Power Platform .

Para quem nunca ouviu falar, a Power Platform é uma solução no-code, ou seja, não é necessário saber programar, para criar automação em tarefas genéricas. Dentre elas temos várias ferramentas como o Power BI, que permite a melhor visualização de dados para quem está trabalhando com BI.

A ideia de que, no futuro, a disseminação da programação venha não por uma forma mais "pura" de código, mas sim através de plataformas desenvolvidas por empresas parece interessante, uma vez que mantemos a necessidade de haverem pessoas especializadas, e ainda sim buscamos reduzir o abismo que existe entre saber as pessoas que sabem e as que não sabem programar.

A Gangorra da Especialidade

Para quem está pensando que seu emprego pode acabar nos próximos cinco anos, há muito tempo, em 2016, escrevi o que viria a ser o meu primeiro artigo publicado. Neste artigo eu abordo justamente este tema. E, veja que, apesar de ter quatro anos de idade, estou ainda citando o artigo por aqui, porque ele ainda está atual!

Em determinado momento deste artigo eu comento sobre a diferenciação de devs generalistas e especialistas. Enquanto a oferta de empregos vai sim ficar menor para pessoas que estiverem no nível mais iniciante, outras ofertas para desenvolvedores mais avançados ainda continuarão existindo.

Isto acontece por causa do efeito gangorra. Este é um efeito bastante observado em todos os tipos de mercado de trabalho. Quando temos uma determinada tecnologia nova, não temos muitas pessoas que se destacam nela, portanto o mercado pende para a área de especialistas. Mas, no momento que esta tecnologia se tornar mais comum, e estas pessoas serão substituídas pelas generalistas, mais baratas, porém mais rasas no assunto. E aí a gangorra pende para o outro lado em um movimento perpétuo.

A área de tecnologia não irá acabar, muito menos a carreira de desenvolvimento. O que irá mudar muito é a quantidade de habilidade necessária para se manter no mercado de trabalho. Hoje, podemos estar falando de aplicações distribuídas como se fosse ciência avançada, daqui a dez anos isso pode ser tão comum quando um formulário.

O mercado futuro não ficará defasado de pessoas desenvolvedoras, mas sim de pessoas desenvolvedoras com a habilidade necessária. Hoje temos infinitos cursos iniciais sobre vários assuntos, porém estes cursos não passam muito do básico e muitas pessoas não tem a noção de que precisam estar estudando constantemente para se manter no mercado, portanto, estas pessoas formarão a base da carreira no futuro, onde hoje já há e no futuro haverá ainda mais concorrência para pouca demanda.

Precisamos investir em um conhecimento contínuo, que perpetua o conhecimento desde o básico até o avançado, ou pelo menos o intermediário, de forma que todos nós possamos estar presentes no mercado de trabalho no futuro, pois ele vai ser mais exigente. Lembre-se, a ciência de hoje é comum amanhã.

O futuro do mercado de tecnologia

A minha opinião sincera é de que, além da tecnologia, o futuro reserva uma oportunidade especial para quem se destacar em uma área que existe há mais ou menos 10 mil anos. As relações interpessoais.

Atualmente estamos tão focados na parte técnica do nosso trabalho que não temos tempo para focar nas pessoas e no que elas representam para a gente. Como eu sempre falo em todas as palestras que eu já fiz sobre o assunto:

Nós fazemos código, criado por pessoas, para pessoas. Não estamos codando para máquinas, mas sim para outras pessoas

Algo que muita gente hoje parece esquecer. No futuro isso será importantíssimo, pois além dos conhecimentos técnicos é muito importante que saibamos lidar uns com os outros.

Além disso, a técnica tende a ser abstraída para algo mais simples. Hoje temos intellisense, temos IDEs, temos várias ferramentas que pessoas há 30 anos não tinham. Atualmente é muito mais fácil desenvolver um software de qualidade e muito mais complexo do que era há 30 anos.

Então, assim como este artigo que li há um tempo também fala. Se escolhermos chamar de "Era dourada" a era que tiver a menor fricção possível para aprender algo novo e mostrar para o mundo, então estamos na maior era de ouro possível.

Nunca foi tão fácil criar um software quanto é hoje. Mas temos que tomar cuidado, pois o futuro não está tendendo para quem não pretende acompanhar as tendências, pelo que temos visto hoje, o futuro é daquelas pessoas que estarão constantemente evoluindo e buscando entender não só sobre suas próprias áreas, mas também sobre as áreas que as cercam.

Conclusão

Espero que tenham gostado deste artigo, ele foi uma tentativa mais pessoal de mostrar minha visão do mercado de tecnologia para o futuro embasando em outras visões que já havia visto no passado.

Eu acredito que ainda temos muito a mostrar e que a área de técnologia está só começando sua jornada!

]]>
<![CDATA[ Microsoft Reactor: Workshop de AKS com Virtual Nodes e AKS Com GitHub Actions ]]> https://blog.lsantos.dev/microsoft-reactor-workshop-de-aks-com-virtual-nodes-e-aks-com-github-actions/ 5f76080d82fe718273266112 Qui, 01 Out 2020 14:17:34 -0300 No dia 29 participei de uma live sensacional do Microsoft Reactor com o grande Caio Calado como host!

Nesta live fizemos um workshop prático de como podemos criar um cluster no Azure Kubernetes Service e utilizar o poder dos Virtual Nodes para ter escalabilidade infinita, essa é a nossa primeira demonstração.

Depois, partimos para uma criação prática de um repositório no GitHub para utilizar o GitHub Actions de forma a podermos criar e fazer os deploys de aplicações completas para o AKS sem precisar sequer tocar nelas

Tudo isso você encontra de forma gratuita no vídeo abaixo!

Não esqueça de curtir e compartilhar para que outras pessoas também possam ter acesso a este conteúdo! Se inscreva na newsletter aqui embaixo e deixe suas impressões nos comentários. Todos os feedbacks são bem vindos 🤓

]]>
<![CDATA[ Deploy de imagens Docker: Do VSCode para a Azure ]]> https://blog.lsantos.dev/deploy-de-imagens-docker-do-vscode-para-a-azure/ 5f6e825b82fe7182732660bc Sex, 25 Set 2020 20:54:57 -0300 Comentamos neste post que o Docker estaria ganhando uma integração nativa com a Azure! Com essa integração o deploy de uma imagem docker direto para o Azure Container Instances (ACI) seria tão fácil quando um único  comando no Docker CLI.

A integração já estava presente no Docker Edge, que é a versão beta do Docker. Agora temos a excelente notícia de que esta integração chegou para todos os usuários do Docker! E, junto com ela, agora temos a mesma integração direto no VSCode!

E como podemos ter acesso a essa facilidade?

Habilitando a integração com VSCode

Primeiramente, precisamos ter o Docker instalado na máquina. Se você ainda não possui então vá até o site oficial e baixe a versão stable para o seu sistema operacional.

Assim que você baixar e instalar o Docker, faça login na Azure utilizando o seguinte comando:

docker login azure

E depois crie um novo contexto usando o comando docker context create aci <nome>, o nome do contexto fica por sua conta!

Agora, vá até o marketplace do VSCode, baixe e instale a extensão oficial do Docker para o Code. Ela deve aparecer no seu canto esquerdo da tela, conforme a imagem a seguir.

Baixe e instale a extensão do Docker

Perceba que, no final da barra lateral, teremos um menu chamado Contexts. Abra-o e clique com o botão direito sobre o contexto que você acabou de criar. Selecione Use:

Selecione o contexto que vamos operar
Use o contexto criado

Com o contexto selecionado, já temos tudo o que precisamos para fazer nosso deploy.

Fazendo um deploy para o ACI

Com a extensão, podemos fazer login em nosso DockerHub ou Azure Container Registry:

Clique no botão de conexão para fazer o login
Clique no botão de conexão para logar em um registro

A partir daí podemos selecionar o tipo de registro que estamos usando

Selecione o serviço
Selecione o tipo de CR que quer logar

E então teremos a listagem das nossas imagens hospedadas nestes serviços:

Veja as imagens
Veja as imagens listadas

Agora podemos abrir e selecionar uma das imagens, clicar com o botão direito, e selecionar "Deploy Image to Azure Container Instances":

Selecione a imagem e faça o deploy

Veja o processo como um todo:

A partir daí você terá prompts para fazer o input do nome do seu ACI ou se quer usar um já existente, e também poderá trazer os logs, abrir portas e muito mais!

Conclusão

A integração nativa do Docker com o ACI é uma das ferramentas mais interessantes que temos quando estamos nos referindo à cloud. Essa capacidade de integração direta faz com que fique muito mais fácil realizar nossos deploys e também gerenciar nossos containers.

Se você quiser saber mais, veja a documentação oficial sobre a integração e também leia a documentação no site do Docker.

Até mais!

]]>
<![CDATA[ Conheça o GitHub Container Registry ]]> https://blog.lsantos.dev/conheca-o-github-container-registry/ 5f65116782fe718273265f94 Sex, 18 Set 2020 19:59:27 -0300 Há algum tempo atrás, o GitHub anunciou que estavam criando os GitHub Packages. Esta era mais uma ferramenta desta incrível rede para que a vida dos desenvolvedores e desenvolvedoras ficasse ainda mais fácil.

Você pode acessar os packages através do seu próprio perfil, e lá você vai ver uma série de opções, desde pacotes do NPM até, mais recentemente, imagens do Docker!.

Acesse seu perfil para poder ver seus pacotes

Isso mesmo! Agora, além do Docker Hub, você também pode armazenar suas imagens diretamente no GitHub, mas o que isso muda?

GitHub Container Registry

O GH Container Registry é uma forma de você conseguir unir o seu código com a imagem que ele representa, ou seja, deixar a infraestrutura e o código em si no mesmo ambiente.

Isso facilita muito quando você precisa direcionar algum usuário para uma página de downloads, ou então informar que você pode também baixar a imagem de um determinado container diretamente do seu próprio perfil!

Inicialmente, essa ferramenta estava em um beta fechado, porém, no dia 1 de setembro, a equipe liberou um beta aberto para todos os usuários testarem o novo modelo de pacotes.

Quanto custa

Comparando com o Docker Hub, que não cobra para repositórios públicos, mas sim para repositórios privados, o GH Container Registry é a mesma coisa. Quando essa funcionalidade for para a sua versão pública - a famosa GA (General Availability) - o custo para imagens públicas será zero, enquanto imagens privadas terão um custo a parte.

Por enquanto, no entanto, durante todo o beta, tanto os pacotes públicos quanto privados serão gratuitos. Então aproveita para fazer aquele teste e colocar suas imagens lá!

Mas, espera um pouco... Como a gente faz isso?

Criando uma imagem

Para criarmos e armazenarmos uma imagem no registro do GitHub é super simples. Podemos fazer de duas maneiras.

Através do Docker

Podemos logar no GitHub com nosso Docker local e enviar a imagem para o registro, da mesma forma que logamos em outros registros privados, como o ACR.

Para isso vamos seguir os seguintes comandos:

  1. Execute o login em seu terminal de preferência:
docker login docker.pkg.github.com -u <usuario do github>

2. O prompt vai pedir sua senha, é gerar um novo access token nesta página

É importante que o token tenha as permissões de escrita e leitura de packages

Tenha certeza que as permissões estão corretas

3. Utilize este token gerado como senha para logar

4. Faça um push de qualquer imagem utilizando docker push docker.pkg.github.com/username/repositório/nome-da-imagem:tag

Através de GitHub Actions

Já falamos em outro artigo sobre como podemos automatizar uma série de coisas utilizando GH Actions (e mais artigos vão vir por ai 😎), então como podemos utilizar as actions para enviar nossas imagens para o CR do GitHub?

Para este exemplo eu vou usar um repositório que mantenho, chamado Zaqar. Isto porque este projeto já é em sua natureza um microsserviço e precisa do Docker para funcionar, então podemos utiliza-lo de forma mais real.

Primeiramente vou criar um novo access token para que eu possa usar como minha senha na nossa action

Dentro do nosso repositório, vamos na aba settings e depois na guia secrets. Lá, vamos criar um novo secret chamado PACKAGES_PASSWORD e vamos colocar o conteúdo do nosso access token dentro dele:

Depois, vamos na aba actions dentro do repositório:

Clicaremos em new workflow (no meu caso, porque eu já tinha um workflow existente), quando formos direcionados para a tela onde seremos apresentados com os mais diversos workflows, vamos clicar no link abaixo do título que diz "Set up a workflow yourself".

Não vamos trabalhar com templates

Lá vamos ter um pequeno documento padrão com algumas linhas, vamos fazer uma alteração para que tenhamos a nossa action sendo executada somente em pushes para a branch master e também em tags que comecem com v, assim podemos ter v1.0.0 e assim por diante.

Veja como nosso arquivo base vai ficar

# This is a basic workflow to help you get started with Actions

name: Publish to GitHub Container Registry

on:
  push:
    branches: [ master ]
    tags: v*

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v2

Agora, vamos no menu do lado direito e iremos procurar a action da própria Docker chamada "Build and push Docker images"

Iremos copiar o código clicando no botão de cópia do lado direito:

Copiando o código de uso

E vamos colar logo abaixo da nossa action anterior, dentro de steps, depois vamos apagar algumas das linhas que estão colocadas como parâmetros, porque não vamos utilizar todos eles.

# This is a basic workflow to help you get started with Actions

name: Publish to GitHub Container Registry

on:
  push:
    branches: [ master ]
    tags: v*

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
  # This workflow contains a single job called "build"
  build:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    # Steps represent a sequence of tasks that will be executed as part of the job
    steps:
      # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
      - uses: actions/checkout@v2

      - name: Build and push image
        uses: docker/build-push-action@v1.1.1
        with:
          # Username used to log in to a Docker registry. If not set then no login will occur
          username: khaosdoctor
          # Password or personal access token used to log in to a Docker registry. If not set then no login will occur
          password: ${{ secrets.PACKAGES_PASSWORD }}
          # Server address of Docker registry. If not set then will default to Docker Hub
          registry: docker.pkg.github.com
          # Docker repository to tag the image with
          repository: khaosdoctor/zaqar/zaqar
          # Automatically tags the built image with the git reference as per the readme
          tag_with_ref: true

Vamos passar parte por parte deste arquivo para que possamos entender o que está acontecendo. Primeiramente, estamos dizendo para o workflow na chave name que vamos nomear esse passo do nosso processo com um novo nome além do nome da action, isto é apenas para organização.

Depois, estamos dizendo que vamos utilizar a action da Docker como uma base, estamos passando o nome do repositório e o nome da action, bem como sua versão.

Depois estamos passando pelos parâmetros que queremos setar:

  • username é o nome do nosso usuário do github
  • password é o nosso secret que acabamos de criar, contendo o nosso access token para podermos fazer o login no registry
  • Depois temos a chave registry que é onde vamos definir para que registro vamos mandar o nosso container. Neste caso, como estamos usando o próprio GitHub, vamos fixar esse valor em docker.pkg.github.com
  • Na chave repository vamos colocar o nome da nossa imagem, ela deve ser sempre neste formato que comentamos acima, onde temos nomedeusuario/repositorio/imagem:tag
  • E então temos a última configuração, que é a mais interessante e é a que mais poupa trabalho para quem está desenvolvendo. A chave tag_with_ref faz algumas coisas interessantes, a principal delas é que ela faz o taggeamento automático da imagem para nós, quando estamos enviando um push para a branch master diretamente, nossa imagem vai receber a tag latest quando enviamos para uma tag do git, a tag da imagem vai ser o nome da nossa tag do git. Você pode checar mais sobre as peculiaridades e como cada uma das configurações funciona na página da action

Depois de terminarmos, vamos dar um nome ao nosso arquivo e então salvá-lo. A partir daí uma nova build irá rodar e vamos ter uma nova imagem no nosso repositório:

A nova imagem no nosso repositório, pronta para ser baixada

Você pode checar a página deste pacote individual nesta URL

Conclusão

Juntando o GitHub com os container registries, temos uma capacidade muito maior de poder unir o nosso código e nossa infraestrutura em um único lugar. Essa unificação faz com que nossa complexidade diminua, pois vamos ter que lidar com menos ambientes, portanto podemos pensar em outras funcionalidades que tirem proveito destas facilidades no futuro!

Espero que tenham gostado do artigo, deixe seu comentário, curta e compartilhe! Não se esqueça de se inscrever na newsletter para mais conteúdo exclusivo!

Até mais!

]]>
<![CDATA[ Saiba de tudo sobre seus serviços com Jaeger e Linkerd ]]> https://blog.lsantos.dev/saiba-de-tudo-sobre-seus-servicos-com-jaeger-e-linkerd/ 5f5f889182fe718273265ea9 Seg, 14 Set 2020 14:16:46 -0300 Já falamos um pouco sobre o Linkerd no último post, agora, para podermos estender ainda mais o nosso conhecimento sobre service mesh, vamos falar de um conceito interessante que vamos explorar com mais calma em outros artigos. Mas aqui vamos ter um exemplo prático! O Tracing.

Tracing

Tracing é um dos elementos da tríade que compõe o que vamos tratar mais a frente como Observabilidade, porém, no momento vamos focar nesse aspecto.

O tracing é a capacidade de poder acompanhar uma request de ponta a ponta verificando a ordem das chamadas dos serviços, o payload enviado e recebido por cada um, as respostas de cada serviço e também o tempo que cada request demorou.

Implementar um tracing simples em uma aplicação não é uma tarefa complexa, basta fazermos um log de tudo que recebemos e enviamos. Porém, existe um outro conceito chamado deep tracing ou distributed tracing que é justamente voltado à aplicações distribuídas.

E é ai que as coisas começam a ficar complicadas...

Deep Tracing

Deep Tracing é o nome que se dá à técnica de ligar logs de uma chamada a outra, formando uma linha do tempo do que foi feito. A cada log, damos o nome de um span e uma request pode desencadear múltiplos spans, ainda mais quando estamos tratando com microsserviços que chamam uns aos outros em sequencia.

Sendo muito simplista, o que o Deep Tracing faz é adicionar um header na request inicial que é chamado de Request ID – cada implementação da técnica tem seu próprio nome para isso. A cada nova request, esse ID é passado para frente e, combinada com a data da requisição, essas informações são utilizadas para construir um histórico de tudo que foi feito.

O problema é que, para fazer a implementação manual disso tudo, temos que:

  • Ter uma noção muito boa da nossa aplicação
  • Interceptar todas as chamadas HTTP e colocar um header em cada uma delas

O que não é uma tarefa fácil, por isso existem ferramentas como o Jaeger.

Jaeger

O Jaeger é uma ferramenta open source hospedada pela CNCF que serve justamente para evitar a complicação de ter que faz o deploy e implementação de todas as partes móveis que compõem um sistema de tracing distribuído.

Ele também utiliza o Open Telemetry um projeto que visa simplificar a telemetria de aplicações através de um conjunto padrão de ferramentas para obtenção de métricas.

O Jaeger é escrito em Golang, o que torna ele super rápido e muito útil para sistemas que estão distribuídos ou então possuem uma malha complexa de serviços. Sabe o que mais vai bem com essa arquitetura? O Linkerd!

Linkerd e Jaeger

Service Mesh e Observabilidade são conceitos que andam lado a lado, porém nem sempre você tem a capacidade de implementar ambos de forma simples. E é ai que tanto o Linkerd quanto o Jaeger brilham individualmente, mas, além de serem incríveis por si só, eles são ainda melhores juntos.

Usar o Linkerd com o Jaeger é uma excelente forma de obter todas as informações e métricas possíveis de sua aplicação de forma rápida e prática, até porque o próprio Linkerd suporta add-ons que incluem tanto o Jaeger como o OpenCensus Collector, uma ferramenta de coleta de métricas.

Mas chega de conceitos! A melhor forma de explicar o que é o Jaeger é através da prática! Então vamos colocar a mão na massa!

Aplicando Tracing

Para começar, vou assumir que você já está com o cluster criado e com o Linkerd Instalado, se você ainda não instalou tudo, volta para o artigo anterior e siga o tutorial até o final

Com o nosso cluster configurado e o Linkerd já instalado, vamos começar instalando a configuração all-in-one, que provê uma única imagem que contém todos os elementos necessários para que o tracing do Jaeger funcione tranquilamente.

Para instalar essa configuração, vamos criar um novo arquivo chamado config.yaml e colocar o seguinte conteúdo nele:

tracing:
  enabled: true

Então, vamos executar um comando do Linkerd para adicionar o novo add-on, na pasta aonde você criou o novo arquivo execute o seguinte comando:

linkerd upgrade --addon-config config.yaml | kubectl apply -f -

Assim que terminarmos, devemos ter dois novos deploys, um chamado linkerd-collector e outro chamado linkerd-jaeger no namespace linkerd:

Temos dois novos deployments dentro do namespace Linkerd

Anotações

Para detectar as mudanças e começar a realizar o tracing, o Linkerd utiliza duas anotações novas nos nossos deployments, sempre que precisarmos fazer essa modificação vamos incluir estas linhas juntamente com a anotação linkerd.io/inject: enabled, ficando assim:

spec:
  template:
    metadata:
      annotations:
      	linkerd.io/inject: enabled
        config.linkerd.io/trace-collector: linkerd-collector.linkerd:55678
        config.alpha.linkerd.io/trace-collector-service-account: linkerd-collector

Instrumentação

O tracing, diferentemente da maioria das técnicas do DevOps, exige uma instrumentação na aplicação, ou seja, temos que manualmente inserir o código do coletor de métricas no nosso sistema para que ele possa identificar e adicionar os headers e o ID para o trace.

Isso pode ser feito de forma manual na nossa aplicação usando Node através deste pacote, porém, para mantermos o processo mais simples, vamos utilizar a aplicação padrão do Linkerd para testar.

Primeiramente instalamos a aplicação com o comando

kubectl apply -f https://run.linkerd.io/emojivoto.yml

Depois executamos o seguinte comando para que possamos incluir a anotação que mostramos anteriormente

kubectl -n emojivoto patch -f https://run.linkerd.io/emojivoto.yml -p '
spec:
  template:
    metadata:
      annotations:
        linkerd.io/inject: enabled
        config.linkerd.io/trace-collector: linkerd-collector.linkerd:55678
        config.alpha.linkerd.io/trace-collector-service-account: linkerd-collector
'

Esperamos o deployment terminar de ser finalizado. Você pode executar o comando a seguir para acompanhar:

kubectl -n emojivoto rollout status deploy/web

Para finalizar, vamos ativar o tracing setando uma nova variável de ambiente no deployment com o seguinte comando

kubectl -n emojivoto set env --all deploy OC_AGENT_HOST=linkerd-collector.linkerd:55678

Explorando o Jaeger

Vamos ter certeza de que nossa implementação funcionou quando rodarmos o comando linkerd dashboard e virmos o ícone do Jaeger ao lado dos nossos namespaces

Veja o ícone do logo do Jaeger ao lado do Grafana

Se clicarmos nele, teremos todas as chamadas feitas dentro da aplicação e poderemos acompanhar o histórico de tudo que foi chamado via HTTP

Cada círculo é uma chamada

Se clicarmos em uma das linhas, conseguiremos ver por onde todas as requisições passaram e quais serviços foram chamados, bem como seus payloads e tempos

Outra funcionalidade interessante é a capacidade do Jaeger identificar a possível arquitetura do sistema. Basta clicar na guia "System Architecture" e selecionar entre o grafo direcionado e o DAG:

Diagrama de número de requisições
Grafo direcionado

Podemos também explorar cada uma das requisições individualmente através do mesmo ícone do Jaeger em cada uma das linhas quando iniciamos o "Live View" de uma rota.

Conclusão

Vamos falar mais de observabilidade por aqui no futuro, mas tenha em mente que o tracing é uma das principais razões pelo qual o service mesh é tão cobiçado. O poder que é possível extrair quando você sabe exatamente o que está acontecendo em seu sistema permite que você resolva bugs e trate erros de forma muito mais simples e rápida.

Espero que tenham gostado do artigo, deixe seu comentário, curta e compartilhe! Vamos ajudar todo mundo a entender o que é tracing! Não se esqueça de se inscrever na newsletter para mais conteúdo exclusivo!

Até mais!

]]>
<![CDATA[ Uma Introdução à Service Mesh com Linkerd ]]> https://blog.lsantos.dev/uma-introducao-a-service-mesh-com-linkerd/ 5f58d59282fe718273265cac Qui, 10 Set 2020 18:26:02 -0300 Conversamos recentemente sobre Service Mesh e como este padrão de arquitetura pode salvar seu projeto de forma que ele ganhe mais observabilidade e facilidade de utilização.

Falamos de Service Mesh de forma muito conceitual. Agora vamos colocar a mão na massa e criar nossa própria mesh utilizando o Linkerd!

Linkerd

Uma breve história do Linkerd

O Linkerd é um projeto criado dentro do Twitter em 2013, quando a rede estava migrando sua arquitetura de uma plataforma de camadas para uma arquitetura de microsserviços. O projeto Linkerd foi transformado em open source em 2016 no que ficou conhecida como  a versão 0.1.

Em 2017, o projeto foi doado a CNCF (Cloud Native Computing Foundation), que é um braço da Linux Foundation dedicado a cuidar de projetos open source. Alguns projetos como o Kuberntes, Helm, Brigade, Harbor, Envoy e o CoreDNS fazem parte dessa incrível fundação.

Recentemente, em 2018, o Linkerd chegou a versão 2.0. O Linkerd v2 foi lançado corrigindo uma série de problemas e tendo como base as lições aprendidas na v1. O projeto inicial tinha a intenção de ser altamente configurável, poderoso e multiplataforma. Já a v2 tinha como objetivo ser muito mais simples. Os principais objetivos de design da versão dois eram:

  • Zero configuração
  • Simples e leve
  • Pensado para Kubernetes

Por padrão, o Linkerd já nos proporciona uma série de funcionalidade nativas:

  • Detecção de protocolos: O Linkerd pode, além de fazer um proxy para protocolos TCP e gRPC, detectar se o tráfego é do tipo HTTP ou gRCP automaticamente.
  • Proxy HTTP 1.1/2 e gRPC: Se qualquer um destes protocolos for usado, o Linkerd pode, nativamente, extrair métricas, fazer proxies, lógicas de tentativas e load balancing.
  • Injeção automática e zero configuração: Como vamos ver mais adiante, o Linkerd é super simples de se instalar, mesmo em clusters com aplicações existentes. Portanto isso facilita muito a adoção da ferramenta
  • mTLS automático por padrão: Por padrão, toda comunicação interna é criptografada usando mTLS
  • Observabilidade: Além de logs e métricas, o Linkerd pode construir um grafo de serviços baseado somente nos tráfegos de entrada e saída. Tudo com métricas e dados.
  • Divisão de tráfego: Uma técnica bastante comum para aplicações distribuídas é chamada de Canary Deployment. Que é quando fazemos o deploy de aplicações parcialmente, através do redirecionamento de parte do tráfego gradualmente para a aplicação nova enquanto a aplicação antiga ainda está funcionando. O Linkerd nativamente proporciona essa funcionalidade.

Arquitetura

Para entendermos melhor o que estamos fazendo, vamos tentar entender como o Linkerd funciona e qual é a arquitetura que ele segue. Para isso temos dois conceitos que são inerentes de service mesh.

  • Control Plane: Responsável por coletar e armazenar todas as informações de requisições e também controlar o fluxo de informações
  • Data Plane: Aonde sua aplicação está localizada e aonde estamos tendo transferências de dados
Overview da arquitetura do Linkerd

Não vou passar por todas as camadas do sistema de forma completa, pois não é o intuito do artigo, mas vou falar um pouco de cada uma para podermos entender como todas as partes se comunicam.

Proxy

A única parte que existe no data plane. É a responsável por capturar informações e métricas dos containers onde suas aplicações estão rodando.

Isto é feito através da injeção de dois containers dentro dos seus pods do Kubernetes:

  • Init Container: Vai ser executado antes do container do pod e vai configurar as regras de acesso, IP e rotas para que toda a comunicação passe primeiro pelo proxy do Linkerd
  • Proxy Container: É a camada que vai capturar as requisições e extrair métricas para análise.

Controller

É a parte com mais responsabilidades. Ela contém o core do Linkerd e possui diversas funcionalidades:

  • Host do servidor de API
  • Atua como CA (autoridade certificadora) para o mTLS
  • Informação de service discovery e balanceamento de carga para o proxy
  • Permite o tap, que é a inspeção em tempo real do tráfego de rede em uma ou mais rotas/aplicações
  • Injeta os proxies automaticamente nos containers iniciados

Web

Uma das grandes facilidades do Linkerd é a existencia de uma interface web que mostra e controla todos os aspectos da malha.

Interface web do Linkerd

Prometheus

Serviço de coleta de métricas do proxy. Busca e armazena temporariamente as métricas coletadas das suas aplicações.

Muitas vezes o cluster do Kubernetes já vem com um serviço do Prometheus, o serviço do Linkerd não é configurado para análise, somente para performance, então ele não armazena mais do que 6h de métricas.

Grafana

Busca métricas e as exibe em forma de gráficos e dashboards

Dashboard do Grafana

E o Istio?

Uma das principais perguntas que as pessoas fazem quando olham o Linkerd é:

Qual é a diferença entre o Linkerd e o Istio?

Basicamente, temos algumas diferenças de foco da plataforma em si e também na arquitetura. Porém, em suma, o Linkerd é uma ferramenta mais focada em performance do que o Istio.

O Istio é mais focado em prover mais ferramentas e funcionalidade e, por isso, é mais complexo do que o Linkerd. Em minha opinião, o uso do Linkerd é muito mais fácil para quem está começando com Service Mesh do que o Istio.

Criando um service mesh

Chegou a hora de sair da teoria e entrar na prática! Vamos colocar a mão na massa e entender como podemos criar um service mesh usando Linkerd.

Para realizar esse hands on eu vou criar um cluster no AKS, mas você pode escolher realizar em qualquer cluster Kubernetes que você tiver, inclusive em um cluster já em produção visto que o Linkerd não é destrutivo e instala suas informações em outro namespace.

Você também pode utilizar soluções para executar o K8S de forma local com o minikube, Docker ou outras opções

Dentro deste cluster vou fazer o deploy da aplicação presente no repositório abaixo

Azure-Samples/aks-bootcamp-sample
Demo repository for the AKS Bootcamp course. Contribute to Azure-Samples/aks-bootcamp-sample development by creating an account on GitHub.

Esta é uma aplicação de demonstração que utiliza um front-end e um backend para comunicação. Você também pode seguir utilizando outra aplicação como, por exemplo, a aplicação de exemplo do Linkerd.

Vou assumir que o cluster já está criado e que a aplicação já está rodando para podermos pular diretamente para a parte que importa.

Instalação

Para rodarmos o Linkerd, primeiramente precisamos instalar a linha de comando da ferramenta com o comando:

curl -sL https://run.linkerd.io/install | sh

E adicionar no nosso path com o comando:

export PATH=$PATH:$HOME/.linkerd2/bin
Se você usa Mac, então você pode instalar o Linkerd com o Homebrew através do brew install linkerd

Validação

Antes de instalarmos o Linkerd no cluster propriamente dito, vamos realizar uma validação executando o comando:

linkerd check --pre

Isso vai garantir que todos os nomes e serviços estão disponíveis para que o Linkerd seja instalado

Saída com sucesso do comando

Então rodamos o comando:

linkerd install | kubectl apply -f -

Perceba que este comando, na verdade é uma junção, pois o Linkerd não faz nenhuma instalação no cluster, ele somente gera um output de um arquivo YAML que pode ser usado pelo cluster para criar seus workloads, por isso que damos um pipe para o kubectl apply -f -. Experimente rodar somente linkerd install e veja os arquivos sendo mostrados na tela.

Depois, vamos rodar o comando a seguir para podermos checar se todo o processo foi executado com sucesso:

linkerd check

Se tudo correu bem, você deverá ter uma saída como esta:

Saída de sucesso do linkerd check

Todos os workloads do Linkerd são instalados dentro de um namespace próprio chamado linkerd, então você pode buscar todos os recursos que comentamos através do kubectl pelo comando:

kubectl get all -n linkerd

Primeiras impressões

Para começarmos a entender como o Linkerd funciona, você pode digitar o comando linkerd dashboard & e aguardar alguns segundos para a abertura do painel web.

Painel web do Linkerd

Por este painel você será capaz de realizar executar todas as funcionalidades do Linkerd, bem como verificar o estado do seu cluster, ele também pode agir como uma forma de dashboard para o Kubernetes, de certa forma. Basta vermos a seção Workloads no menu lateral:

Veja que estamos trabalhando no namespace default. Isso pode ser trocado através do menu de seleção.

Control Plane

Se clicarmos na seção Control Plane, iremos ter uma visão geral de todo o nosso Service Mesh.

Painel de visualização do Control Plane

Veja que este painel nos mostra todo o status do sistema e também a quantidade de componentes que temos instalados.

Mais abaixo, podemos ver a quantidade de namespaces que estão "meshed", ou seja, que estão injetados pelo proxy do Linkerd de forma que estão gerando métricas:

Perceba que apenas um namespaces está gerando métricas

Adicionando métricas

Nossa aplicação está no namespace default, como fazemos para incluí-la no Service Mesh? Simples! Vamos executar o seguinte comando:

kubectl get deploy -o yaml

Veja que temos todos os arquivos YAML dos nossos deployments, então vamos criar um pipe deste comando para o comando linkerd inject:

kubectl get deploy -o yaml | linkerd inject -

Agora veja a diferença entre os dois arquivos. O arquivo injetado terá uma annotation no template do pod desta forma:

Anotação do Linkerd no pod

Esta é a única mudança que é feita em seus deployments, ou seja, é muito fácil injetar o Linkerd em serviços que já estão em produção sem causar muitos efeitos colaterais.

Vamos aplicar as mudanças através de um outro pipe para o comando apply:

kubectl get deploy -o yaml | linkerd inject - | kubectl apply -f -

Assim que aplicarmos a mudança poderemos ver uma movimentação no Control Plane de forma que ele estará carregando os nossos pods novamente para incluí-los nas métricas:

O namespace está entrando no mesh

Veja que não temos um dos pods enviando métricas... O pod do MongoDB está fora do Mesh, mas por que?

Pois, neste exemplo, ele é um StatefulSet e não um deployment! Então vamos executar o mesmo comando que executamos anteriormente, mas vamos substituir deploy por sts.

kubectl get sts -o yaml | linkerd inject - | kubectl apply -f -
Importante: Para este exemplo estou utilizando uma versão simples do MongoDB dentro do cluster local, você pode utilizar uma versão hosteada por qualquer provedor – como o Mongo Atlas, por exemplo – ou então outras soluções. A aplicação só precisa de um MongoDB para funcionar.

Agora temos todos os detalhes:

Ferramentas

O Linkerd nos dá uma série de ferramentas interessantes, vamos conhecer algumas delas.

Grafana

Clique no nome do namespace na aba Control Plane.

Além de conseguirmos verificar as métricas dentro do painel do Linkerd, também podemos encontrar um dashboard do Grafana ao clicarmos no pequeno ícone laranja no canto direito

Vamos abrir o painel para o deployment porto-backend:

Nosso dashboard está vazio

Veja que o dashboard não apresenta nenhum dado... Por que? Pois ainda não fizemos nenhuma requisição que possa ser capturada! Vamos entrar na nossa aplicação e criar alguns acessos. Para achar a URL da sua aplicação utilize o comando kubectl get ing para buscar os ingresses e assim as URLs.

Assim que começarmos a fazer alguns acessos podemos ver alguns dados chegando:

E podemos ver o que está acontecendo nos gráficos do grafana também!

Temos também métricas de tráfego TCP:

Além deste dashboard, temos outros dashboards já internos que o próprio Linkerd criou.

Visão em tempo real

Ao clicarmos no nosso deployment dentro da visão do namespace, podemos ver em tempo real as chamadas que estão sendo feitas.

Também podemos ver um mapa da aplicação que nos mostra quais são os serviços conectados.

Top

Se clicarmos no menu Top na seção Tools do menu lateral. Vamos poder selecionar um namespace ou recurso para podermos acompanhar em tempo real. Selecione o namespace default e faça algumas requisições para a aplicação. Veja que começamos a ter um acompanhamento de métricas:

Tap

Ainda na mesma visualização, veja que temos o ícone de um microscópio, este ícone permite que observemos aquela rota específica para mais detalhes.

Vamos abrir a aba Tap no menu lateral. Selecionar o namespace default no topo e clicar em start. Assim que fizermos algumas requisições, poderemos ver todas elas em tempo real

Ao clicarmos na seta esquerda, teremos uma visualização da request em detalhes

Grafo de serviços

Na minha opinião, uma das funcionalidades mais legais do Linkerd é a do grafo de serviços. Para podermos representar bem como ela funciona, vamos voltar para a visualização de namespaces no menu esquerdo e clicar sobre o namespace linkerd

No topo da tela teremos uma representação dos serviços que estão conversando entre si e quais deles estão enviando requisições. Isto é muito útil quando estamos falando de meshes muito grandes.

Removendo o Linkerd

Para removermos o linkerd de um namespace basta executar o comando

kubectl get deploy -o yaml | linkerd uninject - | kubectl apply -f -

Ou seja, da mesma forma que executamos o inject, executamos também o uninject.

Conclusão

O Linkerd pode facilitar muito a vida de quem esta desenvolvendo aplicações distribuídas, por ser simples e fácil de instalar, ele tem uma grande vantagem sobre os demais sistemas de Service Mesh e pode provar ser um excelente adendo à sua infraestrutura.

Não se esqueça de se inscrever na newsletter para receber esse conteúdo e também notícias semanais! Curta e compartilhe seus feedbacks nos comentários!

Até mais!

]]>
<![CDATA[ Otimização de custos com Kubernetes e AKS ]]> https://blog.lsantos.dev/otimizando-custos-com-kubernetes-e-aks/ 5f4d3b5c82fe7182732659ec Qui, 03 Set 2020 12:41:57 -0300 Desde os primórdios da computação distribuída e da chegada da cloud, todas as pessoas já tiveram que lidar, de alguma forma, com otimização de custos. Seja esta otimização em forma de redução de uso de espaço, ou até mesmo em redução de tráfego de rede!

Uma das ferramentas mais caras quando falamos em computação distribuída é o Kubernetes. Isto é um pouco óbvio por dois motivos bastante simples:

  1. O Kubernetes trabalha é um cluster de máquinas
  2. Por ser um cluster, temos mais recursos para gerenciar

Infelizmente, este problema faz com que muitas soluções incríveis deixem de estar rodando em seu ambiente ideal, que é um ambiente distribuído e altamente escalável, para rodarem em ambientes menores por pura questão de custo.

Mas isso não precisa ser assim!

Modelos de cobrança

Para entendermos como podemos otimizar os custos dentro de uma arquitetura cloud com computação distribuída – usando Kubernetes – primeiro precisamos entender os modelos de cobrança.

Cada cloud tem seu modelo de cobrança individual, aqui vamos trabalhar somente com o AKS (Azure Kubernetes Service) que roda na Microsoft Azure.

Se você usa outro provedor cloud, busque no site do fornecedor o preço e as opções de cobrança para cada serviço oferecido. No entanto, as opções de otimização que vou mostrar por aqui servem para todas as clouds, claro, com algumas mudanças na linha de comando.
Descrição das formas de cobrança para o AKS

Como podemos ver, no caso do AKS, a cobrança é feita somente pelos recursos utilizados, sendo que o o gerenciamento do cluster como um todo não possui nenhuma cobrança extra. Isso quer dizer que, em uma arquitetura Kubernetes padrão, o que chamamos de Control Plane, ou o plano de controle onde todos os recursos de sistema são criados, não é cobrado. Ao invés disso, todos os demais recursos que o Kubernetes usa para poder funcionar são.

Esta é uma prática bastante comum em vários provedores cloud. Alguns outros provedores cobram também pela alocação do control plane, geralmente porque não são totalmente gerenciados e abrem espaço para que as pessoas que os administram possam modificar uma parte do seu conteúdo, ou seja, oferecem uma capacidade maior de customização.

E quais recursos seriam estes?

Recursos de cobrança

No geral, criar um cluster Kubernetes exige uma série de pequenos recursos que vão desde máquinas virtuais – que suportam os nodes – até interfaces de rede e controladores de tráfego. Dependendo do tipo de funcionalidades que você está escolhendo para seu cluster, você pode ter zonas de DNS e outros recursos nessa lista.

No caso do AKS, quando criamos um cluster, selecionamos o que é chamado de Resource Group. Neste resource group, a Azure irá criar um recurso de controle chamado Kubernetes Service, como podemos ver a seguir:

Criamos um demo-cluster em um resource group

Porém, aonde estão todos os recursos cobrados que falei no início do parágrafo? Para isto, por conta de gerenciamento interno, a Azure cria um outro resource group que começa com o nome MC_<resource-group>-<cluster>_<região>. E nele serão colocados todos os recursos cobrados:

Recursos cobrados por um cluster

Veja que temos oito recursos diferentes, porém podemos ser cobrados por mais que isto porque o que é chamado de Virtual Machine Scale Set é, na verdade, uma lista de VMs que podem ser escaladas de acordo com o precisamos.

Otimizando custos com AKS

Para construir este artigo, estou usando como base um excelente material disponibilizado gratuitamente no Microsoft Learn. O curso "Otimização de Custos com AKS e Node Pools".

Neste artigo, vamos passar pelo material completo, porém vou dar alguns exemplos fora do contexto e explicar algumas coisas além do que está sendo mostrado. Porém, é fortemente recomendado que você complete o módulo, ele é gratuito e leva apenas alguns minutos.

Criando o ambiente

Para começar, vamos precisar de três itens importantes:

  1. Você precisa ter o Azure CLI instalado em sua máquina
  2. Você precisa ter o Kubectl instalado em sua máquina
  3. Você precisa ter uma conta na Azure

Como uma segunda opção, se você já tiver uma conta na Azure, você pode entrar no Azure Cloud Shell e rodar todos os comandos por lá, pois o Cloud Shell já possui tanto o Azure CLI quando o Kubectl instalados. Se for a sua primeira vez usando o Cloud Shell, então selecione o Bash como shell principal.

Execute o comando a seguir para habilitar o modo de preview no seu CLI da Azure:

az extension add --name aks-preview

Depois execute o seguinte comando para registrar as funcionalidades de permissão que vamos precisar:

az feature register --namespace "Microsoft.ContainerService" --name "spotpoolpreview"

Este comando demora alguns minutos para rodar, para checar o progresso, rode periodicamente o comando abaixo:

az feature list -o table --query "[?contains(name,'Microsoft.ContainerService/spotpoolpreview')].{Name:name,State:properties.state}"

Enquanto o resultado desta query for Registering, aguarde até que seja Registered. Uma vez registrado, rode o comando para atualizar o CLI:

az provider register --namespace Microsoft.ContainerService

Node Pools

Antes de podermos partir para a criação do nosso cluster, precisamos entender o que são as chamadas Node Pools, elas serão essenciais para podermos economizar durante o uso do AKS.

Basicamente, uma Node Pool descreve um grupo de nodes do Kubernetes que compartilham características em comum.

Por exemplo, podemos ter nodes que são VMs específicas para Machine Learning, ou então aqueles que possuem mais memória. O objetivo das Node Pools é justamente permitir que as pessoas que estejam operando o cluster possam ter uma opção de escolha para criar suas aplicações na infraestrutura que for mais adequada para o tipo de trabalho que está sendo realizado.

No AKS, temos dois tipo de Node Pools.

System Node Pools

São criadas automaticamente com o cluster e, geralmente, servem para armazenar pods e deployments do sistema do AKS e do Kubernetes no geral. Não é uma boa prática executar workloads personalizados usando a mesma node pool. Todo o cluster do AKS deve conter pelo menos uma Node Pool de sistema com pelo menos um node.

User Node Pools

Como podemos imaginar, são os grupos de nodes criados pelo usuário. Nestas pools temos algumas configurações interessantes, já que podemos especificar tanto Windows como Linux para as máquinas que são executadas, e também podemos alocar máquinas de tamanhos e categorias diferentes das do que foram definidas no AKS

Capacidade de execução

Cada node tem uma capacidade máxima de execução de pods, ou seja, conseguimos colocar uma quantidade máxima de pods dentro de uma VM antes que seus recursos sejam esgotados. Por isso, você pode especificar a quantidade de nodes dentro de uma pool até um limite de 100.

Em pools de usuário você pode setar a quantidade de nodes para zero, enquanto em pools de sistema o número mínimo é um.

Criando uma Node Pool

Você pode criar uma nova pool em um cluster existente usando o Azure CLI com o seguinte comando:

az aks nodepool add \
  -g <resource-group> \
  --cluster-name <nome do cluster> \
  --name <nome da pool> \
  --node-count <numero de nodes> \
  --node-vm-size <tamanho e tipo da VM> \

Escalabilidade

Quando um node atinge a sua capacidade máxima de execução, ou seja, quando já colocamos o número máximo de pods possível dentro daquela máquina, temos de aumentar – ou escalar – o número de nodes dentro da pool. Isso pode ser feito de forma manual, através do comando a seguir:

az aks nodepool scale \
  -g <resource-group> \
  --cluster-name <nome do cluster> \
  --name <nome da pool> \
  --node-count <novo numero de nodes>

A escalabilidade é uma das principais razões pelo qual seu cluster pode custar bem caro. Principalmente porque quanto mais máquinas, mais recursos. E, quanto mais recursos estamos usando, mais vamos ter que pagar. Por isso é altamente recomendável utilizarmos formas automáticas de escalar nossas pools, a principal delas é o Cluster Autoscaler.

Cluster autoscaler

Escalabilidade deve ser algo automático, pois é muito mais seguro e também economiza muito mais dinheiro porque ele sempre vai aumentar a quantidade de nodes quando for necessário e vai reduzir a quantidade de nodes quando estes nodes não precisarem mais serem utilizados. Você pode ativar o autoscaler em um cluster existente através do comando:

az aks update \
  -g <resource-group> \
  -n <nome do cluster> \
  --enable-cluster-autoscaler \
  --min-count <minimo de nodes> \
  --max-count <maximo de nodes>

Spot Instances com Node Pools

Uma das formas mais eficientes de se economizar dinheiro enquanto estamos utilizando instâncias do AKS é através do uso de múltiplas node pools com as chamadas Spot Instances.

Spot VMs

As VMs do tipo Spot são máquinas virtuais que oferecem todos os recursos de escalabilidade que uma VM normal teria, mas ainda sim reduzindo os custos através do uso de computação excedente. Isso significa que as Spot VMs usam poder computacional que não está sendo utilizado pela Azure no momento, garantindo descontos significativos no preço das mesmas.

Porém, isso tudo vem com um preço. As Spot Instances, por se aproveitarem de poder computacional não utilizado, podem ser desativadas ou interrompidas a qualquer momento. Isso significa que, durante o uso da VM, você terá uma notificação 30 segundos antes da máquina ser desalocada, depois deste tempo a máquina entrará em um estado de desalocação e sua computação será parada abruptamente.

Por este motivo, as Spot VMs são muito boas quanto utilizamos aplicações que não guardam estado e podem ser interrompidas a qualquer momento, reiniciando seus processos sempre que for necessário. Alguns casos de uso:

  • Processamento batch
  • Aplicações stateless
  • Ambientes de desenvolvimento
  • Pipelines de CI e CD

Juntando forças

Utilizar Spot VMs com node pools dá um grande poder para economizar um bom dinheiro na hora de processar dados de larga escala. Principalmente porque, quando usamos Spot VMs com node pools, temos a capacidade de escolher entre duas políticas de desalocação:

  • Desalocar: Quando a política é definida como desalocação, a máquina será parada e desalocada quando a VM chegar a um estado que não há mais poder computacional. Você pode fazer o deploy dela novamente quando a capacidade voltar a estar disponível, mas lembre-se de que todos os custos de alocação de CPU e discos continuam sendo contados.
  • Deletar: Neste caso, a máquina será completamente removida e você não irá pagar por mais nenhum recurso consumido.

Spot Node Pools

As Spot Node Pools permitem que você defina um valor máximo por hora para pagar, quando o valor for atingido, a máquina será desalocada ou removida, de acordo com a política selecionada.

Apesar de garantirem uma redução de custos. Spot node pools não são recomendadas para nenhum tipo de workload muito importante, pois a disponibilidade da mesma não é garantida.

Para criarmos uma spot node pool, podemos utilizar o seguinte comando:

az aks nodepool add \
  -g <resource-group> \
  --cluster-name <nome do cluster> \
  --name <nome da pool> \ 
  --enable-cluster-autoscaler \
  --min-count <numero minimo de nodes> \
  --max-count <numero maximo de nodes> \
  --priority Spot \
  --eviction-policy Delete \
  --spot-max-price -1 \

Quando setamos o valor do preço por hora para -1, os nodes não vão ser removidos com base no preço e as novas instâncias criadas serão baseadas no menor valor entre o valor atual das spot VMs ou então o valor padrão de um node

Criando recursos na nova pool

Para criarmos recursos nas nossas spot node pools, precisamos saber o conceito de Taints e Tolerations do Kubernetes (vamos ter um artigo aqui sobre isso logo mais). E também o conceito de Node Affinity.

Em suma, cada node tem uma taint. Essas taints repelem novos pods de serem criados nesses nodes a não ser que estes pods tenham uma toleration àquela taint específica. É uma forma de escolher em qual VM suas aplicações serão criadas.

Por padrão, todos os nodes criados dentro de uma spot node pool terão um taint do tipo kubernetes.azure.com/scalesetpriority=spot:NoSchedule, isso significa que, a não ser que o pod tenha uma toleration do mesmo tipo, nenhum outro pod poderá sofrer um schedule naquele node.

Para que possamos criar um pod – ou qualquer outro workload – dentro de um node que está presente em uma spot node pool, precisamos definir uma nova toleration no arquivo declarativo do pod, por exemplo:

apiVersion: v1
kind: Pod
metadata:
  name: example-pod
  labels:
    env: example
spec:
  containers:
  - name: example
    image: node
    imagePullPolicy: IfNotPresent
  tolerations:
  - key: "kubernetes.azure.com/scalesetpriority"
    operator: Equal
    value: spot
    effect: NoSchedule

Veja que definimos um operador para que ele seja igual a taint do node em questão, portanto novos pods serão criados neste node.

Conclusão

Apesar de ser um trabalho extenso, o uso de spot node pools pode ser um salvador de vidas em termos de recursos quando estamos trabalhando com otimização de custos no AKS, mas lembre-se sempre que as máquinas do tipo spot não são máquinas que garantem alta disponibilidade!

Recomendo fortemente a leitura da documentação sobre a baseline de otimização de custos do AKS, uma documentação incrível sobre como você pode definir polícias e melhores práticas para clusters AKS em produção

Neste artigo falamos bastante sobre conceitos mais avançados do Kubernetes, como taints e tolerations. Estou preparando um artigo somente sobre estes conceitos e também sobre uma outra ferramenta super interessante desse orquestrador de containers, então não se esqueça de se inscrever na newsletter para receber esse conteúdo e também notícias semanais! Curta e compartilhe seus feedbacks nos comentários!

Até mais

]]>
<![CDATA[ O que há de novo no TypeScript 4.0 ]]> https://blog.lsantos.dev/o-que-ha-de-novo-no-typescript-4-0/ 5f4d515f82fe718273265a94 Seg, 31 Ago 2020 17:45:39 -0300 No dia 20 de Agosto de 2020, o TypeScript anunciou a sua mais nova versão, a 4.0! Então, neste artigo, me preparei para apresentar para vocês as últimas mudanças e novidades da versão!

Apesar de ser uma major version as alterações que foram introduzidas nesse release não são muito substanciais e, pode se acalmar, não temos nenhuma breaking change :D

Tuplas nomeadas

Para começar, temos a resolução de um problema relativamente antigo do superset mais amado por todos. Quando temos tuplas (elementos que são compostos de pares de dados), antigamente tínhamos uma definição como esta:

function tupla (...args: [string, number]) {}

Veja que não temos nenhum nome para nenhum dos parâmetros que ocupam tanto a posição da string quanto a posição do number. No que diz respeito à inferência de tipos e à checagem no geral, isso não faz diferença alguma, mas é muito útil quando estamos documentando nosso código.

Por conta da checagem de tipos, a função anterior seria traduzida para algo semelhante a isto:

function tupla (args_0: string, args_1: number) {}

Que é essencialmente a mesma coisa, porém, na hora de fazermos o código, o nosso intellisense – que é uma das grandes vantagens do uso do TypeScript, no geral – vai nos dar uma nomenclatura que não ajuda ninguém, como podemos ver no gif abaixo

Nomenclatura como args_0 e args_1

Agora, com a versão 4.0, podemos incluir nomes nas nossas tuplas para que elas sejam nomeadas durante o intellisense:

function tupla (...args: [nome: string, idade: number]) {}

E ai conseguimos um resultado como o seguinte:

Conseguimos ver a nomenclatura de cada parâmetro

É importante notar que: Se você está nomeando qualquer elemento de uma tupla, você precisa nomear os dois. Caso contrário você terá um erro:

type Segment = [first: string, number];
//                             ~~~~~~
// error! Tuple members must all have names or all not have names.

Inferência de propriedades a partir do construtor

A partir de agora, quando configuramos o TypeScript com a configuração noImplicitAny, podemos usar a análise de fluxo que é feita no tempo de compilação para determinar os tipos de propriedades em classes de acordo com as atribuições em seu construtor.

class Test {    
   public x   
   constructor (b: boolean){      
     this.x = 42
     if (b) this.x = 'olá'
   }
}

Em versões anteriores, como não estamos especificando o tipo da propriedade, isto faria o compilador atribuir o tipo any, mas como checamos que não queremos any de forma implícita, então o compilador nos daria um erro dizendo que não podemos ter nenhum tipo de any implícito.

Na versão mais atual, o TypeScript consegue inferir, a partir do construtor, que x é do tipo string | number.

Short-Circuit em operadores compostos

Poucas pessoas conhecem esta funcionalidade do JavaScript, mas muitas outras linguagens também possuem o que é chamado de compound assignment operator, ou, operadores de atribuição compostos.

O que eles fazem é resolver a expressão do lado direito e atribuir o valor para a variável do lado esquerdo. Os mais famosos são os operadores algébricos:

let b += 2
let c /= 3

Todos funcionam muito bem e existem para a maioria das operações lógicas. Porém, de acordo com o próprio time do TS, existem três notáveis exceções à esta regra. Os operadores lógicos &&, || e o operador de coalescencia nula ??. No 4.0 temos a adição de três novos operadores:

a ||= b
// que é igual a
a || (a = b)

Além disso temos os operadores &&= e ??=.

Catch com unknown

Desde os primórdios do TypeScript, sempre que tínhamos uma cláusula catch, o valor do argumento de erro era sempre definido como any, pois não havia como saber qual era o tipo de retorno.

Portanto, o TypeScript simplesmente não checava os tipos destes parâmetros, mesmo se o noImplicitAny estava ativo.

try {
  throw 'Alguma coisa'
} catch (err) { // Este 'err' é Any
  console.log(err.foo()) // não vai dar erro
}

Isso era pouco seguro uma vez que podíamos chamar qualquer função dentro do catch. A partir do 4.0, o TS vai tipar os erros como unknown.

O tipo unknown é um tipo especificamente voltado para tipar coisas que não sabemos o que são. Portanto elas precisam de um type-casting antes de poderem ser usadas. É como se um dado do tipo unknown fosse um papel em branco e você pudesse pinta-lo da cor que quiser. Neste caso, o unknown pode ser convertido para qualquer tipo.

Outras mudanças

Além de mudanças na linguagem, a velocidade de compilação com a flag --noEmitOnError ficou mais rápida quando usamos junto com a flag --incremental. O que a última flag faz é dar a possibilidade de compilarmos uma aplicação mais rapidamente a partir de uma outra aplicação que já foi compilada, a chamada compilação incremental.

Quando utilizávamos --incremental com --noEmitOnError, se compilássemos um programa pela primeira vez e ele der um erro, isso significa que ele não emitira nenhuma saída, portanto não há um arquivo .tsbuildinfo onde o --incremental poderá olhar, o que tornava tudo super devagar.

Na versão 4.0 este problema foi corrigido. E, além disso, agora é permitido o uso da flag --noEmit juntamente com --incremental, o que não era permitido antes pois --incremental precisava da emissão de um .tsbuildinfo.

Algumas outras mudanças menores foram feitas no que diz respeito à edição e editores no geral. Você pode conferir a postagem do blog aqui.

Conclusão

E fechamos por aqui a nossa atualização deste superset sensacional! Lembrando que estamos precisando de ajuda na tradução para português no site do TypeScript, nos ajude a traduzir!

Não se esqueça de se inscrever na newsletter para mais conteúdo exclusivo e notícias semanais! Curta e compartilhe seus feedbacks nos comentários!

]]>
<![CDATA[ Maratona Full Cycle 4.0 ]]> https://blog.lsantos.dev/maratona-full-cycle-4-0/ 5f49c03082fe718273265982 Sex, 28 Ago 2020 23:48:18 -0300 Tive o prazer imenso de ser convidado pelo Wesley a participar da Maratona Full Cycle 4.0!

O objetivo dessa maratona é mostrar que é possível ser uma pessoa desenvolvedora completa que vai desde o desenvolvimento até o deploy e monitoramento da aplicação!

Nesta talk de mais de 2h, falamos sobre cloud no geral, sobre suas diferenças e como podemos tirar o máximo proveito delas através de publicações com containers utilizando Docker e Azure Container Registry, também usamos o Azure Container Instances, App Services, passamos por Azure Functions e o tão famoso Azure Kubernetes Service para mostrar que você pode fazer o deploy da sua aplicação sem esforço!

]]>
<![CDATA[ Elevando o nível de microsserviços com service meshes ]]> https://blog.lsantos.dev/service-mesh-1/ 5f3bdab482fe718273265869 Sex, 21 Ago 2020 17:42:36 -0300 Desde que o Docker foi lançado – e até mesmo antes disso – pessoas desenvolvedoras se preocupam em como vão desacoplar suas aplicações das suas configurações e também da sua infraestrutura, de forma que elas possam ser facilmente migradas e possam facilmente se comunicar umas com as outras.

O advento do Kubernetes facilitou muito a implementação de melhores camadas de serviços, principalmente quando falamos de serviços distribuídos utilizando a arquitetura de microsserviços.

O grande problema é que, mesmo com todas essas facilidades, ainda enfrentamos muitas dificuldades quando o assunto é migrar uma aplicação ou até mesmo fazer com que ela seja mais independente. Principalmente por conta do modelo de comunicação que utilizamos.

O problema dos microsserviços

Quando estudamos microsserviços, vemos que o ideal é que tenhamos aplicações individuais que se comunicam com seus próprios bancos de dados e são independentes o suficiente a ponto de não precisarem de nenhuma outra aplicação externa para funcionarem.

A representação de um microsserviço versus um monólito (Fowler, 2015)

Além disso sempre estamos ouvindo frases como esta:

Um microsserviço é uma aplicação independente, que é auto-contida e pode utilizar seu próprio banco de dados para não depender de nenhuma outra parte do sistema.

O grande problema é que nem sempre conseguimos criar uma aplicação neste modelo, isso pode estar relacionado a diversos problemas mais impeditivos:

  • Grande aumento de complexidade
  • Custo para manter todos os bancos de dados
  • Dispersão dos dados
  • Duplicação de dados
  • Segurança

E outro grande problema que muitas empresas enfrentam quando utilizam microsserviços são os clássicos problemas da observabilidade (que vamos falar em outro artigo) e do Service Discovery. Apesar deste último ter sido resolvido em grande parte pelo uso de ferramentas de orquestração como o Kubernetes.

Então entramos em um assunto bastante interessante, o Service Mesh.

Service Mesh

Em palavras bastante simples, service mesh pode ser considerada uma nova "camada" que abstrai os problemas de rede quando estamos falando de comunicação entre serviços.

Porém, quando falamos de abstrações, não estamos falando apenas de algo passivo. Uma camada de service mesh também proporciona uma série de benefícios, entre eles os principais são:

  • Telemetria
  • Canary Deployments
  • Testes A/B
  • Roteamento de tráfego
  • Descoberta de rede (Service Discovery)
  • Monitoramento
  • Tracing

Estes pontos são os principais problemas que temos em arquiteturas distribuídas, perguntas do tipo "como posso monitorar todos os meus serviços de forma completa?", "como posso saber o número de requisições que estou recebendo?" geralmente exigem a existência de um recurso compartilhado, o API Gateway, que é um recurso muito comum em arquiteturas distribuídas como um ponto de entrada e controle para toda a malha de serviços por trás.

Em geral, chamar Service Mesh de camada está um pouco errado, porque ela não é uma camada no topo dos demais serviços, mas sim uma rede embutida diretamente na estrutura das aplicações.

Uma das principais vantagens de se utilizar um modelo de mesh é, por exemplo, não termos este recurso compartilhado, como teríamos com um API Gateway se tivermos que rotear as nossas requisições através de um único ponto de entrada, pois a implementação do roteamento já está dentro da aplicação em si.

Além disso, o uso de Service Mesh permite a implementação de um padrão chamado Circuit Breaker, que isola instâncias quebradas de uma aplicação até gradualmente trazê-las de volta à vida.

As implementações mais famosas de Service Mesh hoje são o Istio e o Linkerd.

Definições

Por se tratar de um novo padrão que permeia todo o ecossistema de serviços, algumas nomenclaturas se fazem necessárias.

Instâncias e serviços

Por padrão, todas as aplicações dentro de um service mesh são chamadas de serviços, cada uma delas é uma instância de um serviço.

Especificação de um pod do Kubernetes

Basicamente todo serviço é uma cópia em execução de algum microsserviço, algumas vezes essa instância é um único container, outras vezes pode ser uma aplicação constituída de mais de um container, que é o conceito de Pod que temos no Kubernetes.

Sidecar

O grande trunfo do Service mesh é o uso de sidecars. Sidecars são proxies em containers que vivem lado a lado com os serviços que estão suportando. Quando estamos falando em um Pod no Kubernetes, um sidecar é outro container que vive dentro do mesmo pod.

O sidecar é o responsável por receber o tráfego de rede e rotear este tráfego para sua aplicação irmã. O sidecar se comunica com os demais sidecars em outros microsserviços e são gerenciados pelo orquestrador de containers em uso. Além disso, o sidecar é um dos maiores responsáveis por obter métricas de uso de rede da aplicação.

Este é o grande trunfo porque um sidecar é basicamente uma implementação distribuída de um API Gateway. Ao invés de termos um único ponto de comunicação, temos vários pontos que permitem à rede saber como chegar do serviço A ao serviço B.

Data Plane

O Data Plane é o responsável pelo tráfego de rede entre as instâncias, isso é chamado de "Tráfego Leste-Oeste", como podemos ver na imagem anterior, este tipo de tráfego é aquele que está se movendo horizontalmente dentro da mesma rede. Ou seja, o data plane é o próprio sidecar.

Explicação de um tráfego norte-sul para um leste-oeste

O tráfego Norte-Sul, como mostrado na imagem acima, é a comunicação entre diferentes redes, ou seja, a comunicação interna para uma rede externa é um tipo de tráfego N-S, enquanto um serviço se comunicando com outro dentro do mesmo cluster é um exemplo de tráfego L-O.

Control Plane

O plano de controle, ou Control Plane, é somente utilizado pelas pessoas operando o service mesh, ou seja, é o plano que contém as ferramentas de controle para a malha de serviços.

Visão geral da arquitetura de um Service Mesh

Esta camada inclui, na maioria dos casos, uma API de comunicação com o Service Mesh e também pode incluir um CLI e/ou uma interface visual para controle da aplicação, com podemos ver na imagem abaixo.

Imagem do dashboard do Linkerd

Como um service mesh pode melhorar a comunicação de serviços

É difícil entender como uma simples malha de serviço pode melhorar a comunicação de uma rede inteira. Pois bem, vamos ver uma situação prática onde podemos tirar proveito das métricas e da visibilidade do service mesh para melhorar nossas aplicações.

Imagine que você possua um serviço que falhe constantemente, isso já é conhecido mas não se sabe uma possível solução, então sua aplicação implementa um sistema de tentativas que faz um retry da chamada a cada 5, 8, 10 e 15 segundos respectivamente a cada tentativa. Porém isso está causando um gargalo de requisições visto que, em horários de pico, sua aplicação está segurando o usuário por até 15 segundos.

Por conta da natureza inerentemente transparente das métricas do service mesh, toda a comunicação é documentada, incluindo as métricas de tráfego entre serviços. Com estas métricas em mãos, podemos ver o tempo médio que nosso serviço está falhando – ou até mesmo se é este serviço que está falhando e não um outro – e ajustar o nosso tempo de retry para ser equivalente.

Vamos supor que o tempo médio de falha é de 6 segundos, então nosso retry não está sendo efetivo, pois temos que fazer duas tentativas antes de poder ter uma terceira que está válida. Podemos fixar nosso tempo de retry para 6 segundos então, evitando uma carga desnecessária de requisições do serviço.

Conclusão

Nos próximos artigos vamos explorar ainda mais como podemos criar e manipular service meshes e também seus conceitos! Por isso se inscreva na newsletter para receber notícias semanais de todos os artigos e também outras notícias do mundo da tecnologia!

Até mais!

]]>
<![CDATA[ Um Mergulho em Imagens de Containers - Parte 3 ]]> https://blog.lsantos.dev/um-mergulho-em-imagens-de-containers-parte-3/ 5f36b6a482fe7182732657a9 Sex, 14 Ago 2020 14:34:25 -0300 No artigo anterior falamos como podemos criar uma imagem Docker da melhor maneira para linguagens consideradas estáticas, como o C ou o Go. Neste artigo vamos explorar um pouco mais a criação de imagens utilizando linguagens dinâmicas, como o Python ou o JavaScript.

Adeus Imagens Scratch

Como falamos lá no primeiro artigo, temos um tipo de imagem chamada scratch, que é uma imagem completamente vazia, realmente so um filesystem vazio. Utilizamos este tipo de imagem para construir nosso container no artigo anterior.

Porém, a péssima notícia é que não podemos utilizar este tipo de imagem para poder criar nossos containers dinâmicos, pois vamos precisar do runtime da linguagem instalado no sistema operacional, então vamos estar utilizando somente as imagens full, slim e alpine.

Builds de múltiplos estágios

Assim como fizemos no artigo anterior, é possível tirar vantagem de um processo de build de múltiplas fases, ou seja, temos um container que contém todos os recursos e ferramentas de desenvolvimento para construir a nossa aplicação, mas não utilizamos este container para produção, mas sim um outro container que conterá o mínimo possível.

Isto também é válido para linguagens dinâmicas, porém temos algumas modificações que precisamos fazer para que estas builds sejam mais eficientes. Como não vamos ter um único binário para copiar, o ideal seria copiar o diretório todo. Algumas linguagens como o Python possuem um bom relacionamento com este tipo de build porque esta linguagem possui o VirtualEnv, que permite que separemos logicamente os ambientes que estamos trabalhando.

Vamos fazer este teste com uma aplicação simples, uma API em JavaScript que envia emails – o código fonte pode ser visto aqui – Para começar, vamos analisar o Dockerfile com a imagem de build:

FROM node:12 AS builder

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

## Install dependencies
COPY ["./package.json", "./package-lock.json", "/usr/src/app/"]

RUN npm install

## Add source code
COPY ["./tsconfig.json", "/usr/src/app/"]
COPY "./src" "/usr/src/app/src/"

## Build
RUN npm run build

A imagem do Node:12 pode variar de espaço utilizado, mas a imagem crua possui cerca de 340Mb.  Como você pode observar, as imagens base de linguagens dinâmicas são bem maiores do que imagens de linguagens compiladas porque temos a necessidade do runtime estar junto.

Porém, vamos fazer uma alteração já que as imagens full podem ter muitas vulnerabilidades, vamos mudar para a imagem slim que tem aproximadamente 40mb

FROM node:12-slim AS builder

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

## Install dependencies
COPY ["./package.json", "./package-lock.json", "/usr/src/app/"]

RUN npm install

## Add source code
COPY ["./tsconfig.json", "/usr/src/app/"]
COPY "./src" "/usr/src/app/src/"

## Build
RUN npm run build

Podemos deixar ainda melhor se alterarmos a nossa imagem para uma imagem alpine!

FROM node:12-alpine AS builder

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

## Install dependencies
COPY ["./package.json", "./package-lock.json", "/usr/src/app/"]

RUN npm install

## Add source code
COPY ["./tsconfig.json", "/usr/src/app/"]
COPY "./src" "/usr/src/app/src/"

## Build
RUN npm run build

Agora a imagem de build tem somente 28mb iniciais para serem baixados.

Imagem de produção

Já criamos o nosso builder, vamos agora criar a nossa imagem de produção. Para isso, vamos utilizar a imagem alpine que é bem menor!

# PRODUCTION IMAGE

FROM node:12-alpine

RUN mkdir -p /usr/app
WORKDIR /usr/app

COPY --from=builder [\
  "/usr/src/app/package.json", \
  "/usr/src/app/package-lock.json", \
  "/usr/app/" \
  ]

COPY --from=builder "/usr/src/app/dist" "/usr/app/dist"
COPY ["./scripts/install_renderers.sh", "/usr/app/scripts/"]

RUN npm install --only=prod

EXPOSE 3000

ENTRYPOINT [ "npm", "start" ]

Estamos copiando apenas a pasta de saída do TypeScript para dentro da nossa imagem de produção e estamos apenas instalando as dependências necessárias para uma aplicação de produção com o npm install --only=prod.

Da mesma forma estamos expondo as portas necessárias e criando o script de inicialização somente nesta imagem e não na imagem de build, já que ela não vai ser utilizada.

Colocando todas juntas temos:

FROM node:12-slim AS builder

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

## Install dependencies
COPY ["./package.json", "./package-lock.json", "/usr/src/app/"]

RUN npm install

## Add source code
COPY ["./tsconfig.json", "/usr/src/app/"]
COPY "./src" "/usr/src/app/src/"

## Build
RUN npm run build

# PRODUCTION IMAGE

FROM node:12-alpine

RUN mkdir -p /usr/app
WORKDIR /usr/app

COPY --from=builder [\
  "/usr/src/app/package.json", \
  "/usr/src/app/package-lock.json", \
  "/usr/app/" \
  ]

COPY --from=builder "/usr/src/app/dist" "/usr/app/dist"
COPY ["./scripts/install_renderers.sh", "/usr/app/scripts/"]

RUN npm install --only=prod

EXPOSE 3000

ENTRYPOINT [ "npm", "start" ]

A imagem final tem aproximadamente 120mb, mas a imagem alpine do Node tem 28Mb, ou seja, temos aproximadamente 90mb de aplicações e dependências nesta imagem. Se estivéssemos utilizando uma imagem full, este tamanho seria facilmente maior do que 1gb.

Conclusão

Saber criar as suas imagens é uma habilidade importante, pois com ela podemos reduzir o tamanho e transformar nossa aplicação em algo muito mais conciso e leve que facilite muito mais o download e uso das nossas imagens.

Não se esqueça de se inscrever na newsletter para mais conteúdo exclusivo e notícias semanais! Curta e compartilhe seus feedbacks nos comentários!

Até mais!

]]>
<![CDATA[ Faça código em qualquer lugar com os Codespaces ]]> https://blog.lsantos.dev/faca-codigo-em-qualquer-lugar-com-os-codespaces/ 5f358fec82fe71827326564f Qui, 13 Ago 2020 19:20:32 -0300 O sonho de qualquer pessoa que trabalha com tecnologia é estar sempre com o seu melhor amigo, o seu computador. Existem diversas soluções que transformam pequenos dispositivos como Raspberry Pis em computadores completos, outras soluções criam pequenos computadores de bolso que podem ser acessados a todo o momento.

Existem vários motivos para alguém precisar – ou apenas gostar – de estar com um computador ao seu lado o tempo todo. Muitas vezes a pessoa prefere ter uma ferramenta super poderosa sempre a mão para eventuais problemas ou até mesmo desenvolver aquela ideia que teve no meio da rua. Em outros casos mais especiais, a pessoa pode ser uma das mantenedoras de projetos críticos e precisa estar sempre disposta a resolver eventuais problemas, isto tira a liberdade de locomoção, possuir um dispositivo destes faz com que a pessoa possa ganhar sua mobilidade de volta.

Podemos ter as melhores máquinas portáteis, porém nenhum computador se iguala ao nosso próprio, com nossos ambientes e com nossas configurações.

Codespaces

Tela de abertura do site do GitHub Codespaces

Recentemente o GitHub em seu evento Satellite 2020 o lançamento de uma nova funcionalidade na plataforma, os Codespaces. No momento, ela está disponível somente mediante a requisição de acesso antecipado pelo site. Porém, você pode ser um dos que recebeu acesso recentemente e poderá acessar essa incrível facilidade!

Por enquanto o Codespaces do GitHub está em beta e você pode não ter a funcionalidade habilitada. Então você não conseguirá ver o que mostrarei a seguir, porém você poderá se cadastrar diretamente no site para receber um acesso antecipado.

Os codespaces são uma implementação online do Visual Studio Code. Por ser um editor baseado em tecnologias web, o VSCode tem a incrível facilidade de ser um dos poucos projetos que podem ser portados entre plataformas de forma extremamente fácil.

Isso já havia sido feito antes com o Code Server – nós até chegamos a utilizar uma implementação interessante criada pelo Alejandro Oviedo, nosso colega da Argentina, no Nodeschool SP – porém um dos grandes problemas do Code Server eram que a loja de extensões não suportava todas as extensões existentes para o VSCode e também que o editor se confundia quando utilizávamos teclados com layouts diferentes.

A grande vantagem que o GitHub Codespaces (GH Codespaces ou GHC) trouxe é que ele é integrado diretamente ao GitHub, ou seja, você pode abrir qualquer repositório que você tenha permissão de escrita em um editor pronto na web! Veja um exemplo do meu repositório GotQL:

Tela de clonagem de um repositório do GitHub mostrando o botão "Open with Codespaces"

E tudo isso é grátis, tanto para repositórios pagos quanto para privados.

Como funciona

Os Codespaces são construídos em cima de uma outra solução já existente da Microsoft chamada Visual Studio Codespaces (VSC), que é uma excelente alternativa paga ao GH Codespaces quando você está procurando algo mais aberto, isto porque os VS Codespaces criam uma máquina virtual na Azure e conectam-se a ela através da funcionalidade nativa do VSCode chamada Remote Development.

O Remote Development se conecta a um outro computador rodando um pequeno servidor do outro lado. Ou seja, você consegue separar o processamento do editor, da interface. Dessa forma você consegue praticamente rodar o VSCode em qualquer lugar, porque todo o browser que suporte JavaScript mais recente consegue executar a interface do editor.

E então unimos outra tecnologia sensacional que são os containers. Como você já deve ter visto aqui mesmo no blog, os containers são uma tecnologia incrível que permite que você rode praticamente todas as aplicações de forma auto contida sem dependender de bibliotecas externas. Os Codespaces tiram muitas vantagens disso principalmente para que eles possam construir as imagens das máquinas que vão executar.

Dessa forma podemos ter um container que contém todas as ferramentas necessárias para o nosso projeto rodar, porque ele é completamente customizável.

Visual Studio Codespaces

Tela de abertura do Visual Studio Codespaces

Antes de mergulharmos no GitHub Codespaces, vou mostrar como podemos utilizar os VS Codespaces para criar um ambiente de desenvolvimento remoto, para que possamos entender com o que estamos lidando

Depois de fazer o login e criar sua instância do VSC, vamos criar um novo codespace:

Botão de criação para um Visual Studio Codespace

E, após clicarmos no botão "Create Codespace", as coisas começam a ficar interessantes, pois começamos a ter uma série de opções sensacionais que podem ser ativadas:

Tela de opções do Visual Studio Codespaces

A primeira opção é bem simples, temos que dar um nome para nosso codespace, será o nome que vamos identificar essa máquina, então precisa ser um nome bem descritivo.

Depois, temos a opção mais interessante, o próprio VSC já permite que começemos um Codespace a partir de outro repositório do GitHub, ou seja, o próprio GH Codespaces é uma interface diferente para os VSCs.

A partir daí temos a configuração das nossas VMs, então podemos escolher o quão forte a nossa máquina remota vai ser e também quanto tempo de ociosidade ela pode ter antes de se suspender para não gastar.

E então entramos na parte mais legal! Podemos definir o que são chamados de dotfiles, que são os arquivos de configuração do nosso shell. Dessa forma, nós podemos ter um repositório separado de dotfiles – como eu tenho – e um script para instalar estes dotfiles na nossa máquina online, ou seja, podemos replicar exatamente o que temos no nosso computador local em uma interface web!

Visual Studio Codespace online

GitHub Codespaces

Com o GHC é exatamente a mesma coisa! A única ação diferente que você terá de fazer será entrar em um repositório – como o GotQL – clicar no botão verde que seria o "clonar". Então clicar em "open with codespace":

Abrindo um novo repositório com codespace

Todos os usuários beta tem direito a 2 codespaces gratuitos. Ou seja, você pode manter até 2 máquinas de desenvolvimento sem pagar absolutamente nada. Depois, você precisará remover as antigas para criar novas. No exemplo do GotQL, temos a opção de abrir com o Codespaces, esta ação já vai criar um ambiente pronto para desenvolvimento, com todas as dependências instaladas

Mas como fazemos isso?

Personalização

Uma das funcionalidades mais legais dos codespaces é o fato de que eles são completamente personalizáveis.

No caso dos dotfiles, você pode configurar tanto o seu VSCode local (o editor instalado na sua máquina) para buscar uma série de arquivos conforme esta documentação explica ou, no caso do GitHub, você pode ter diretamente um repositório chamado dotfiles, como a documentação do GitHub explica.

Porém você pode fazer mais do que isso com uma pasta chamada .devcontainer. O que esta pasta faz é agrupas as configurações possíveis para um codespace. Nela podemos ter um arquivo Dockerfile, um arquivo chamado devcontainer.json e um arquivo sh que seria a configuração da shell do nosso ambiente. Quando criarmos um codespace a partir de um repositório, o GitHub irá buscar esta pasta no repositório e irá criar o codespace de acordo com ela.

Vamos ver o exemplo do .devcontainer do GotQL. Nós temos um Dockerfile, ele é o responsável por dizer qual tipo de container, ou qual tipo de ambiente vamos ter, será nele que vamos instalar pacotes, criar usuários e etc. Bem como escolher a imagem do nosso sistema operacional base.

FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:14

# The javascript-node image includes a non-root node user with sudo access. Use 
# the "remoteUser" property in devcontainer.json to use it. On Linux, the container 
# user's GID/UIDs will be updated to match your local UID/GID when using the image
# or dockerFile property. Update USER_UID/USER_GID below if you are using the
# dockerComposeFile property or want the image itself to start with different ID
# values. See https://aka.ms/vscode-remote/containers/non-root-user for details.
ARG USERNAME=node
ARG USER_UID=1000
ARG USER_GID=$USER_UID

# Alter node user as needed, install tslint, typescript. eslint is installed by javascript image
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
        groupmod --gid $USER_GID $USERNAME \
        && usermod --uid $USER_UID --gid $USER_GID $USERNAME \
        && chmod -R $USER_UID:$USER_GID /home/$USERNAME \
        && chmod -R $USER_UID:root /usr/local/share/nvm /usr/local/share/npm-global; \
    fi \
    #
    # Install tslint, typescript. eslint is installed by javascript image
    && sudo -u ${USERNAME} npm install -g tslint typescript gitmoji-cli

Então, quando um codespace do GotQL for criado, será esse o ambiente que vamos ter, um ambiente com um usuário não root com acesso ao sudo e o tslint e TypeScript já instalados. Também adicionei o gitmoji-cli que é o padrão de commits que uso neste projeto.

Depois, temos o arquivo devcontainer.json, que é o responsável não só por setar as configurações do nosso editor mas também por dar as direções para o construtor do codespace. Nele definimos o nome do codespace, as extensões que nosso VSCode online terá logo que for iniciado, qual é o Dockerfile que ele precisará usar para construir a base do sistema e também podemos sobrescrever as configurações do próprio VSCode.

{
  "name": "TypeScript website codespace",
  "extensions": [
    "emmanuelbeziat.vscode-great-icons",
    "dbaeumer.vscode-eslint",
    "oderwat.indent-rainbow",
    "vtrois.gitmoji-vscode",
    "dracula-theme.theme-dracula",
    "2gua.rainbow-brackets",
    "ms-vscode.vscode-typescript-tslint-plugin"
  ],
  "dockerFile": "Dockerfile",
  // Set *default* container specific settings.json values on container create.
  "settings": { 
    "terminal.integrated.shell.linux": "/bin/bash",
    "window.autoDetectColorScheme": true,
    "workbench.preferredDarkColorTheme": "Dracula",
    "editor.renderWhitespace": "boundary",
    "workbench.colorTheme": "Dracula",
    "workbench.iconTheme": "vscode-great-icons"
  },
  // Use 'postCreateCommand' to run commands after the container is created.
  "postCreateCommand": "npm install"
}

Além disso temos a adição de um postCreateCommand, que é extremamente útil para rodar comandos depois que o codespace foi criado, neste caso estamos rodando o comando npm install, pois assim já teremos todos os pacotes instalados quando abrirmos.

Também temos um excelente repositório de exemplos de codespaces que podem servir de base para que você possa criar os seus. Vamos buscar o arquivo setup.sh do exemplo de codespace para Node

## update and install some things we should probably have
apt-get update
apt-get install -y \
  curl \
  git \
  gnupg2 \
  jq \
  sudo \
  zsh

## set-up and install yarn
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
apt-get update && apt-get install yarn -y

## install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash

## setup and install oh-my-zsh
sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
cp -R /root/.oh-my-zsh /home/$USERNAME
cp /root/.zshrc /home/$USERNAME
sed -i -e "s/\/root\/.oh-my-zsh/\/home\/$USERNAME\/.oh-my-zsh/g" /home/$USERNAME/.zshrc
chown -R $USER_UID:$USER_GID /home/$USERNAME/.oh-my-zsh /home/$USERNAME/.zshrc

Este arquivo é referenciado dentro do Dockerfile, sendo executado assim que é iniciado.

Conclusão

Os codespaces podem se tornar uma das principais tecnologias que temos atualmente, principalmente pelo fato de que eles permitem uma edição ou então até mesmo um desenvolvimento mais complexo utilizando aparelhos móveis como o celular.

Muitos programadores já utilizavam soluções baseadas no Docker para programar no ipad, porém estas soluções acabavam sempre sendo mais como "gambiarras" do que soluções propriamente ditas.

Com o surgimento de tecnologias e funcionalidades como estas, temos a capacidade de levar nosso ambiente de trabalho para qualquer lugar. Fiquem ligados nos próximos artigos onde vou explicar como montei meu ambiente de trabalho remoto usando o GHC!

Até mais!

]]>
<![CDATA[ Hipsters.tech #212 - Tecnologias Cloud na Microsoft ]]> https://blog.lsantos.dev/hipsters-tech-212/ 5f297e2982fe7182732655c1 Ter, 04 Ago 2020 17:21:26 -0300 Esta semana tive o prazer imenso de participar mais uma vez do meu podcast de tecnologia favorito! O Hipsters.tech.  Já acompanho o Hipsters há anos, praticamente desde que o projeto começou e poder participar dele é uma das coisas que tenho mais orgulho!

Neste podcast comentamos sobre tecnologias cloud da Microsoft, como funciona a cloud, como a Azure surgiu e como você pode criar aplicações utilizando o que já existe por lá!

Além do que comentamos no podcast, falamos de várias coisas super legais! Vou deixar a lista de todos os links que chegamos a puxar o assunto!

Temos o Microsoft Learn! Um serviço completamente gratuito que você pode usar para aprender qualquer tecnologia cloud Microsoft:

Microsoft Learn
The skills required to advance your career and earn your spot at the top do not come easily. Now there’s a more rewarding approach to hands-on learning that helps you achieve your goals faster. Earn points, levels, and achieve more!

O curso de introdução à Azure gratuito do Microsoft Learn:

Deploy a website to Azure with Azure App Service learning path - Learn
In this learning path, get acquainted with using Azure App Service to create and deploy your website without underlying servers, storage or network assets.

Toda a documentação da Microsoft sobre a Azure:

Azure documentation
Learn how to build and manage powerful applications using Microsoft Azure cloud services. Get documentation, example code, tutorials, and more.

Visual Studio Codespaces, que permite a você criar uma instância do seu VSCode e utilizá-lo online!

Visual Studio Codespaces
]]>
<![CDATA[ Executando Containers no Azure Container Instancies com Docker ]]> https://blog.lsantos.dev/executando-containers-no-azure-container-instancies-com-docker/ 5f230a1c82fe71827326548d Seg, 03 Ago 2020 10:00:00 -0300 Se você já utiliza Docker então, provavelmente, já executou um container em sua máquina. Porém, já imaginou como seria se você pudesse executar diretamente um container em uma infraestrutura cloud sem precisar fazer nada novo?

No mês passado o time do Docker anunciou que as novas versões do Docker para desktop (chamadas de versões Edge) estariam ganhando o suporte nativo aos Azure Container Instances, ou seja, poderíamos rodar containers da nossa máquina diretamente para um ambiente cloud sem precisar baixar nenhuma imagem localmente!

Azure Container Instances

Os Azure Container Instances (ACI) é um dos produtos oferecidos pela Azure que permitem que você faça o deploy de uma aplicação em um container sem se preocupar com máquinas virtuais ou qualquer outro tipo de infraestrutura.

Já cheguei a utilizar o ACI quando realizamos uma talk para o #CodeInQuarentena. No vídeo abaixo você pode ver o vídeo onde criamos uma API completa utilizando Mongoke e ACI

Vídeo do Code In Quarentena sobre como fazer um deploy no ACI

Essencialmente, o ACI é uma forma de você rodar sua aplicação como um container online. Dessa forma a única coisa que você precisa ter é uma conta na Azure e uma aplicação que já esteja dentro de um container. O melhor é que isso é tudo cobrado pelo segundo de uso, então você realmente não para por computação ociosa. O que é excelente para aplicações que precisam ser criadas e destruídas rapidamente.

Mas como o ACI pode ser integrado com o Docker?

Integrando o ACI com o Docker

Vamos realizar a nossa integração entre o Docker e o ACI. Assumindo que você já possua uma conta na Azure e já conheça um pouco sobre Docker, o que você precisa fazer é baixar o Docker Edge.

Docker Edge

O Docker Edge é uma versão experimental do Docker que recebe todas as atualizações que ainda não foram disponibilizadas na versão de disponibilidade geral. Para que a integração com o ACI aconteça, como ela ainda é uma flag experimental, temos que baixar a versão Edge do Docker, este repositório tem todos os links necessários para instalação em várias plataformas.

Se você tem o Docker instalado na sua máquina, você precisará remover a versão CE oficial e instalar o Docker Edge. Não é possível ter ambas as versões instaladas na mesma máquina.

Uma vez que o Docker estiver instalado, você deverá ver esta tela:

Imagem mostrando a página principal de preferências do Docker Edge

Veja na aba Command Line se você possui a opção Enable Cloud Experience ativada:

Imagem mostrando a opção "Enable Cloud Experience" ativa nas ferramentas do Docker

Com isto você já tem a integração ativa, agora o que precisamos fazer é criar um contexto!

Docker Context

Os contextos do Docker não são uma funcionalidade nova. O objetivo dos contextos é permitir que você possa alterar o local de onde você está trabalhando. Isto é muito interessante depois do advento do Kubernetes para Docker, pois com os contextos você pode alterar entre o uso do Docker engine, do Kubernetes dentro do Docker ou até mesmo de um contexto do Docker Swarm.

Com o ACI não é diferente, temos que criar um contexto para a integração com a cloud! Isso é bastante simples, primeiro temos que logar na Azure através do seguinte comando:

$ docker login azure

Após isto, você será levado ao site da Azure para realizar o login, uma vez que ele for completado, você poderá voltar para o CLI e executar o seguinte comando:

$ docker context create aci nome-do-contexto

Em seguida você terá de escolher a sua subscription e depois o resource group que será utilizado para subir as imagens. Uma vez terminado, você poderá rodar o comando docker context ls para ver todos os contextos existentes e a sua integração está completa!

Criando um container

Para podermos executar este exemplo, vamos usar uma imagem pública que tenho no Docker Hub, chamada Simple Node API.

Vamos começar trocando o nosso contexto para o novo contexto criado através do comando docker context use <nome-do-contexto>.

Para fazermos o deploy, podemos rodar o comando que estamos acostumados, o docker run:

$ docker run -d -p 80:80 --name node-api -e PORT=80 khaosdoctor/simple-node-api

Veja o output que vamos ter:

Para conseguirmos buscar o caminho que devemos acessar para vermos a nossa API online, vamos executar um docker ps e buscar os conteúdos de PORTS.

Veja que tenho que acessar o IP 13.86.141.148 para poder ver o resultado da API no browser, vamos lá!

Imagem mostrando o browser acessando o IP anterior e o resultado da API com a frase "Hello World"

Veja que, se acessarmos o portal da Azure, vamos ter o nosso recurso criado como se tivéssemos criado manualmente!

Imagem do portal da Azure com o nosso recurso "node-api" criado

Para removermos, basta executar o comando docker rm <nome>, veja que não é possível executar o comando docker stop porque este tipo de integração não permite que paremos o container remotamente. Se digitarmos docker rm node-api vamos ter o nosso ACI removido da azure e o nosso container parado!

Outras aplicações

Podemos utilizar também o ACI para criar aplicações multi-container usando o Docker Compose. Para isto, vamos utilizar o próprio exemplo que a Docker nos dá do site da DockerCon! Temos o seguinte arquivo YAML:

# docker-conpose.yaml
version: '3.3'

services:
  db:
    image: bengotch/acidemodb
  
  words:
    image: bengotch/acidemowords
  
  web:
    image: bengotch/acidemoweb
    ports:
      - "80:80"

Fazemos o mesmo processo. Porém, ao invés de rodar o comando docker run vamos rodar o comando docker compose up -d.

Note que não temos um - entre o docker e o compose como era de se esperar quando estamos rodando o comando localmente. Porque o compose é um comando da integração em si!

Depois podemos utilizar o comando docker ps normalmente para pegar o IP público e ver o site no ar:

Site da DockerCon no ar utilizando a integração com ACI

Limitações

  • O ACI não suporta mapeamento de portas, portanto você precisa ter certeza de que seu container está rodando na mesma porta no host e no container através da flag -p porta:porta (veja esta issue)
  • Nem todos os comandos presentes no docker estão presentes na integração (veja esta outra issue para mais detalhes)

Conclusão

Com a integração do ACI podemos integrar muito mais rápido com a Azure e isso facilita muito o desenvolvimento de aplicações voltadas para a cloud! Experimente você também!

Não se esqueça de se inscrever na newsletter aqui embaixo para mais conteúdo exclusivo e notícias semanais! Curta e compartilhe seus feedbacks nos comentários logo após o post!

Até mais.

]]>
<![CDATA[ Imersão Alura React Live #02 - Usando TypeScript no nosso projeto ]]> https://blog.lsantos.dev/imersao-react-live-02-usando-typescript-no-nosso-projeto/ 5f21fc6a82fe7182732653e2 Qua, 29 Jul 2020 20:28:30 -0300 Hoje tive o prazer de participar junto com o Mario Souto da #ImersãoAlura uma live super animada sobre como podemos utilizar TypeScript no nosso projeto React!

Veja como foi a live no vídeo abaixo:

Veja o vídeo da imersão React live!

Durante a live tivemos várias perguntas super pertinentes sobre como funcionava o TypeScript, quais eram as diferenças entre um tipo e uma interface, além de muitos outros conteúdos muito legais!


Conteúdo Comentado

Para facilitar o seu aprendizado, estou agrupando aqui todo o conteúdo relacionado que comentamos durante o vídeo!

Symbol.Iterator

Logo no início da live comentamos sobre a evolução do JavaScript e como o Node.js e o CoffeeScript foi essencial para que o ecossistema evoluísse. Então comentamos um pouco sobre Iterators e Downleveling.

Para que você entenda um pouco sobre Iterators, separei um artigo que escrevi há um tempo:

Javascript— Entendendo Iterators
Com certeza você já ouviu falar sobre iterators em outras linguagens como o C++ e o C#, mas aposto que você não sabia que o Javascript também era um adepto deste protocolo.

E, para complementar, um artigo sobre Symbols:

Javascript Symbols: Decifrando o mistério
Neste artigo conheça o mundo obscuro do Javascript, os Symbols, saiba que eles são estruturas extremamente poderosas e para que você pode utiliza-las!

Node.js Por baixo dos Panos

O Mário comentou sobre uma série de artigos que produzi ao longo de um ano sobre os fundamentos do Node.js, estes artigos podem ser encontrados aqui:

Node.js Por Baixo dos Panos #1 - Conhecendo nossas ferramentas
Esta é uma tradução do meu artigo original Recentemente eu fui convidado para fazer uma palestra em...

Além disso, você pode encontrar também uma palestra minha sobre este tema (em inglês) no The Conf 2019:

Veja os slides aqui:

Deno

Tivemos muitas perguntas sobre Deno também! Por isso vou deixar aqui o Hipsters que participei sobre a tecnologia!

Deno, o novo Node? - Hipsters #203 - Hipsters Ponto Tech

E aproveito para deixar um tutorial de como fazer o deploy de uma aplicação Deno para a Azure!

Deploy Deno apps to Azure App Service from the Azure CLI
Tutorial part 1, introduction and prerequisites.

Promises

Uma pergunta bastante comum e um grande problema para quem está aprendendo JavaScript é o entendimento do fluxo assíncrono de mensagens usando Promises! Por isso vou deixar aqui um artigo que escrevi sobre o assunto

Entendendo Promises de uma vez por todas
Promises sempre foram o calcanhar de Aquiles de muitos programadores. Com este guia vamos desmistificar o que é uma Promise e como ela funciona

Juntamente com a Live que participei sobre o tema:

Ajude a traduzir o site do TypeScript!

Estou, em conjunto com meu colega Orta – que coordena a criação e desenvolvimento do site do TypeScript – coordenando a tradução para a língua portuguesa do site do TypeScript. Você pode nos ajudar traduzindo os arquivos!

Tenha seu código no repositório oficial do website 😱

Portuguese Localization Coordination · Issue #233 · microsoft/TypeScript-Website
tsconfig intro.md allowJs.md allowSyntheticDefaultImports.md allowUmdGlobalAccess.md allowUnreachableCode.md allowUnusedLabels.md compilerOptions.md top_level.md watchOptions.md Additional_Checks_6...

Outros conteúdos relacionados

O TypeScript é um mundo imenso, então estou linkando alguns outros posts e conteúdos sobre o universo TypeScript que podem abrir a sua mente para as coisas que você pode realizar

Padrões de projeto com TS

Escrevi um guia de algumas partes sobre como podemos aplicar padrões de projetos com TypeScript lá no iMasters!

Artigo novo: Design Patterns com JavaScript & TypeScript
Os Design Patterns fazem parte do dia-a-dia de uma pessoa desenvolvedora de software quer ela queira ou não, muitas vezes nem sabemos

Arquitetura de projetos com TS

Em conjunto com outras pessoas produzi alguns vídeos sobre como podemos aplicar diversas arquiteturas como Event Sourcing:

Event Sourcing - A arquitetura que pode salvar seu projeto - iMasters - We are Developers
No seguinte artigo, Lucas Santos apresenta os conceitos do Event Sourcing e explica suas principais vantagens e desvantagens.

Juntamente com o vídeo explicativo:

E os slides para esta palestra:

E, para finalizar, o conteúdo sobre como podemos criar uma aplicação em camadas usando TS!


Espero que tenham gostado da live!

Não deixe de acompanhar mais do meu conteúdo no meu blog e se inscreva na newsletter para receber notícias semanais!

Até mais!

]]>
<![CDATA[ Um Mergulho em Imagens de Containers - Parte 2 ]]> https://blog.lsantos.dev/um-mergulho-em-imagens-de-containers-parte-2/ 5f2074cc82fe718273265324 Qua, 29 Jul 2020 10:00:00 -0300 No último post da nossa série falamos um pouco sobre o que são as imagens de containers e como elas se dividem. Passamos sobre o que é uma imagem slim, uma imagem full e falamos sobre as imagens alpine. Mas, o que isso impacta para a sua aplicação?

A aplicação

Vamos construir uma aplicação simples utilizando Go que serve um arquivo estático em HTML para descrever como podemos otimizar a nossa imagem para a nossa aplicação.

Não vamos desenvolver esta aplicação aqui, vamos apenas imaginar a seguinte situação:

  • Temos uma aplicação em Go que serve um arquivo estático HTML
  • Esta aplicação compila sem erros, é pequena e rápida

Otimizando a build

Para linguagens compiladas como: Go, C#, Java e outras, uma boa prática antes até de escolher qualquer tipo de sistema operacional base é otimizar a compilação, ou build da aplicação. Para isso, o que é geralmente utilizado, porém não muito divulgado, é o que é chamado de builds de múltiplos estágios.

Quando realizamos a compilação de alguma aplicação, geralmente o container que está fazendo esta compilação possui todas as ferramentas necessárias para que a compilação seja feita. Isso inclui ferramentas que não são necessárias no ambiente de execução. Por exemplo, com o Go, não temos que possuir o runtime inteiro instalado, pois ele já é capaz de rodar como um binário único, então não faz sentido termos todo esse ferramental instalado no nosso container que vai ser executado em produção.

E é isso que as builds de múltiplos estágios fazem. Começamos com um container – que contém todo o ferramental para a build da aplicação, geralmente um container próprio para aquela linguagem – e depois movemos somente o binário gerado no final para um novo container vazio. Neste container vazio não temos nenhuma dessas ferramentas que não vamos precisar utilizar em produção.

Este tipo de build é utilizado para manter as imagens de produção as menores possíveis, assim não temos problemas com entrada e saída de rede e também temos menos dados para baixar! E a grande vantagem é que podemos fazer isso direto do nosso Dockerfile!

Criando uma build otimizada

Para começarmos, vamos criar o Dockerfile, dentro dele vamos criar os passos responsáveis por fazer com que a aplicação seja construída:

FROM golang:1.11-stretch as build

WORKDIR /go/src/github.com/khaosdoctor/webapp

COPY web.go web.go

RUN CGO_ENABLED=0 GOOS=linux go build -o ./bin/webapp github.com/khaosdoctor/webapp

FROM debian:stretch

RUN mkdir -p /web/static/ 

COPY --from=build /go/src/github.com/khaosdoctor/webapp/bin/webapp /usr/bin
COPY index.html /web/static/index.html

WORKDIR /web

EXPOSE 3000

ENTRYPOINT ["webapp"]

Veja que estamos dando um nome ao primeiro container como FROM <imagem>:<tag> as build, o as build é o que dá o nome ao nosso container intermediário e diz que este container não será o passo final da nossa esteira de construção.

Veja que estamos usando uma imagem full na construção. Isso é ok porque esse container vai ser descartado.

Vamos pular uma linha e criar o nosso próximo container, que será o container final com nossa aplicação já construída para produção:

# Começo do container de build
FROM golang:1.11-stretch as build

WORKDIR /go/src/github.com/khaosdoctor/webapp

COPY web.go web.go

RUN CGO_ENABLED=0 GOOS=linux go build -o ./bin/webapp github.com/khaosdoctor/webapp

# Começo do container de produção
FROM debian:stretch-slim

RUN mkdir -p /web/static/ 

COPY --from=build /go/src/github.com/khaosdoctor/webapp/bin/webapp /usr/bin
COPY index.html /web/static/index.html

WORKDIR /web

EXPOSE 3000

ENTRYPOINT ["webapp"]

Perceba uma instrução importante. Estamos usando COPY --from=build, ou seja, estamos dizendo para o Docker copiar os arquivos não do nosso filesystem, mas sim de outro container intermediário! Isso é o que define uma build de múltiplos estágios.

Reduzindo o tamanho da imagem

Agora veja que estamos utilizando uma imagem debian:stretch-slim, como já vimos, essa imagem é menor do que uma imagem full, portanto tem menos vulnerabilidades e ocupa menos espaço. Um teste de construção das duas imagens mostra que a imagem full tem mais ou menos 110mb contra 62mb da imagem slim.

Além das duas imagens, temos mais um tipo de imagem que comentamos, a alpine, que é baseada no Alpine Linux. Para isso a gente só precisa mudar a imagem base para o alpine:3.8

# Começo do container de build
FROM golang:1.11-stretch as build

WORKDIR /go/src/github.com/khaosdoctor/webapp

COPY web.go web.go

RUN CGO_ENABLED=0 GOOS=linux go build -o ./bin/webapp github.com/khaosdoctor/webapp

# Começo do container de produção
FROM alpine:3.8

RUN mkdir -p /web/static/ 

COPY --from=build /go/src/github.com/khaosdoctor/webapp/bin/webapp /usr/bin
COPY index.html /web/static/index.html

WORKDIR /web

EXPOSE 3000

ENTRYPOINT ["webapp"]

Agora temos uma imagem de 11mb e sem nenhuma vulnerabilidade ou dependência externa. Então podemos ver que mover uma imagem para o Alpine é uma excelente pedida quando estamos trabalhando com aplicações compiladas que já possuem seu runtime junto com seu binário.

Otimizando do zero

Para finalizar, vamos tentar colocar nossa aplicação em uma imagem scratch. Se lembrarmos bem, uma imagem scratch, na verdade, não possui absolutamente nada instalado, ou seja, ela é uma imagem "from scratch". Aqui vamos poder ver duas principais mudanças.

A primeira será no nosso container de build:

# Começo do container de build
FROM golang:1.11.2-alpine3.8 as build

WORKDIR /go/src/github.com/khaosdoctor/webapp

COPY web.go web.go

RUN CGO_ENABLED=0 GOOS=linux go build -o ./bin/webapp github.com/khaosdoctor/webapp

Veja que estamos utilizando o Alpine para buildar nossa aplicação, uma vez que o scratch não possui absolutamente nada. Então vamos construir a imagem em um container Alpine e copiar o binário para o container de produção scratch.

Podemos fazer isso também com a imagem de build no container Slim. Isso fará com que a build seja ainda mais rápida porque a imagem de build será menor.

Agora vamos copiar o binário do Go para dentro da imagem scratch:

# Começo do container de build
FROM golang:1.11.2-alpine3.8 as build

WORKDIR /go/src/github.com/khaosdoctor/webapp

COPY web.go web.go
COPY index.html /web/static/index.html

RUN CGO_ENABLED=0 GOOS=linux go build -o ./bin/webapp github.com/khaosdoctor/webapp

# Começo do container de produção
FROM scratch

RUN mkdir -p /web/static/ 

COPY --from=build /go/src/github.com/khaosdoctor/webapp/bin/webapp /usr/bin
COPY --from=build /web/static/index.html /web/static/index.html

EXPOSE 3000

ENTRYPOINT ["/usr/bin/webapp"]

Perceba que fizemos duas mudanças principais:

  1. Tiramos a instrução WORKDIR, porque o scratch não tem um sistema de arquivos inicial, então tudo está no mesmo diretório
  2. O ENTRYPOINT agora é um caminho completo, pois o container scratch não possui um PATH para olhar uma vez que ele não tem um SO

Com isso, reduzimos ainda mais o tamanho da imagem para 7mb. E, além disso, temos a melhor segurança possível, já que não temos nenhum tipo de pacote instalado na nossa imagem.

Conclusão

Aprendemos como melhor construir uma imagem para uma linguagem compilada, neste caso foi Go, mas você pode transpor essa criação para qualquer outra linguagem que precise de compilação prévia!

Não se esqueça de se inscrever na newsletter aqui embaixo para mais conteúdo exclusivo e notícias semanais! Curta e compartilhe seus feedbacks nos comentários logo após o post!

Até mais.

]]>
<![CDATA[ Meetup: 1º Meetrybe - Arquitetura de código ]]> https://blog.lsantos.dev/1-meetrybe-arquitetura-de-codigo/ 5f1c583282fe718273265245 Sáb, 25 Jul 2020 14:01:16 -0300 Hoje participei do Meetrybe, o primeiro evento online da Trybe junto com um monte de gente de peso como: Elton Minetto, Rogério Munhoz e Igor Halfeld.

Depois de palestras sensacionais do Elton e da Ana Cardoso, montamos uma roda de perguntas e respostas sobre arquitetura de software. Tiramos diversas dúvidas super legais e ainda criamos frases icônicas como:

Todo o legado um dia já foi um hype

Foi uma conversa super descontraída! Dá uma olhada como rolou!

1º Meetrybe

Leitura recomendada

Dois excelentes livros clássicos sobre clean code e arquitetura clean:

Clean Code: A Handbook of Agile Software Craftsmanship (Robert C. Martin Series) (English Edition) - eBooks em Inglês na Amazon.com.br
Compre Clean Code: A Handbook of Agile Software Craftsmanship (Robert C. Martin Series) (English Edition) de Martin, Robert C. na Amazon.com.br. Confira também os eBooks mais vendidos, lançamentos e livros digitais exclusivos.
Livro Clean Code
Arquitetura Limpa: o Guia do Artesão Para Estrutura e Design de Software | Amazon.com.br
Compre online Arquitetura Limpa: o Guia do Artesão Para Estrutura e Design de Software, de Martin, Robert C., Henney, Kevlin, Gorman, Jason na Amazon. Frete GRÁTIS em milhares de produtos com o Amazon Prime. Encontre diversos livros escritos por Martin, Robert C., Henney, Kevlin, Gorman, Jason com ó…
Livro Clean Architecture

Você consegue também encontrar os dois livros acima em um pacote:

The Robert C. Martin Clean Code Collection (Collection) (Robert C. Martin Series) (English Edition) - eBooks em Inglês na Amazon.com.br
Compre The Robert C. Martin Clean Code Collection (Collection) (Robert C. Martin Series) (English Edition) de Martin, Robert C. na Amazon.com.br. Confira também os eBooks mais vendidos, lançamentos e livros digitais exclusivos.
Pacote clean architecture

O clássico livro do GoF sobre padrões de projeto, que estão inspirando meu conteúdo no iMasters:

Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos | Amazon.com.br
Compre online Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos, de Gamma, Erich, Helm, Richard, Johnson, Ralph, Vlissides, John, Salgado, Luiz A. Meirelles, Paulo, Fabiano Borges na Amazon. Frete GRÁTIS em milhares de produtos com o Amazon Prime. Encontre diversos livros …

Uma série de livros sensacional sobre gestão e aplicação de projetos

O Projeto Fênix: um Romance Sobre TI, DevOps e Sobre Ajudar o seu Negócio a Vencer | Amazon.com.br
Compre online O Projeto Fênix: um Romance Sobre TI, DevOps e Sobre Ajudar o seu Negócio a Vencer, de Kim, Gene, Spafford, George, Behr, Kevin na Amazon. Frete GRÁTIS em milhares de produtos com o Amazon Prime. Encontre diversos livros de Computação, Informática e Mídias Digitais com ótimos preços.
The Unicorn Project: A Novel about Developers, Digital Disruption, and Thriving in the Age of Data (English Edition) - eBooks em Inglês na Amazon.com.br
Compre The Unicorn Project: A Novel about Developers, Digital Disruption, and Thriving in the Age of Data (English Edition) de Kim, Gene na Amazon.com.br. Confira também os eBooks mais vendidos, lançamentos e livros digitais exclusivos.

Este livro sensacional sobre OOP com Java:

Design Patterns com Java: Projeto orientado a objetos guiado por padrões eBook: Guerra, Eduardo: Amazon.com.br: Loja Kindle
Design Patterns com Java: Projeto orientado a objetos guiado por padrões eBook: Guerra, Eduardo: Amazon.com.br: Loja Kindle

O livro clássico do Erick Evans sobre DDD:

Domain-Driven Design: Atacando as Complexidades no Coração do Software | Amazon.com.br
Compre online Domain-Driven Design: Atacando as Complexidades no Coração do Software, de Evans, Eric, Fowler, Martin na Amazon. Frete GRÁTIS em milhares de produtos com o Amazon Prime. Encontre diversos livros escritos por Evans, Eric, Fowler, Martin com ótimos preços.

Conclusão

É isso ai pessoal! Espero que gostem do conteúdo! Não deixem de curtir e compartilhar o post e se inscrever na newsletter para conteúdos exclusivos por email!

Valeu!

]]>
<![CDATA[ Um Mergulho em Imagens de Containers - Parte 1 ]]> https://blog.lsantos.dev/um-mergulho-em-imagens-de-containers-parte-1/ 5f183ff2f1911484af3ab9b0 Sex, 24 Jul 2020 10:00:00 -0300

Recentemente li uma série de artigos do Scott Coulton[1][2][3] no Medium sobre como escolher as imagens base para seus containers. Decidi então escrever estes artigos para que outras pessoas também possam entender como escolher melhor como começar a criar seus containers usando Docker da melhor forma possível!


  1. I Chose You Container Image - Part 1 ↩︎

  2. I Chose You Container Image - Part 2 ↩︎

  3. I Chose You Container Image - Part 3 ↩︎

Imagens Base

Quando começamos a criar nossas imagens, a primeira coisa que temos que nos preocupar é: Qual será a imagem base que vamos utilizar para a nossa aplicação?

Por exemplo, eu crio muitas imagens que utilizam o Node.js, portanto minha escolha natural seria utilizar uma imagem base do próprio Node, como a node:14 que é uma imagem oficial presente no DockerHub.

Porém eu poderia tranquilamente criar minha própria imagem tendo o Node como uma ferramenta instalada ao invés de tê-la nativamente a partir da minha imagem base. Para isso, basta que eu escolha uma outra imagem base que contenha apenas o sistema operacional, poderíamos utilizar a imagem do Debian e então criar um Dockerfile parecido com o seguinte:

FROM debian:buster 
RUN sudo apt update \ 
    && sudo apt upgrade -y \ 
    && sudo apt install -y curl \ 
    && curl -sL https://deb.nodesource.com/setup_14.x | bash - \ 
    && sudo apt install -y nodejs

O que não é muito diferente do que é feito na imagem oficial. Mas será que estamos criando uma imagem que possui os requisitos básicos para que ela seja utilizada por outras pessoas?

O que quero dizer com esta introdução é que temos que pensar muito além da imagem base. Qualquer imagem do Docker precisa ser pensada para obter a melhor escalabilidade e usabilidade possível. Isto é feito com os seguintes requisitos:

  • O tamanho da imagem é pequeno o suficiente
  • A aplicação que executará dentro da imagem possui uma boa performance
  • A segurança está garantida dentro da imagem base

Para podermos atacar todas as partes, vamos trabalhar com linguagens compiladas – como Golang – na próxima parte deste artigo e, na última parte, vamos trabalhar com linguagens dinâmicas – como JavaScript ou Python.

Tipos de imagens base

Para deixar a escolha diretamente a quem lê, não vou dizer se um sistema operacional é melhor que outro, afinal isto é uma escolha completamente pessoal. Ao invés disso, vamos trabalhar com três tipos de nomenclaturas:

  • Imagens full
  • Imagens slim
  • Imagens alpine

Imagens Full

departure from Euromax terminal, 400 meters long
Foto de um navio carregado de containers Andrey Sharpilo / Unsplash

As imagens full (ou completas), são imagens que contém toda a extensão de um sistema operacional, por exemplo, uma imagem debian-full é uma imagem que possui a distribuição Debian do Linux por completo, com todos os pacotes e módulos carregados.

Este tipo de imagem é o melhor tipo de imagem para se começar e, provavelmente, o jeito mais simples de colocar uma aplicação no ar. Isto porque elas possuem todas as ferramentas e pacotes já instalados por padrão.  Neste artigo vamos apenas comparar as imagens e não vamos nos preocupar muito com as aplicações que vamos rodar dentro delas.

Um dos grandes problemas de imagens full é que, por terem muitos pacotes instalados por padrão, isso significa que existem muitos pacotes com vulnerabilidades comprovadas que também vem instalados por padrão. Isto abre portas para que um ataque possa ocorrer.

O próprio Docker Hub possui uma ferramenta de análise de vulnerabilidades que pode ser utilizada diretamente do site, através da aba Tags. Como mostra a imagem a seguir.

Detecção de vulnerabilidades do Docker Hub

Como você pode ver, a própria imagem tem vulnerabilidades que você está "herdando" antes mesmo de fazer qualquer deploy do seu código.

Imagens Slim

Foto de uma etiqueta escrito "the slim" Mat Reding / Unsplash

Uma imagem slim é uma imagem um pouco menor. Toda a imagem slim só possui os pacotes necessários para que o sistema operacional funcione, é esperado que você instale todos os pacotes extras para que sua aplicação possa funcionar. Atualmente, só o Debian possui uma imagem slim.

Um dos benefícios diretos do uso de imagens slim é que, por ter menos pacotes, você tem menos dependências que podem estar sujeitas a vulnerabilidades. Veja um exemplo.‌

Vulnerabilidades de uma imagem slim

Outro ganho interessante é que, justamente por serem mais "limpas", imagens slim são muito menores em tamanho do que uma imagem full. Em casos mais extremos, uma imagem slim pode ser até 50% menor do que uma imagem full. Veja o exemplo da comparação do debian:buster com o debian:buster-slim.

Tamanho da imagem slim do Debian Buster

Agora veja o tamanho da imagem completa

Tamanho da imagem full do Debian Buster

Veja que, se pegarmos a arquitetura amd64, que é a mais comum para processadores de 64 bits, e compararmos as versões full com 48.06MB contra os 25.84MB da mesma imagem, só que na versão slim. Isso é uma redução de 47% no tamanho da imagem. Tudo isso só removendo pacotes não utilizados.

Imagens Alpine Linux

Foto dos Alpes por Benni Asal / Unsplash

As imagens Alpine Linux são o tipo de imagem mais otimizada possível. Este tipo de OS foi construído do zero para ser um sistema operacional já nativo de containers. A principal diferença entre esse tipo de imagem para uma imagem tradicional é que esse sistema não utiliza a glibc. O Alpine depende de uma outra biblioteca chamada musl libc. Além disso, por padrão, o Alpine só vem com os pacotes base instalados, ou seja, qualquer outra coisa que sua aplicação precisar, você vai ter que instalar manualmente.

Isso pode parecer um pouco complicado, mas veja como reduzimos a quantidade de vulnerabilidades só por reduzir os pacotes. Perceba que não há nenhuma vulnerabilidade crítica.

Vulnerabilidades de uma imagem Alpine

Além disso, uma imagem alpine tem menos de 6MB de tamanho total. No caso das imagens mais novas, esse tamanho pode chegar a pouco mais de 2MB. Isso é uma redução de mais 95% no tamanho geral da imagem.

Tamanho de uma imagem Alpine Linux

Os contras de se trabalhar com uma imagem Alpine é que é um sistema completamente diferente, você terá de aprender a lidar com seu package manager nativo, como ele não usa as bibliotecas padrões do Linux, provavelmente a grande maioria dos sistemas precisarão ser recompilados utilizando as ferramentas do Alpine.

Imagens Scratch

Foto de um papel preto sem nada escrito com um lápis branco em cima por Kelly Sikkema / Unsplash

Por último, vamos dar uma olhada nas imagens do tipo scratch. Quando usamos scratch como imagem base, estamos dizendo ao Docker que queremos que o próximo comando no nosso Dockerfile seja a primeira camada do nosso sistema de arquivos dentro dessa imagem.

Isso significa que não temos nenhum gerenciador de pacotes, nem mesmo pacotes. Somente um sistema de arquivos vazio. Você pode começar utilizando FROM scratch, já que este é um namespace reservado no DockerHub e não possui nenhum tipo de vulnerabilidade ou pacote instalado.

FROM scratch
ADD rootfs.tar.xz /
CMD ["bash"]

A maioria das imagens de sistemas operacionais começam deste modo, pois a maioria delas são consideradas imagens vazias. Que começam do absoluto zero.

Conclusão

Vamos explorar um pouco mais sobre imagens e colocar esses conceitos em prática nos próximos artigos escrevendo uma aplicação em Go.

Fique ligado para mais novidades! Se inscreva na newsletter para receber todo o conteúdo diretamente no seu e-mail!

]]>
<![CDATA[ Olá, Mundo! ]]> https://blog.lsantos.dev/ola-mundo/ 5f173a2ef1911484af3ab995 Qua, 22 Jul 2020 21:23:11 -0300 Olá pessoal! Este é o primeiro artigo deste blog!

Quem já me conhece e acompanha meu trabalho sabe que sou um grande fã de escrita de conteúdo escrito e já escrevo há algum tempo sobre tecnologia. Nos últimos 4 anos venho escrevendo bastante para publicações como o Medium e o Dev.to, mas chegou a hora de evoluir para uma nova fase da minha carreira como criador de conteúdo.

Neste post vou descrever tudo que me motivou a criar este blog, bem como vou descrever as tecnologias utilizadas e o que há de novo! Vamos lá!

Uma nova fase

The most powerful word in the world pops up everywhere. Ironically, this is on Sandown Pier on the Isle of Wight (UK) — a place that has not changed for 30 years.
Letreiro neon escrito "change" por Ross Findon / Unsplash

Nos últimos anos sempre pensei em criar um local próprio para compartilhar meu conteúdo, não que eu não considere as outras publicações boas o suficiente, muito pelo contrário, elas são excelentes locais para se publicar conteúdo no geral. O que me fez mudar foram dois simples fatores: controle e presença

Controle

O controle pelo simples fato de que eu gosto de ter o controle das minhas publicações para que eu possa criar integrações, modificar ou migrar o conteúdo, fazer backups seguros e manter o conteúdo de livre acesso. Além do controle do conteúdo em si, para mim é importante ter o controle sobre o meio onde eu estou publicando.

Plataformas como o Medium ou o Dev.to, por mais incríveis que sejam, ainda são limitadas em alguns casos. E você, como autor, não tem controle sobre o que você pode fazer com o sistema, nem sobre quais integrações pode criar, além disso todo o seu conteúdo está nas mãos das pessoas que controlam essas redes e, se um dia quiserem, podem simplesmente sumir da mesma forma que apareceram.

Além disso, ter um local próprio para publicar os meus conteúdos faz com que eu possa não só buscar novas formas de apresentar esse conteúdo, mas também que eu possa evoluir o site para ter novas funcionalidades bacanas e aplicar a minha marca pessoal. E ai entramos na presença.

Presença

Um dos pontos que mais me deixava incomodado era que eu tinha que mostrar uma lista de redes sociais para as pessoas quando eu queria que elas encontrassem meu conteúdo, isso dilui um pouco a sua presença e o seu impacto porque fica muito mais difícil para que as pessoas achem você na rede.

Outro ponto que me levou a criar este blog foi que eu poderia transformá-lo em um lugar "com a minha cara". Aplicar a marca que foi feita com carinho por várias pessoas que me ajudaram a transformar tudo isso que vocês estão vendo em realidade. Tendo poder sobre o front-end e podendo adicionar novas ferramentas, eu posso criar uma experiência diferente para algum tipo de post ou então até mesmo transformar o blog em um hub ou um agregador, em suma, eu posso fazer o que eu quiser!

Por isso decidi que criar um blog seria a melhor alternativa, pois assim posso centralizar o conteúdo de forma que todos os meus artigos sairão deste único ponto e posso pedir para que as pessoas me encontrem somente por aqui também.

E agora? Como vai ser?

A partir de agora, todos os conteúdos novos serão publicados primeiro neste blog. Eles também serão automaticamente replicados no meu Dev.to através do feed RSS.

Todos os conteúdos antigos continuarão existindo onde sempre estiveram. A única coisa que muda é que, a partir de agora, todos os novos conteúdos vão ser postados somente aqui no meu blog.

Criar um blog próprio também me permitiu criar uma página exclusiva para todas as pessoas que desejarem me apoiar o meu trabalho e me ajudar nesta empreitada através de doações ou contribuições.

Só artigos?

Em resumo: NÃO 😌

Como eu disse anteriormente, esta é uma nova fase de produção de conteúdos, ou seja, além do blog estarei também investindo em outros formatos de publicação de conteúdo. No momento o blog é o primeiro passo, porém já estou elaborando um canal do YouTube e também pensando em outras formas de compartilhar conteúdo de qualidade para ainda mais pessoas.

Todos os vídeos serão publicados no canal e terão um post dedicado no blog! Assim fica fácil saber de tudo o que ocorreu! Mas não é só isso. A ideia é que o blog se torne um arquivo de todo o meu conteúdo, tanto escrito quando não escrito.

Portanto, todas as minhas futuras palestras – que já podem ser encontradas no meu SpeakerDeck – ficarão também disponíveis aqui com uma pequena descrição de como ocorreram, e os detalhes sobre aquele evento em si, ou seja, todo o conteúdo que eu já participei ou vou participar ficará guardado aqui em segurança para que todos possam encontrar!

Sempre alerta!

Um dos grandes motivos que me influenciaram fortemente a criar um blog foi a possibilidade de ter uma newsletter!

Uma das coisas que eu sinto mais falta em conteúdo escrito é que não temos a possibilidade de atingir as mesmas pessoas sempre, de estar em contato constante mesmo após quem leu já ter saído do blog.

Meu objetivo com esta newsletter é enviar mensalmente (ou semanalmente, ainda não decidi) um resumo de todas as postagens que foram feitas, destacar postagens importantes, avisar quem está acompanhando sobre eventos, códigos de desconto e outras coisas legais que vão acontecer por aqui!

Então, se você está lendo isso, considere se inscrever na lista de emails e fique por dentro de todas as novidades, você também vai receber códigos de desconto e conteúdos exclusivos. Prometo que não vou lotar a sua caixa de emails!

Por baixo dos panos

Sei que provavelmente você é uma pessoa desenvolvedora e quer saber quais foram as tecnologias que eu utilizei para fazer este site não é mesmo? Vamos lá então!

A saga pelo CMS

Desde que comecei a trabalhar com tecnologia o WordPress é uma das ferramentas mais utilizadas para a criação de blogs. O problema é que ele é muito pesado e eu, pessoalmente, não gosto muito de utilizá-lo, pois já tive muitos problemas no passado.

Minha primeira opção seria criar um blog simples utilizando um gerador estático chamado Hugo, que é escrito em Golang. A ideia era simples: criar o blog, escrever o conteúdo em markdown e publicar tudo pelo GitHub Pages. Cheguei até a montar um repositório com uma pequena prova de conceito, porém infelizmente acabei não o usando justamente porque eu tinha mais planos para este blog do que simplesmente um blog. E o Hugo não é tão simples para criar algo mais complexo.

Passei para o Gatsby, que é essencialmente a mesma coisa, porém escrito em JavaScript usando React. A ideia surgiu quando vi um projeto chamado Ghost, isso resolveria todos os meus problemas!

O Ghost é um CMS Headless, isso significa que você pode plugar qualquer tipo de front-end nele e utilizar somente sua API para gerenciar e criar o conteúdo. Em suma, é como se você tivesse um WordPress sem o front-end, somente com a API aberta e o painel administrativo. Então eu teria um painel de gerenciamento de posts, login e senha, controle de acesso, a possibilidade de ter mais de uma pessoa escrevendo o mesmo artigo e muitas outras funções que qualquer CMS já tem de fábrica.

Mas o que me ganhou foi a possibilidade nativa do Ghost integrar com um sistema de envio de newsletters, que era algo que eu já queria fazer há um tempo. Comecei o desenvolvimento usando Gatsby, mas depois que analisei e estudei o tema percebi que o que o Gatsby fazia não era nada mais nada menos do que puxar as postagens do Ghost remotamente.

O Ghost já possuía um tema que vinha pré instalado, chamado Casper e um outro tema com a newsletter já implementada chamado Lyra, portanto não tinha necessidade de ter ainda outra camada só para servir o front-end. Acabei largando o Gatsby e usando apenas o Ghost por completo

A infraestrutura

O Ghost é o clássico projeto open source com suporte pago. Você pode baixar e instalar ele na sua própria máquina sem custo – que foi o que eu fiz – ou então pagar para usar a cloud deles já pré instalada.

Meu objetivo era não gastar nenhum dinheiro para manter o blog em si, então utilizei os crédito da Azure que possuo por ter sido um Microsoft MVP para criar os recursos que precisei na cloud e instalar o Ghost localmente.

Portanto, este blog está rodando exclusivamente na Azure.

O front-end

Eu não sou um front-end. Na verdade é uma das funções que menos desempenho no mundo de desenvolvimento. Então eu não tenho um vasto conhecimento em frameworks como o Vue, React, Angular ou qualquer outro. Portanto, a ideia era ser algo simples.

Peguei o tema padrão, Lyra, como um início. Fiz um fork para um novo repositório para armazenar somente o tema do blog e todas as alterações que foram feitas nele.

O Ghost trabalha com temas renderizados no lado do servidor utilizando o Handlebars como template engine. Então a partir daí foi simples, tive apenas que alterar o HTML, CSS e JavaScript das páginas para montar o meu layout em cima do layout padrão do blog. E este é o resultado.

O futuro

Enquanto esse é o primeiro de muitos artigos que vão aparecer por aqui, este não é tudo que este blog pode ser!

Para o futuro estou com a ideia de abrir este meio de conteúdo para que outras pessoas também possam escrever seus conteúdos por aqui! Assim criando um hub de ensino e colaboração! Mas isso fica para uma próxima!

Conclusão

Carta escrito "thanks" por Kelly Sikkema / Unsplash

Muito obrigado por acessar e por ler meu conteúdo, se você tiver qualquer feedback tanto sobre conteúdo quanto também sobre o site em si, por favor, entre em contato comigo em qualquer uma das minhas redes sociais (que podem ser encontradas no menu e no rodapé) ou então através do meu site. Sempre estou disponível para responder!

Se alguma coisa quebrou para você enquanto estava passeando por aqui, acesse a página de issues do blog e descreva seu problema que vou tentar corrigir o mais rápido possível.

Muito obrigado por ler e acreditar em meu conteúdo! 😍

]]>