11 settembre 2018

Anatomy of a Data Migration Manager



Italian version

How to turn an obstacle into the opportunity of building a reusable tool

We'll discuss the architecture of a Data Migration Manager built with Outsystems technology. this project was inspired by the urgent need to move data between DB in different environments. Later we'll find out that the platform can't easily help with that.
The suggested design and algorithms can be improved and some choises depended by external factors.
However we built a generic and effective tool, altough limited not efficient because of REST services tecnhology.
But Outsystems helped a lot in keeping costs down and in accelerating development.

What is Outsystem?

OutSystems is a low-code platform that lets you visually develop your entire application, easily integrate with existing systems, and add your own custom code when you need it.
This is the home page definition. We add that platform is a Paas served by AWS. This could help a legacy modernization process.
Moreover tha platform has a feedback system, code management tools, release and dependency management tools. This is why Outsystems is a strongly DevOps enabling platform, natively oriented to continuos integration and countinuos delivery.

What it's for?

Outsystems is for visual development. It lets you code very very fast, without coding. Custom .Net code can be added when needed with extensions.
It is also a complete lifecycle manager: version control, release and dependency management and even database release automation (i.e. Liquibase or DBmaestro), still a touchy point.
So, in few minutes we are able to build an application from scratch and to release it in production environment.
But what if I need to move datas between environments?
Outsystems (in cloud version) does not natively allow data migration.
In a perfect world, in a correct development process flow, we should not need this. But in real world, honestly, it may happen...

Friends don’t let friends manage servers

It's an AWS saying. Indeed we can't access the DB using administration tools: too risky. We can do it using a VPN. Outsystems will support you in doing this.
But we have two more ways:
  1. Infosistema Data Migration Manager it's a commercial solution aimed to solve this problem. It also allows to apply data transformations (ETL).
  2. Punk way do it yourself: a Data Migration Manager home made.
We choose the second, of course :)

Self producer-self consumer model

We start from two points:
  1. Outsystems environments are publicly exposed on the internet.
  2. Applications may expose and use services.
So we could have an application A in an environment calling services exposed by same application running in another environment.
Better: A dev-instance can extract its datas and pass them to the test-instance using REST services. A model that we could call self producer - self consumer.
We can work on it. If Infosistema can do it, there must be a way 😊
Design could be:
Using this approach every application must have migration logic. Its code will be replicated n times. We can do better.
We can encapsulate migration logic in a Data Migration Tool (we are going to call it GearDMT).
Outsystems, aiming to reuse, lets us to define “public” tables and import and use them inside other modules.
So every application will define “public” its tables and GearDMT  will import them as dependencies.
Pros: generic solution, migration logic isolated.
Cons: inelegant, violates several patterns and programming principles.
Design would become:
Other techincal issues:
  1. Every time we have to migrate a new table we will have to add a dependency in GearDMT and republish both applications.
  2. GearDMT needs to know the structure of the tables to migrate. So it will have to import also some system tables (Entity, Entity_Attr, …).
  3. GearDMT needs to run generic and dinamyc querie and it's impossible to write such queries using platform base tools.

It's time to get our hands dirty

We are lucky: we can integrate custom code using .Net extensions. So we can write a custom extension to exploit platform Runtime Public DB API to talk to data layer.
Pros:
  • Extension is engaged while reading and writing datas;
  • It easily can run dynamic and generic queries involving unknown tables infering data types;
  • It has low-level access to DB and to all platform's tables;
  • It introduces a decoupling;
  • It manages transactions in autonomy.
Something like this:
We will use also LifeTime Services API to get from platform informations about environments and installed applications.
REST layer is necessary to allow dialog between application instances. Furthermore it can be used to populate tables from external sources.
Of course we have to keep in mind that services must be secured: they are a doorway to our database.

The process

Once the design is defined, process is very easy. Here are main steps:
Two main steps:
  1. Verifiy if we can migrate and then load datas (CheckEnvAndLoadSourceTable)
  2. Run migration (MigrateTable)

CheckEnvAndLoadSourceTable

First detailed step:
  1. verify that GearDMT is installed also on target environment (using REST ping service)
  2. source table must be present on target environment. Source and target table structures must be identical (checkDestTable will compare hashes of json objects representing table structures)
  3. load datas from source table using readTable. This method will call low level methods exposed by .Net extension

2. migrateTable

