Qualche tempo fa abbiamo parlato del Cloud e del suo futuro, fatto sempre meno di computer e sempre più di servizi. Abbiamo anche visto che Google detiene un ruolo di rilievo alla guida di questo trend, grazie alla sua piattaforma integrata e potente, fatta di servizi No-Ops e fully managed che realizzano una vera e propria Serverless Architecture.

In questo articolo, e nel successivo, vogliamo darvi un piccolo assaggio di questa piattaforma, seguendo la strada di un caso d’uso reale.

Ecco quindi lo scenario: come sapete Noovle è Google Service Partner (e anche uno dei migliori: se non lo sapete, #sapevatelo). In altri termini, Noovle rende semplice per le aziende integrare l’offerta di Google nella propria strategia tecnologica. Ciò significa anche che qui in Noovle gestiamo un gran numero di progetti Cloud Platform, ognuno dei quali utilizza un mix dei vari servizi di computing, storage e analisi offerti da GCP. Conservare e analizzare i dati di utilizzo di ognuno di questi progetti è importante per offrire il miglior supporto possibile ai propri clienti e per indirizzare la strategia aziendale. Nei paragrafi che seguono vedremo quindi come realizzare una pipeline di processing e analisi per questi dati; l’obiettivo è farlo utilizzando soltanto componenti Serverless.

Setup

Tutte le risorse necessarie all’implementazione della soluzione di analytics saranno contenute all’interno di un singolo progetto Cloud Platform. Per crearlo seguiamo le istruzioni della documentazione: nella home page della Cloud Console selezioniamo “Create new project”, scegliamo un nome e (opzionalmente) un ID e premiamo “Create”. La creazione del progetto richiede qualche secondo.

Dati, dati, dati

La sorgente dei dati che utilizzeremo per l’analisi è Billing Export, lo strumento di esportazione dei dati di Billing integrato in Google Cloud Platform. Questo strumento consente di creare giornalmente un report, in formato CSV o JSON, con il dettaglio dei consumi relativi al giorno precedente, e di salvarlo all’interno di un bucket Google Cloud Storage.

cloud-storage

Offre agli sviluppatori un object storage altamente durevole, affidabile e disponibile. Fornisce tre livelli di servizio per indirizzare le specifiche necessità, contenendo i costi e garantendo un accesso API semplice e consistente.

Per i nostri scopi il formato JSON è più indicato, quindi lo preferiamo rispetto al CSV. Gli elementi del tracciato contengono varie informazioni sull’utilizzo delle risorse Cloud, ed hanno più o meno questa forma:

[
{
"accountId": "012345-6789AB-CDEF12",
"lineItemId": "com.google.cloud/services/app-engine/BackendInstances",
"description": "Backend Instances",
"startTime": "2016-07-29T00:00:00-07:00",
"endTime": "2016-07-30T00:00:00-07:00",
"projectNumber": "12345678901",
"projectId": "my-test-project",
"projectName": "My Test Project",
"projectLabels": {
"customer": "acme"
},
"measurements": [
{
"measurementId": "com.google.cloud/services/app-engine/BackendInstances",
"sum": "137239.127932",
"unit": "seconds"
}
],
"cost": {
"amount": "1.723572",
"currency": "EUR"
}
},
{...},
{...},
{...}
]

Come si può notare, ogni record contiene l’indicazione del Billing Account, del progetto, della risorsa e del periodo temporale a cui si fa riferimento.

Abilitare Billing Export è molto semplice, seguendo la documentazione. È sufficiente creare un bucket Cloud Storage e riportarne il nome nella apposita interfaccia all’interno della sezione Billing unnamed della Cloud Console (il nostro bucket si chiama noovle-billing-analytics). Da questo momento in poi, ogni giorno verrà creato un file JSON contenente un report di utilizzo della giornata appena trascorsa. Opzionalmente, quest’ultima operazione può essere ripetuta per tutti gli eventuali Billing Subaccount di cui si è amministratori – in questo caso verrà creato un file per ogni account.

Due note riguardo alla creazione del bucket:

    1. gli amanti della riga di comando troveranno molto interessante Google Cloud SDK, un tool Python, disponibile per Windows, Mac e Linux, utile per amministrare e configurare i propri progetti Cloud Platform. Attraverso questo strumento, creare un bucket è semplice come scrivere
      gsutil mb -l EU gs://noovle-billing-analytics
      mb sta per “make bucket” e l’opzione -l EU specifica che il bucket dovrà essere mantenuto su datacenter europei.
    2. poiché i dati JSON saranno utilizzati soltanto da procedure batch fault-tolerant, l’availability non è il nostro requisito principale. Possiamo sacrificare quindi un po’ di disponibilità, a vantaggio del risparmio sui costi, scegliendo un bucket con Storage Class DRA (Durable Reduced Availability). Ciò ci consente di risparmiare più del 23%, mantenendo gli stessi standard di durability, rispetto ad un bucket Standard. Il comando per Google Cloud SDK diventa: gsutil mb -c dra -l EU gs://noovle-billing-analytics

Storage e analisi

Dobbiamo ora scegliere uno strumento che ci permetta di memorizzare i dati in forma strutturata e di analizzarli. In questo caso, la scelta è obbligata: quale strumento migliore di Google BigQuery, per memorizzare ed analizzare grosse moli di dati?

bigquery

Un Data Warehouse analitico fully-managed che consente analisi near-real-time su dataset massivi (fino a centinaia di Terabyte) in modo affidabile, sicuro, scalabile, economico e semplice da usare.

Depositando i dati in tabelle BigQuery avremo a disposizione un motore di interrogazione SQL estremamente potente, scalabile… e Serverless! Grazie a BigQuery, non dobbiamo più occuparci di dimensionare l’hardware necessario per storage e analisi, e non dobbiamo farci carico di mantenere il sistema sicuro, affidabile e disponibile. Google Cloud Platform pensa a tutto questo per noi.

Tutto ciò che dobbiamo fare è accedere alla Web UI di BigQuery, selezionare il nostro progetto “Noovle Billing Analytics” e creare un dataset che ospiterà le nostre tabelle.

Con Google SDK possiamo fare lo stesso attraverso la riga di comando:

bq --project_id noovle-billing-analytics mk noovle_billing_analytics

L’unica altra operazione necessaria per configurare BigQuery è la creazione di una tabella che, come vedremo in seguito, servirà al nostro sistema come template. A questo scopo apriamo il menu a discesa accanto al nome del dataset appena creato, e scegliamo “Create new table”. Poi definiamo il nome e lo schema della tabella all’interno dell’interfaccia, come mostrato in figura.

Ecco lo schema della tabella in formato JSON:

[
{
"type": "STRING",
"name": "accountId",
"mode": "NULLABLE"
},
{
"type": "STRING",
"name": "accountName",
"mode": "NULLABLE"
},
{
"type": "STRING",
"name": "lineItemId",
"mode": "NULLABLE"
},
{
"type": "TIMESTAMP",
"name": "startTime",
"mode": "NULLABLE"
},
{
"type": "TIMESTAMP",
"name": "endTime",
"mode": "NULLABLE"
},
{
"type": "INTEGER",
"name": "projectNumber",
"mode": "NULLABLE"
},
{
"type": "STRING",
"name": "projectId",
"mode": "NULLABLE"
},
{
"type": "STRING",
"name": "projectName",
"mode": "NULLABLE"
},
{
"type": "STRING",
"name": "projectLabels",
"mode": "NULLABLE"
},
{
"fields": [
{
"type": "STRING",
"name": "measurementId",
"mode": "NULLABLE"
},
{
"type": "FLOAT",
"name": "sum",
"mode": "NULLABLE"
},
{
"type": "STRING",
"name": "unit",
"mode": "NULLABLE"
}
],
"type": "RECORD",
"name": "measurements",
"mode": "REPEATED"
},
{
"fields": [
{
"type": "STRING",
"name": "creditId",
"mode": "NULLABLE"
},
{
"type": "FLOAT",
"name": "amount",
"mode": "NULLABLE"
},
{
"type": "STRING",
"name": "currency",
"mode": "NULLABLE"
}
],
"type": "RECORD",
"name": "credits",
"mode": "REPEATED"
},
{
"fields": [
{
"type": "FLOAT",
"name": "amount",
"mode": "NULLABLE"
},
{
"type": "STRING",
"name": "currency",
"mode": "NULLABLE"
}
],
"type": "RECORD",
"name": "cost",
"mode": "NULLABLE"
}
]

Vale la pena osservare che lo schema della tabella sfrutta una delle feature più interessanti di BigQuery, la quale consente di memorizzare campi innestati e ripetuti. Ne sono esempi i campi measurements, credits e cost. Grazie a questo sistema, la gestione delle relazioni uno-a-molti è ottimizzata rispetto al classico approccio basato su chiavi esterne e operazioni di JOIN.

Unire i puntini

Ottimo! Con pochi clic abbiamo inizializzato il sistema che produce i dati (Google Cloud Billing Export), abbiamo creato un’area di storage sicura, economica ed affidabile (Google Cloud Storage), e abbiamo configurato un motore di analisi estremamente performante (Google BigQuery). Ora manca soltanto qualcuno che “unisca i puntini”, prelevando i dati da Cloud Storage e spostandoli su BigQuery al momento opportuno. Seguendo un approccio tradizionale, ci viene in mente di creare una macchina virtuale dove installare e configurare qualche tool di ETL, ma una vocina ci dice che c’è una strada migliore.

Questa strada in effetti c’è, ed è costituita dalle nuovissime Google Cloud Functions (in versione Alpha al momento della redazione di questo post). Con Cloud Functions, Google ci mette a disposizione una piattaforma per l’esecuzione di micro-servizi node.js, interamente Serverless ed event-based. In altre parole, tutto ciò che dobbiamo fare è  sviluppare una funzione e scegliere l’evento che ne scatenerà l’avvio, senza preoccuparci minimamente del server o del runtime su cui avverrà l’esecuzione.

Google Cloud Functions

Una piattaforma che abilita la creazione di funzioni single-purpose ed event-based, senza dover configurare un server o un runtime.

Con la scelta di Google Cloud Functions come runtime per il nostro micro-ETL, abbiamo tutti gli elementi per realizzare finalmente un disegno della nostra architettura (con un’anticipazione sugli strumenti di analisi che descriveremo nel prossimo episodio):

Architettura, Serverless Architecture in action: Cloud Academy, Noovle

Scendiamo ora più nel dettaglio della nostra procedura di elaborazione. Tra i vari eventi (o Trigger) che possono avviare una Cloud Function, i Cloud Storage Triggers sono quelli che fanno proprio al caso nostro: si innescano quando un oggetto viene creato, modificato o cancellato all’interno di un bucket Cloud Storage.

La nostra funzione dovrà recuperare il file JSON, effettuare qualche elaborazione, e caricare ogni record su BigQuery. Poche decine di righe di codice sono sufficienti allo scopo:


'use strict';

exports.ingestbillingdata = function (context, data) {
console.log("ingestbillingdata v. 0.1.1")

// Se l'oggetto non esiste più (evento di cancellazione) esco dalla funzione
if(data.resourceState != 'exists') {
context.success();
return;
}

var projectId = "noovle-billing-analytics";
var gcloud = require('gcloud')({
projectId: projectId
});

// recupero la data di riferimento dal nome del file
var contentsDate = data.name.substring(data.name.length - 15, data.name.length - 11) + data.name.substring(data.name.length - 10, data.name.length - 8) + data.name.substring(data.name.length - 7, data.name.length - 5)
console.log("Date: " + contentsDate);

// recupero il nome del Billing Account dal nome del file
var contentsAccountName = data.name.substring(0, data.name.length - 16);
console.log("Account Name: " + contentsAccountName);

// scarico il contenuto del file da GCS e ne elaboro ogni riga
var gcs = gcloud.storage();
var billingBucket = gcs.bucket(data.bucket);
var newFile = billingBucket.file(data.name);
var contentsJSON = "";

var bigquery = gcloud.bigquery();
var billingDataset = bigquery.dataset('noovle_billing_analytics');
var billingTable = billingDataset.table('BILLING');

newFile.download(function(err,contents) {
contentsJSON = JSON.parse(contents.toString('utf-8'));

for(var i = 0; i < contentsJSON.length; i++) {
// inserisco il contenuto (arbitrario) del campo projectLabels all'interno di una stringa
// Nota: posso comunque usare le funzioni JSON di BigQuery per interrogare questi campi
contentsJSON[i].projectLabels = JSON.stringify(contentsJSON[i].projectLabels);
// aggiungo un campo con il nome del Billing Account, in modo da abilitare aggregazioni su questo valore
contentsJSON[i].accountName = contentsAccountName;
console.log(JSON.stringify(contentsJSON[i]));
billingTable.insert(contentsJSON[i], {ignoreUnknownValues:true,templateSuffix:contentsDate}, function(err, insertErrors, apiResponse) {
console.log("InsertAll:");
console.log(JSON.stringify(apiResponse));
});
}
})

context.success();
};

Alcuni highlights sul codice:

  • Tutte le Google Cloud Functions accettano due parametri in ingresso, context e data. Il primo rappresenta il contesto di esecuzione, ed è primariamente utilizzato per comunicare all’esterno l’esito della computazione (attraverso il metodo context.success). L’altro contiene i dati in ingresso alla funzione, e dipende dal trigger utilizzato per innescarla. Nel caso dei Cloud Storage Triggers, la variabile data contiene una serie di informazioni sull’oggetto aggiunto o rimosso (ad esempio il suo nome: data.name).
  • La nostra funzione utilizza una libreria esterna (google-cloud) per l’interazione con le API Google Cloud. Per includere correttamente la libreria nel progetto, ci serviamo del file package.json, di uso comune nello sviluppo con node.js: 

{
"dependencies": {
"google-cloud": "latest"
}
}

  • Il caricamento dei dati su BigQuery avviene per singolo record, attraverso le Streaming API: ciò ci consente di effettuare piccole trasformazioni prima di effettuare l’ingestion – per esempio, l’aggiunta del campo accountName. Tale scelta è inoltre resa necessaria dalla mancata corrispondenza del formato del file in ingresso con la specifica NEWLINE_DELIMITED_JSON richiesta da BigQuery.
  • Ogni record viene caricato su una tabella corrispondente alla data di riferimento. Le tabelle sono create automaticamente a partire dalla tabella “template” BILLING creata precedentemente, e i loro nomi presentano il formato BILLINGyyyyMMdd (determinato dall’opzione templateSuffix all’interno del codice). L’utilizzo di questa nomenclatura abilita il meccanismo dei Table Wildcards, utile per indirizzare con una singola query periodi temporali arbitrariamente lunghi.

Nota: il codice è pubblicamente disponibile in un repository GIT all’indirizzo https://bitbucket.org/global-base/noovle-billing-analytics-cloud-functions

Il deploy della funzione avviene attraverso il solito tool Google Cloud SDK. Per eseguire il comando di deploy occorre posizionarsi all’interno della cartella che contiene il file index.js con il codice. Inoltre, occorre aver preventivamente installato le estensioni alpha per il comando gcloud:

gcloud components install alpha
gcloud alpha functions deploy ingestbillingdata --bucket noovle-billing-analytics-gcf --trigger-gs-uri noovle-billing-analytics

Il bucket specificato nell’opzione –bucket sarà usato come area di staging per il deploy della funzione, mentre l’opzione –trigger-gs-uri specifica il nome del bucket che sarà monitorato alla ricerca di aggiornamenti.

Al termine del deploy, la funzione sarà immediatamente operativa, e verrà invocata ad ogni caricamento o cancellazione di un oggetto nel bucket noovle-billing-analytics.

Interrogare i dati

Evviva! Abbiamo configurato un sistema di ETL e analisi perfettamente funzionante, altamente affidabile ed estremamente scalabile, senza perdere tempo con installazioni di software di terze parti, codice boilerplate o complesse configurazioni di ambiente. Tutto ciò che è stato necessario è l’attivazione e l’integrazione dei servizi di nostro interesse.

Ora è semplicissimo, attraverso l’interfaccia di BigQuery, effettuare una prima analisi esplorativa dei dati. La seguente query, ad esempio, calcola il costo complessivo per ogni Billing Account, su un intervallo temporale che va dall’inizio dell’anno corrente fino alla data odierna:
SELECT
accountName,
SUM(cost.amount)
FROM TABLE_DATE_RANGE(noovle_billing_analytics.BILLING,TIMESTAMP(UTC_USEC_TO_YEAR(CURRENT_TIMESTAMP())),CURRENT_TIMESTAMP())
GROUP BY
accountName

Quando sotto al cofano c’è un motore come BigQuery, possiamo sbizzarrirci con analisi estremamente ricche e complesse, grazie ad integrazioni con altri strumenti come BIME Analytics e Google Cloud Datalab; proprio questi strumenti saranno oggetto del prossimo post… Stay tuned!