Later in the second step we call migrateTable to run migration. It calls method postData in target environment passing table structures and loaded datas.
postData will call again the .Net extension's low level methods to write on DB.
This approach has obvious limitrations: datas are passed entirely in a single HTTP POST request. We risk timeout for big tables (think about big files in blob columns).
It's true: we can migrate row by row. But this won't help so much: we risk a huge amount of almost concurrent HTTP requests. We can fix this if we introduce a synchronized request-response management: we start a new request only if the previous one is completed. Anyway we can't have an effective transaction management: every row would be a single transaction and we can't execute a complete rollback. Not so nice.
So, all in all, first approach seems to be the lesser evil.

Id matching strategies

One of the data migration goals is the Id matching warranty: we want that record r with id x in source env has a corresponding record r’ identical to r with id x in target env. This is not trivial because Outsystems uses autoincrement for primary keys.
We can disable autoincrement, to insert datas, and enable autoincrement again but unfortunately Outsystems db user has no rights to perform such operations.
So a strategy to fix this issue could be “Insert-delete until match”
1 order records to copy by id ascending
2 write first record
3 read id assigned in target environment
4 while target id < source id
5    delete just inserted record
6    insert the record again
7 if target id > source id throw an exception and rollback
It's somehow a ragged inelegant solution, but it works.
Let's implement also a “Don’t care” strategy. It will allow to insert records ignoring primary key values. This strategy will be useful for easily testing and debugging GearDMT , and we can use it in future to feed tables from external sources.

Id resolving for static entities

In Outsystems you can create normal tables, called (Entities) and Static Entity. A Static Entity is an entity that has static data associated to it. This static data is managed at design time and can be used directly in the business logic design of the application, benefiting from strong typing. Think of static entities as enums in Java. Outsystems cares about static entities data migration while you promote an application from an environment to another.
But primary key may not be mantained.
Let's suppose we have FileType static entity in dev
At design time we realize that we won't ever need "audio" file type and we decide to delete it. The static entity will look like this:
When we'll promote to test env, records will be generated like this:
So if we are going to use GearDMT to move all other datas from dev to test, all "immagine" file type files will become “video”. Moreover inserting “video” file type files would cause a reference key violation.
The only way to fix this issue is trying to resolve the record's Id in the target environment using Label attribute.
This may be a record of File entity in development:
Let's look in target enviroment for the Id of the filetype having label = “immagine”. Result would be Id = 2. And finally we can write the record:
It's a weak strategy. But we have no other ways.

Limits and preconditions

GearDMT is a Data Migration Tool built to quickly meet our requirements at an extremely low cost. It does not pretend to be a DMM, it is limited and may be improved. It can't be considered a reference architecture.
In order to function properly some preconditions must be met:
  1. A standard primary key (just one column called Id autoincrement)
  2. If insert-delete strategy is choosed, target table must be empty and its Id counter must be reset.
Moreover here are some points of criticism due to design restrictions:
Application server bears the computational cost of whole migration. This cost may be too high and may impact on all other applications running on same server causing delays or service outages.
All datas are moved by REST services using an HTTP POST request.
Doing a POST request for each record to move could be an option. But we wouldn't be able to manage effectively the transaction: in case of exception just last record would be affected by rollback instead of the whole dataset. But first option has performance issues and is exposed to timeout errors in case of moving a huge amout of datas.
Lastly, Static Entities Id resolution, based on label value, is clearly exposed to failures. Even if it would be really weird to have in a Static Entity two or more records having same label value.

Evolutions and improvements

GearDMT development has been an opportunity to deepen Outsystems knowledge. We looked into the hood and customized the platform. We succesfully used the tool in several cases to migrate datas.
There's still scope to improve the tool:
  • We could exploit HTTP 102 Processing response to mitigate client timeout risks while moving a huge data set.
  • We could introduce an ETL module to apply data transformations. If we design it extendable by plugins we will be able to easily meet unpredictable future requirements.
  • Lastly we can call GearDMT services from any other application. In a scenario of legacy modernization, if we are quickly re-engineering an obsolete application using Outsystems, we'll be able to use GearDMT services to feed the new database from a batch or another external application.

10 settembre 2018

Anatomia di un Data Migration Manager in Outsystems

English version

 Ovvero come trasformare un limite nell’opportunità di creare un tool generico e riutilizzabile.

Descriveremo l’architettura di un possibile Data Migration Manager realizzato con la piattaforma Outsystems. Il progetto nasce dall’esigenza di spostare dati tra DB di ambienti diversi. In seguito scopriremo che la piattaforma (nella versione cloud) non consente questa operazione in modo agevole.
L'architettura che presenteremo è migliorabile, così come gli algoritmi proposti. Le scelte implementative sono state spesso condizionate da fattori esterni.
Tuttavia il tool realizzato è generico ed efficace, seppur limitato e con problemi di efficienza per via della tecnologia a servizi REST utilizzata.
Ma grazie ad Outsystems, i tempi e i costi di realizzazione sono stati minimi.

Cos’è Outsystem?

OutSystems is a low-code platform that lets you visually develop your entire application, easily integrate with existing systems, and add your own custom code when you need it.
Questa è la definizione che troviamo sulla home page. Aggiungiamo che la piattaforma è una Paas ospitata su cloud AWS. Questo può aiutare un processo di legacy modernization sia in aziende che hanno già puntato in questa direzione, che nelle realtà che ancora devono muovere i primi passi.
Inoltre la velocità di realizzazione, i sistemi di feedback integrati, gli strumenti di gestione del codice, delle release e delle dipendenze rendono questo prodotto fortemente abilitante al DevOps, orientato alla continuos integration e al countinuos delivery.

A cosa serve?

Outsystems consente di sviluppare in modo visuale, senza scrivere codice, e di farlo molto velocemente. Le applicazioni create possono essere estese con codice custom attraverso estensioni .Net.
Inoltre gestisce l’intero ciclo di vita applicativo: version control, release e dependency management e anche database release automation (cfr Liquibase o DBmaestro), tasto spesso ancora delicato.
In pochissimi minuti siamo in grado di realizzare un’applicazione da zero e di rilasciarla in ambiente di produzione.
Ma se avessi la necessità di spostare i dati da un ambiente ad un altro?
Outsystems (nella versione cloud) non consente in modo nativo la migrazione di dati tra tabelle di ambienti diversi.
In un mondo perfetto, seguendo un corretto processo di sviluppo, questo problema non si porrebbe. Ma nel mondo reale, onestamente, capita spesso…

Friends don’t let friends manage servers

Recita così un mantra AWS. E infatti, per ovvi motivi di sicurezza, non si può accedere direttamente al DB con tool di amministrazione. Sarebbe possibile solo attraverso una VPN creata con l’aiuto del supporto tecnico Outsystems.
Esistono però altre due strade:
  1. Infosistema Data Migration Manager è una soluzione commerciale che colma proprio questa lacuna e consente anche di applicare trasformazioni ai dati in transito (ETL).
  2. La strada punk che prevede il fai da te: un Data Migration Manager home made.
Noi scegliamo la seconda strada :)

Il modello self producer-self consumer

L’idea generale parte da due assunti:
  1. una infrastruttura Outsystems è composta da più ambienti, tutti pubblicamente raggiungibili attraverso internet.
  2. le applicazioni possono esporre e consumare servizi.
Quindi i servizi esposti da un’applicazione A in ambiente di test potrebbero essere consumati dalla stessa applicazione in esecuzione anche in ambiente di sviluppo.
O meglio: l’istanza di A in sviluppo potrebbe estrarre i suoi dati e passarli, attraverso un servizio REST, all’istanza in test, secondo un modello che potremmo definire self producer - self consumer.
Ci si può lavorare. D’altra parte se lo fa Infosistema una strada deve esserci 😊

L’architettura potrebbe essere la seguente:

In questo modo però ogni singola applicazione sarebbe gravata dalla logica della migrazione il cui codice andrebbe replicato n volte, una per ogni applicazione. Ovviamente si può fare di meglio.Possiamo incapsulare la logica di migrazione in un Data Migration Tool (che chiameremo GearDMT).
Outsystems, nell’ottica del riuso, consente di definire “public” le tabelle di un modulo applicativo e di importarle ed usarle in altri moduli.
Quindi ogni applicazione definirà le sue tabelle “public” e GearDMT  le importerà come dipendenze.
Pro: soluzione generica che isola la logica di migrazione.
Contro: non elegante, viola diversi pattern, linee guida e principi di programmazione.
L’architettura diventerebbe qualcosa del genere:
Ci sono anche altre implicazioni di carattere tecnico:
  1. Ogni volta che dovremo migrare una nuova tabella si dovrà aggiungere in GearDMT la dipendenza verso le nuove tabelle e ricompilare entrambe le applicazioni.
  2. GearDMT, per poter leggere la struttura della tabella da migrare, dovrà importare anche alcune tabelle di sistema (Entity, Entity_Attr, …) che mantengono proprio queste informazioni.
  3. Per leggere e scrivere su tabelle che non conosce GearDMT deve poter eseguire query generiche e dinamiche, impossibili da scrivere con gli strumenti base messi a disposizione dalla piattaforma.

Sporchiamoci le mani

Fortunatamente abbiamo la possibilità di integrare codice custom attraverso le estensioni .Net. Potremmo dunque scrivere un'estensione che sfrutti le Runtime Public DB API della piattaforma per dialogare con il data layer.
Pro:
  • L’estensione sarebbe coinvolta in fase di lettura e in fase di scrittura;
  • consentirebbe facilmente di eseguire query dinamiche e generiche su tabelle sconosciute inferendo i tipi di dato
  • dialogando col DB a basso livello avrebbe “naturalmente” accesso a tutte le tabelle della piattaforma
  • introdurrebbe un disaccoppiamento tra l’applicazione GearDMT ed il DB evitando la necessità del consumo diretto delle tabelle
  • gestirebbe autonomamente la transazione garantendo la coerenza del dato
Qualcosa del genere:
Nel diagramma si fa riferimento anche alle LifeTime Services API che serviranno per ottenere dalla piattaforma le necessarie informazioni sugli ambienti presenti e le applicazioni installate.
Lo strato REST, oltre ad essere necessario per consentire il dialogo tra istanze dell’applicazione in esecuzione su ambienti diversi, disaccoppia ulteriormente e consente possibili evoluzioni future (alimentazione delle tabelle da sorgenti esterne ad esempio)
Ovviamente i servizi devono essere protetti in modo adeguato: sono sempre una porta di ingresso verso il DB.

Il processo

Una volta definita l’architettura il processo è molto semplice, riportiamo solo i passaggi principali.
Due step fondamentali:
  1. Verifica se sono presenti le condizioni per eseguire la migrazione e carica i dati da migrare (CheckEnvAndLoadSourceTable)
  2. Esegue la migrazione (MigrateTable)

CheckEnvAndLoadSourceTable

In dettaglio il primo step:
  1. verifica che anche in ambiente di destinazione sia presente GearDMT (attraverso il servizio REST ping)
  2. verifica che in ambiente di destinazione la tabella da migrare sia presente ed abbia la stessa struttura della tabella sorgente (Il confronto viene fatto nel metodo checkDestTable confrontando gli hash degli oggetti json che rappresentano le strutture)
  3. carica il contenuto della tabella sorgente attraverso il metodo readTable. E' proprio questo metodo che chiama i metodi di basso livello messi a disposizione dall’estensione .Net

2. migrateTable

Successivamente lo step 2 chiama il metodo migrateTable che esegue la migrazione chiamando il metodo postData in ambiente di destinazione passando in input la struttura tabellare sorgente e i dati.postData, al suo interno, invocherà di nuovo i metodi di basso livello messi a disposizione dall’estensione .Net per scrivere sulla base dati.
Ora sono più evidenti i limiti di questo approccio: i dati passano tutti insieme su una chiamata POST del protocollo HTTP. Se la tabella contiene una grande quantità di dati - grandi file ad esempio - si rischia il timeout.
Migrare una riga alla volta non migliorerebbe molto la situazione: se la tabella avesse molte righe rischieremmo di avere molte chiamate HTTP pressoché concorrenti. Potremmo introdurre una gestione request-response sincronizzata: facciamo una nuova chiamata solo quando la precedente si è già conclusa. Ma non saremmo in grado di gestire in modo efficace la transazione: ogni riga sarebbe una transazione isolata e non avremmo possibilità di eseguire una rollback completa in caso di errore.
Tutto sommato il primo approccio sembra il male minore.

Strategie di Id matching

Un aspetto fondamentale della migrazione dei dati è la garanzia di Id matching: ovviamente ci si aspetta che al record r con id x in ambiente sorgente corrisponda un record r’ del tutto identico r con id x in ambiente di destinazione. Questo non è banale perché Outsystems gestisce le chiavi primarie con campi autoincrement.
Potremmo disabilitare l’autoincrement, inserire i dati e riabilitarlo ma sfortunatamente l’utenza con cui Outsystems si collega al DB non ha privilegi sufficienti per farlo.
Una strategia per risolvere il problema può essere “Insert-delete until match
Ovvero:
1 ordina i record da copiare per id crescente
2 scrivi il primo record
3 leggi id assegnato ambiente destinazione
4 fintanto che id destinazione < id sorgente
5    elimina il record appena inserito
6    inserisci di nuovo il record
7 se invece id destinazione > id sorgente lancia un’eccezione e rollback
E’ una soluzione arrangiata, non è elegante, ma funziona.
Implementiamo anche una seconda strategia “Don’t care” che consenta l’inserimento del record a prescindere dal valore della chiave. Questa strategia, oltre a consentire test e debug di GearDMT in modo più agevole, in futuro potrà essere usata anche come strategia per l’alimentazione di tabelle da sorgenti esterne.

Id resolving per le static entities

La piattaforma distingue una tabella normale (Entity) da una tabella “tipologica” (Static Entity). Le seconde, a differenza delle prime, vengono migrate automaticamente, complete di dati, in fase di rilascio.
Purtroppo però il processo di rilascio non garantisce la conservazione della chiave primaria a parità di record.
Ad esempio supponiamo di avere in ambiente di sviluppo la Static Entity TipoFile
 Durante lo sviluppo ci accorgiamo che il tipo file “audio”, inserito per errore, non ci servirà mai e decidiamo di eliminarlo. Otteniamo quindi:
Quando rilasceremo in ambiente di test, la piattaforma genererà i record in questo modo:
Quando useremo GearDMT per migrare i dati da sviluppo a test, tutti i file di tipo “immagine” diventeranno “video” e l’inserimento di quelli di tipo “video” causerebbe una reference key violation.
Per risolvere questo problema dobbiamo tentare la risoluzione dell’Id del record nell’ambiente destinazione partendo dall'attributo Label.
Consideriamo un’ipotetica riga della tabella File:
Prima di scrivere il record cerchiamo nell’ambiente di destinazione l’Id del tipoFile che abbia label = “immagine”. Il risultato sarebbe Id 2. E quindi il record da scrivere sarebbe:
E' una strategia molto debole e fallace. Ma anche in questo caso non abbiamo altre alternative.

Limiti e Assunti

GearDMT è un Data Migration Tool nato per risolvere rapidamente un’esigenza con costi minimi. Non ha pretese di essere un DMM, è limitato e migliorabile. Non può e non vuole essere un’architettura di riferimento.
Il corretto funzionamento dipende dal rispetto di alcuni assunti e condizioni preliminari:
  1. La struttura della chiave deve essere standard (un solo attributo chiave primaria chiamato Id di tipo autoincrement)
  2. Se si usa la strategia insert-delete la tabella di destinazione deve essere vuota e con indice resettato.
Inoltre ha dei punti critici legati ai vincoli architetturali:
L’application server si fa carico di tutta la migrazione. Il costo computazionale potrebbe essere troppo alto e impattare su tutte le applicazioni in esecuzione sullo stesso server causando rallentamenti o interruzioni di servizio.
I dati di una tabella viaggiano interamente all’interno di una chiamata a servizi REST. Si sfrutta il protocollo HTTP mettendo i record nel body di una POST request.
L’alternativa può essere quella di fare una chiamata per ogni record della tabella da scrivere.
Questo approccio però non consentirebbe di gestire la transazione in modo efficace: un’eccezione causerebbe la rollback dell’ultimo record inserito e non dell’intero dataset. Il primo approccio però soffre di problemi di efficienza ed è esposto a errori di timeout nel caso di trasferimenti di grandi quantità di dati.
Infine anche il meccanismo di risoluzione degli id delle Static Entities a partire dagli attributi label è evidentemente fallace e si basa sull’assunto, di buon senso, ma debole, che in una Static Entity non esistano due o più record con lo stesso valore per l’attributo label.

Evoluzioni

Lo sviluppo di GearDMT ci ha permesso di approfondire la conoscenza di Outsystems sfruttando anche le sue caratteristiche più avanzate e introducendo una forte customizzazione del prodotto e il tool è stato usato con successo in diverse occasioni.
C'è ancora spazio per evoluzioni future:
  • Si potrebbe sfruttare il codice di ritorno HTTP 102 Processing. Potrebbe limitare il rischio di timeout lato client durante un trasferimento oneroso di dati.
  • Se ci fosse la necessità di applicare trasformazioni ai dati in transito potremmo introdurre un componente di ETL. E se questo fosse estendibile attraverso plugin riusciremmo a far fronte a necessità future non immediatamente prevedibili.
  • Inoltre i servizi esposti possono essere chiamati da qualsiasi applicazione. Immaginiamo di utilizzare Outsystems in un contesto di legacy modernization per riscrivere un’applicazione obsoleta in tempi rapidi. Ovviamente non vogliamo perdere i dati pregressi. Nulla ci vieta di chiamare i servizi di GearDMT per alimentare la nuova applicazione da un batch o un'applicazione esterna.

26 settembre 2017

Scaling orizzontale di microservizi SpringBoot con kubernetes su Azure

In questo tutorial proveremo a creare un'immagine docker a partire da un microservizio SpringBoot. L'immagine sarà poi eseguita in un Azure Container Service orchestrato da Kubernetes che configureremo per fare autoscaling. Applicando un carico di stress apprezzeremo come l'orchestratore manderà in esecuzione altre istanze del microservizio per far fronte alle richieste.

Requisiti

docker, account Azure, account dockerHub, azure CLI, kubectl, immagine docker

Il servizio SpringBoot

Prima di tutto abbiamo bisogno di un microservizio da eseguire e dockerizzare. Se non ne avete uno a disposizione è possibile crearlo in pochi semplici passi utilizzando SpringBoot. Si può fare riferimento a questa guida prima di procedere ai passi successivi.
https://software.oreilly.com/ideas/microservices-for-java-developers/page/2/spring-boot-for-microservices?log-out
Possiamo seguire il tutorial fino a completare il paragrafo "Add the HTTP Endpoints" e saltare il resto. Sarà sufficiente avere un servizio esposto che risponda correttamente all'indirizzo /api/hola.
Aggiungiamo invece un nuovo entry point che ci servirà per stressare il servizio
 @RequestMapping(method = RequestMethod.GET, value = "/stress/{var}", produces = "text/plain")
 public String stress(@PathVariable("var") String var) throws UnknownHostException {

      try {
       
       fatt(Integer.parseInt(var));
              
  } catch (NumberFormatException e) {
   e.printStackTrace();
   return "ERRORE: input numerico richiesto!";
  }
      
      return "delayed for " + var + " cycles";
 }
La funzione fatt(int x) è una semplice implementazione del calcolo del fattoriale appesantita ulteriormente da una sequenza di calcoli trigonometrici.
  private int fatt(int x) {
      int i;
      int f=1;

      for(i=1; i<=x; i=i+1) {
        f=f*i;

        //incrementa il tempo di elaborazione        

        Math.tan(Math.atan( Math.tan(Math.atan( Math.tan(Math.atan( 28564 ))) )));
      }

      return f;
 }
Lo scopo è ovviamente quello di generare un carico computazionale perché sarà poi questa la metrica che utilizzeremo per configurare l'autoscaling.
Per stressare il servizio sarà sufficiente passare un input sufficientemente grande all'indirizzo /api/stress/x (ad esempio 9128811).

Creazione e distribuzione del microservizio su dockerhub

Creazione dell'immagine

Per poter creare l'immagine docker abbiamo bisogno di scrivere il dockerfile che andrà letto ed eseguito in fase di build. Supponendo di eseguire la compilazione attraverso un tool di automation mi dovrò solo preoccupare di indicare una eventuale immagine di partenza, le porte esposte e il comando di pacchettizzazione:
#FROM java:8 FROM frolvlad/alpine-oraclejdk8

#workdirWORKDIR /code

# Prepare by downloading dependencies
ADD target /code/target

EXPOSE 8080
CMD ["java", "-jar", "target/hola-springboot-1.0.jar"]
Altrimenti si può specificare puntualmente come comporre l'immagine e i comandi da eseguire per la compilazione:
#FROM java:8 FROM frolvlad/alpine-oraclejdk8

# Install mavenRUN wget http://ftp.fau.de/apache/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz 
RUN tar -zxvf apache-maven-3.3.9-bin.tar.gz 
RUN rm apache-maven-3.3.9-bin.tar.gz 
RUN mv apache-maven-3.3.9 /usr/lib/mvn
ENV M2_HOME=/usr/lib/mvn
ENV M2=$M2_HOME/bin 
ENV PATH $PATH:$M2_HOME:$M2RUN apk add --update bash && rm -rf /var/cache/apk/*


WORKDIR /code

# Prepare by downloading dependenciesADD pom.xml /code/pom.xml
RUN ["mvn", "dependency:resolve"]

# Adding source, compile and package into a fat jarADD src /code/src
RUN ["mvn", "package"]

ADD target /code/target

EXPOSE 8080

CMD ["java", "-jar", "target/hola-springboot-1.0.jar"]
Per una compilazione in locale ad esempio ho usato questo Dockerfile che aggiunge, rispetto al precedente, l'installazione di maven e la compilazione. Il risultato finale voluto è una immagine contenente una JVM e un jar autoconsistente output della compilazione.
Per generare l'immagine sarà sufficiente utilizzare il comando
 docker build .
Verifichiamo l'immagine creata
PS C:\Users\e.mattei> docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
emattei/firsttag              <none>              f0b3301b99c4        7 weeks ago         304 MB
busybox                       latest              c30178c5239f        7 weeks ago         1.11 MB
frolvlad/alpine-oraclejdk8    latest              8a75025e4288        2 months ago        170 MB
d4w/nsenter                   latest              9e4f13a0901e        10 months ago       83.8 kB
Notiamo che non ha alcun tag. Per poter caricare la nostra immagine su un registry abbiamo bisogno prima di assegnarle un tag
docker tag f0b3301b99c4 emattei/firsttag

Login su dockerhub

Accediamo a dockerhub per caricare l'immagine appena creata
docker login

Caricamento dell'immagine sul repository dockerhub

Con il comando push saremo in grado di caricare l'immagine sul registry
docker push emattei/firsttag
A questo punto l'immagine sarà disponibile su dockerhub. Per qualsiasi altro registry docker i passi sono analoghi.

3. Creazione del cluster kubernetes

A questo punto dobbiamo creare all'interno della sottoscrizione Azure tutte le risorse necessarie al deploy della nostra applicazione. Utilizzeremo il client az

Creazione del resource-group

Creiamo il resource group che accoglierà le risorse
az group create --name=myKubernetesResourceGroup --location=westeurope

Creazione del cluster kubernetes

All'interno del resource group appena creato creiamo il cluster kubernetes. Dobbiamo scegliere un dns e contestualmente richiediamo la generazione della chiave ssh
az acs create --orchestrator-type=kubernetes \
  --resource-group myKubernetesResourceGroup \
  --name=myKubernetesClusterName \
  --dns-prefix=myPrefix \
  --agent-count=2 \
  --generate-ssh-keys \
  --windows --admin-username myWindowsAdminName \
  --admin-password myWindowsAdminPassword

Scaricare la configurazione del cluster sul client kubectl

Per poter accedere al cluster appena creato attraverso il client kubectl dobbiamo scaricare la configurazione e le credenziali
az acs kubernetes get-credentials 

--resource-group=myKubernetesResourceGroup --name=myKubernetesClusterName
A questo punto sarà possibile accedere al cluster kubernetes dalla macchina locale. Per verificarlo basta eseguire un comando tipo
kubectl get nodes
che restituisce la lista dei nodi

Creazione del servizio autoscaled ed esposizione al pubblico

Creazione service e deployment

kubectl run kubeholahub --image=emattei/firsttag --requests=cpu=200m 

--expose --port=8080

service "kubeholahub" created
deployment "kubeholahub" created
La porta esposta dal servizio kubernetes dovrà coincidere con la porta esposta dall'immagine docker. In questo caso i microservizi realizzati sono esposti sulla 8080.

Definizione delle logice di autoscaling 

E' possibile definire il numero di pods minimo, massimo e le soglie di scalabilità attraverso varie metriche. In questo caso richiediamo di scalare non appena il carico sulla cpu raggiunga il 30%.
kubectl autoscale deployment kubeholahub --cpu-percent=30 --min=1 --max=10
deployment "kubeholahub" autoscaled

Esposizione pubblica del servizio

Per verificare quanto realizzato e procedere con i test di carico vogliamo poter raggiungere i microservizi dall'esterno e pertanto creiamo un bilanciatore di carico
kubectl expose deployment kubeholahub --type=LoadBalancer 

--name=kubeholahub-service

service "kubeholahub-service" exposed
Dopo alcuni di minuti Azure assegnerà al bilanciatore un indirizzo IP pubblico.
Sarà possibile verificare lo stato dell'assegnazione attraverso il comando
kubectl get svc

NAME                CLUSTER-IP  EXTERNAL-IP  PORT(S)     AGE
kubeholahub         1xx.xxx.xxx.xx1 <none>      8080/TCP    21m

kubeholahub-service 1xx.xxx.xxx.xx3   <pending>   8080:30885/TCP 20m



kubectl get svc

NAME                CLUSTER-IP  EXTERNAL-IP      PORT(S)     AGE
kubeholahub         1xx.xxx.xxx.xx1 <none>          8080/TCP    1h

kubeholahub-service 1xx.xxx.xxx.xx3   1xx.xxx.xxx.x10 8080:30885/TCP 1h

Prima di proseguire procediamo con alcune verifiche
Attraverso il comando kubectl get pods otteniamo la lista e lo stato dei pods sul cluster
kubectl get pods

NAME             READY  STATUS  RESTARTS AGE

kubeholahub-2216774994-47401/1   Running 0     1m
Attraverso il comando kubectl get deployments otteniamo la lista dei deployments
kubectl get deployments

NAME     DESIRED CURRENT UP-TO-DATE AVAILABLE AGE

kubeholahub 1    1    1      1     2m
Attraverso il comando kubectl get hpa possiamo vedere lo stato del horizontal autoscaling pod. Dovremmo ottenere qualcosa di simile a questo:
kubectl get hpa

NAME     REFERENCE        TARGET  CURRENT MINPODS MAXPODS AGE

kubeholahub Deployment/kubeholahub 50%   3%    1    10    1h
Se tutto è in ordine siamo in grado di raggiungere dall'esterno il microservizio esposto. Sarà sufficiente puntare all'IP assegnato da azure chiamando le API di riferimento:

http://1xx.xxx.xxx.xx0:8080/api/hola

Setup del monitoring e test di carico

Panello di monitoring 

Ora abbiamo un microservizio in esecuzione su un cluster kubernetes con autoscaling definito. Il passo successivo sarà quello di eseguire dei test di carico per stressare la cpu oltre la soglia definita e vedere come reagisce l'orchestratore.
Quindi abbiamo bisogno di monitorare lo stato dei pods.
Abbiamo almeno due opzioni: attraverso CLI con i comandi visti al passo precedente oppure attraverso un pannello di monitoring grafico che è sufficiente attivare con il seguente comando:
kubectl proxy
Il pannello sarà raggiungibile all'indirizzo http://localhost:8001/ui oppure http://localhost:8001/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/



In questa fase abbiamo una sola istanza ed un carico minimo sulla CPU

Setup di un test di carico

Il test di carico consisterà semplicemente in una sequenza di request verso la API appositamente esposta. Per crearlo sfrutteremo ancora docker scaricando ed eseguendo una shell linux attraverso la quale potremo realizzare ed eseguire uno script nel nostro linguaggio preferito.
In questo esempio eseguiremo ubuntu e utilizzeremo uno script bash. Abbiamo però bisogno di installare prima il comando wget, quindi ci dobbiamo preoccupare di creare un'immagine custom a partire dall'immagine ufficiale.
Creiamo un dockerfile allo scopo:
FROM ubuntu:latest
RUN  apt-get update \
  && apt-get install -y wget \
  && rm -rf /var/lib/apt/lists/*
Creiamo l'immagine e la mandiamo in esecuzione:
docker build -t ubuntuwget .docker run -it --rm ubuntuwget
al prompt sarà sufficiente definire lo script nel modo seguente:





Una volta lanciato lo script sarà possibile vedere il container scalare. Il cluster impiega tempi nell'ordine di circa un minuto per replicare la nostra immagine:







Le istanze lanciate iniziano a ridurre il carico





Aumentiamo ulteriormente il carico







In questo caso è interessante notare che il container vorrebbe mandare in esecuzione tutte le 10 istanze del microservizio che avevamo definito
kubectl autoscale deployment kubeholahub --cpu-percent=30 --min=1 

--max=10deployment "kubeholahub" autoscaled
Ma le sette già create saturano la cpu assegnata.
Terminiamo il carico e attendiamo qualche minuto per consentire lo scaling down







Il carico è tornato a 0 e quindi sono state terminate le istanze non più necessarie.

Riferimenti:

https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough
https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-windows-walkthrough

21 novembre 2014

29 ottobre 2014