Beliebte Suchanfragen
//

Datenanalyse auf die schnelle Art – mit Amazon Athena und GitLab

21.3.2023 | 16 Minuten Lesezeit

Wenn wir Erkenntnisse aus großen Datenmengen gewinnen wollen, bieten uns Cloud Service Provider inzwischen Lösungen an, dank derer wir uns kein Data Warehouse oder Hadoop-Cluster mehr in den Keller stellen müssen. AWS hat mit Athena, RedShift und EMR mehrere Big Data Services im Portfolio, die uns die Datenanalyse ermöglichen.

Während Redshift (Data Warehouse as a Service) und EMR (Data Processing Engine as a Service) ziemlich umfassende Lösungen mit großem Featureumfang und Einsatzbereich (und ggf. erheblichem Konfigurations- und Kostenaufwand) sind, hat Athena als Query Service einen engeren Einsatzrahmen: Es ermöglicht, SQL Queries direkt gegen unstrukturierte, semi-strukturierte oder strukturierte Datenbestände auszuführen, während diese in S3 gespeichert sind. Gesonderte Ladeoperationen sind nicht nötig und der Service funktioniert ohne jegliche provisionierte Infrastruktur oder Konfiguration.

Damit ist Athena gut geeignet, um schnell und kostengünstig große Datenbestände mit klassischen Mitteln zu analysieren, ohne sich Gedanken über Konfiguration, Provisionierung, oder hohe Grundkosten machen zu müssen (Wer an einer detaillierteren Abgrenzung der Big Data Services in AWS interessiert ist: AWS stellt dafür eine Entscheidungshilfe zur Verfügung).

Klingt alles gut, aber wie lässt sich Athena in einem realen Szenario sinnvoll einsetzen? In diesem Blogpost demonstriere ich das anhand eines konkreten Beispiels.

TL;DR

Wenig Zeit? Hier gibt es die ultrakurze Zusammenfassung:

  • Amazon Athena ermöglicht uns, unstrukturierte, semi-strukturierte und strukturierte Datenbestände mit SQL an Ort und Stelle (in S3) zu analysieren.
  • Falls wir eine GitLab-Instanz zur Verfügung haben, können wir uns mit Athena, Lambda und GitLab in sehr kurzer Zeit eine Data Pipeline bauen, die kontinuierlich automatisierte Reports generiert und diese auf einer statischen Website zur Verfügung stellt.
  • Die Lösung eignet sich gut für kurzfristiges Enablement, wenn wir heterogene Datenbestände über System- oder Unternehmensgrenzen hinweg vereinen wollen, ohne Infrastruktur oder Konfiguration managen zu müssen. Wenn wir flexibler und langfristiger Datenauswertung betreiben wollen, sind RedShift oder EMR ggf. besser geeignet.

Sämtlichen Beispielcode zum Nachbauen und Ausprobieren findet ihr auf GitHub.

Eine beispielhafte Problemstellung 📚

Bei AmazingBooks, Inc. knallen die Korken: Kürzlich konnte die Fusion mit MediocreBooks, Inc. unter Dach und Fach gebracht werden. Die Kund*innen können sich schon bald über ein noch breiteres Sortiment interessanter Bücher freuen.

So froh man auf C-Level über die Fusion ist, so gestresst sind die Produkt-Teams: In nur sechs Wochen soll der neue, zusammengeführte Online-Store launchen, und die beliebtesten Bücher beider Händler sollen in einer besonderen Marketing-Aktion im neuen Online-Store besonders hervorgehoben angeboten werden. Das Problem: Die Store-Systeme beider Unternehmen arbeiten völlig unterschiedlich, speichern Daten unterschiedlich ab, und die IT-Infrastruktur beider Unternehmen ist noch nicht zusammengeführt. Das Marketing-Team ist momentan im Blindflug unterwegs und der Launch steht vor der Tür.

Wie finden wir nun möglichst schnell die beliebtesten Bücher beider Unternehmen heraus, um den Launch zu retten? Wie halten wir die Lösung möglichst flexibel für Änderungen und automatisieren sie weitestgehend, ohne ein Vermögen dafür auszugeben?

Rahmenbedingungen und Zielsetzung

Leider verfügt AmazingBooks über kein Data Warehouse, das uns das Leben erleichtern könnte (nur über eine Menge schlecht gepflegter Datenbanken). Dazu kommt, dass wir keinen direkten Zugriff auf die Datenbanken von MediocreBooks besitzen.

Nach einiger Recherche kommen wir zu folgenden Rahmenbedingungen:

  • AmazingBooks kann uns Transaktionsdaten von Buchverkäufen im JSON-Format zur Verfügung stellen.
  • MediocreBooks kann uns einen Datenbankexport mit aggregierten Buchverkaufsdaten im JSON-Format zur Verfügung stellen.
  • Wir erhalten Zugriff auf eine GitLab-Instanz und einen AWS-Account von AmazingBooks, um eine Lösung auf die Beine zu stellen.

Unserem Kunden AmazingBooks ist folgendes wichtig:

  • Die Lösung sollte mit minimaler Vorbereitungszeit nutzbar sein.
  • Die Ergebnisdaten sollen leicht aufindbar und verwendbar für Menschen ohne IT-Expertenwissen sein.
  • Die Ergebnisdaten sollen automatisiert aktuell gehalten werden.
  • Die Lösung sollte erweiterbar und flexibel für weitere Abfragen sein.

Lösungsskizze

Um von den halbwegs strukturierten JSON-Files aus den Quellsystemen von AmazingBooks und MediocreBooks zu einfach nutzbaren Reports zu kommen, bauen wir uns eine Data Pipeline auf, die grob aus den folgenden Schritten besteht:

Lösungsskizze der Data Pipeline

Als Herzstück unserer Data Pipeline agiert Amazon Athena, das uns ermöglicht, die Datenbestände über SQL zusammenzuführen und zu aggregieren. Gesteuert wird das ganze mithilfe einer Gitlab CI/CD-Pipeline. GitLab in einem solchen Szenario einzusetzen mag für manche nach Zweckentfremdung klingen, hat aber pragmatische Gründe:

  • Es wird durch unseren Kunden bereits verwendet und steht uns für die Lösungskonzeption zur Verfügung.
  • Es verschafft uns eine Menge Flexibilität: Jedes erdenkliche Tool, das wir brauchen (Export-Tool für Datenbanken, Linux Command Line Tool für JSON-Manipulation, AWS CLI) können wir uns über Docker Container in die Pipeline einbinden.

Wir unterteilen die Pipeline grob in 3 Schritte:

  • Schritt 1 – Vor- und Aufbereiten: Bringt die JSON-Rohdaten für die Verarbeitung ins gewünschte Format und kopiert diese an die richtige Stelle. Zur Vereinfachung gehen wir in unserem Beispiel davon aus, dass AmazingBooks und MediocreBooks die Export-Daten für das jeweilige System bereits anliefern (die Exporte könnten wir ggf. über GitLab aber auch selbst triggern).
  • Schritt 2 – Verarbeitung: Führt die Daten beider Quellen zusammen und aggregiert diese im gewünschten Format (Summe aller Verkäufe je Buch auf Monatsbasis). Wir verwenden AWS Lambda, um eine Query gegen eine von uns erstellte Athena-View zu triggern. Das Resultat der Abfrage landet wieder in S3.
  • Schritt 3 – Bereitstellung: Stellt das Ergebnis der Verarbeitung den Kolleg*innen aus der Marketingabteilung zur Verfügung. Wir verwenden GitLab Pages und MkDocs, um das aktuellste Query-Resultat auf einer statischen Website zur Verfügung zu stellen.

Realitätscheck: Ist das nicht ein Bißchen hacky? Warum nicht RedShift?

Heterogene Daten aus verschiedenen Quellen integrieren zu müssen klingt erstmal nach einem guten Fit für ein Data Warehouse, und RedShift bekommt dies in der Tat mit einer besseren Performance und Flexibilität als Athena hin. Unsere Wahl fällt auf Athena, weil wir mit minimalem Overhead sofort starten können, um eine klar umrissene Problemstellung in der kürzestmöglichen Zeit zu lösen. Hierfür ist Athena gut geeignet.

Ginge es um den Aufbau einer Plattform zur kontinuierlichen Auswertung von Verkaufstransaktionen, würde unsere Lösung definitiv anders aussehen. Themen wie Reproduzier- und Wartbarkeit würden zudem eine wichtigere Rolle spielen.

Auf die Frage, in welchen Szenarien Athena gegenüber RedShift und EMR Vorteile bringt, geht AWS in seinen Athena FAQs ebenfalls ein.

Die Lösung im Detail

Bevor es los geht: Ein Wort zu IAM

Bevor wir starten können sei erwähnt, dass wir entsprechende IAM User bzw. Execution Roles und Policies benötigen, die den Zugriff von GitLab auf AWS (S3; Lambda) und den Zugriff innerhalb von AWS ermöglichen (Lambda-Funktion -> Athena; Lambda-Funktion -> S3). Aus Platzgründen gehe gehe ich auf diesen Punkt im weiteren Verlauf nicht im Detail ein.

Schritt 1: Quelldaten Vor- und Aufbereiten

Um unser Beispiel simpel zu halten, bauen wir uns hier eine kleine Abkürzung ein: AmazingBooks und MediocreBooks liefern uns die Daten zu den Buchverkäufen jede Nacht automatisiert in S3 an. Damit müssen wir uns nicht um die Datenexporte kümmern.

AmazingBooks kann in der Kürze der Zeit Transaktionsdaten aus ihren Buchverkäufen anliefern, die im folgenden Format bei uns ankommen:

1[
2  {
3    "product_id": "63e6accbe23e65a8fc5e439c",
4    "transaction_id": "4f0b6696-3ba1-46b9-ca76-6e2a1de68797",
5    "transaction_date": "2023-03-11T09:49:16",
6    "qty": 1,
7    "product_info": {
8      "type": "book",
9      "title": "The DevOps Handbook, 2nd Edition",
10      "isbn": "9781950508402"
11    }
12  }
13]

MediocreBooks speichert Transaktionsdaten leider nicht dauerhaft ab, sondern kann lediglich die einzelnen Buchverkäufe pro Geschäftstag zur Verfügung stellen. Die Daten dazu werden im folgenden Format angeliefert:

1[
2  {
3    "transaction_id": "c6d909cb-bbfe-47ac-ad26-bbc2489bf1c8",
4    "business_day": "2023-03-11",
5    "book_sales": [
6      {
7        "qty": 2,
8        "isbn": "9780062316097"
9      },
10      {
11        "qty": 1,
12        "isbn": "9781950508402"
13      }
14    ]
15  }
16]

Beide Datensätze bekommen wir als Pretty Printed JSONs angeliefert. Das ist für Athena problematisch, da die Deserializer die Daten im JSON Lines Format erwarten – darum müssen wir uns im Zuge dieses Schritts kümmern.

Wir beginnen mit der Implementierung einer GitLab Pipeline, die unsere Verarbeitungslogik kapselt. Zur Ausführung des Step-Scripts verwenden wir das minimalistische Alpine Linux Docker Image, in das wir uns die AWS CLI und jq installieren. Nachdem wir uns die Datenexports von AmazingBooks (in der Pipeline bezeichnet als "system a") und MediocreBooks (bezeichnet als "system b") via s3 cp herunter geladen haben, bringen wir die JSON-Files mithilfe von jq ins JSON Lines-Format und stellen sie anschließend in einem entsprechenden Input-Verzeichnis für Athena zur Verfügung.

Da die Schritte für beide Quellsysteme identisch sind, können wir uns die Vererbungsmöglichkeiten von GitLab (über extends:) zunutze machen und einige Zeilen YAML sparen:

1.prepare-book-sales-data:
2  stage: prepare-source-data
3  before_script:
4    - apk update && apk add --no-cache jq && apk add --no-cache aws-cli
5  variables:
6    AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY
7    AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY
8  script:
9    - aws s3 cp s3://booksales-merge-athena/data-delivery/$RAW_DATA_FILE_NAME $RAW_DATA_FILE_NAME
10    - cat $RAW_DATA_FILE_NAME | jq -c '.[]' > $RAW_DATA_FILE_NAME-jsonlines.json
11    - aws s3 cp $RAW_DATA_FILE_NAME-jsonlines.json s3://booksales-merge-athena/$SOURCE_SYSTEM/
12
13prepare-book-sales-data-system-a:
14  extends: .prepare-book-sales-data
15  variables:
16    SOURCE_SYSTEM: system-a
17    RAW_DATA_FILE_NAME: book-sales-system-a.json
18
19prepare-book-sales-data-system-b:
20  extends: .prepare-book-sales-data
21  variables:
22    SOURCE_SYSTEM: system-b
23    RAW_DATA_FILE_NAME: book-sales-system-b.json

Wer aufgepasst hat wird fest stellen, dass wir unserem Pipeline Step die Environment-Variablen AWS_ACCESS_KEY_ID und AWS_SECRET_ACCESS_KEY übergeben. Diese korrespondieren mit einem entsprechenden AWS User, den wir für die Ausführung der Pipeline anlegen müssen. Dieser benötigt Zugriffsrechte auf S3 sowie auf Lambda.

Schritt 2: Daten zusammenführen und aggregieren

Wir haben unsere Quelldaten ins gewünschten Format konvertiert und in den beiden Verzeichnissen s3://booksales-merge-athena/system-a und s3://booksales-merge-athena/system-b abgelegt – weiter geht's in der AWS Console für Athena.

Damit wir die Daten via SQL abfragen können, schreiben wir zunächst DDL-Statements für eine entsprechende Datenbank sowie für zwei Tabellen, welche die unterschiedlichen Datenstrukturen der JSON-Dateien abbilden. Hier wird eine der Stärken von Athena bereits deutlich: Wer mit SQL und relationalen Datenbanken vertraut ist, findet sich schnell zurecht.

Was wir dennoch im Hinterkopf behalten sollten: Die Tabellen, die wir nachfolgend anlegen, fungieren lediglich als Definitionen, wie die Daten zum Abfragezeitpunkt zu deserialisieren sind. Die von uns angelegte Datenbank dient lediglich der logischen Zuordnung der Tabellen und hat darüber hinaus keine Bedeutung. Dies wirkt sich auf das Verhalten im Fehlerfall aus: Oft bemerken wir erst zum Abfragezeitpunkt, dass die definierte Tabellenstruktur nicht zu den Daten passt, oder dass sich nicht wohlgeformte Datensätze in unserem Export befinden. Das Datenbank-Feeling kann also auch trügerisch sein: Unsere Tabellen sind lediglich eine Abstraktionsschicht, die auf unseren JSON-Rohdaten operiert. Hier hilft im Zweifelsfall nur: Rumprobieren, bis es passt.

Beginnen wir mit der Erzeugung der Datenbank:

1CREATE DATABASE IF NOT EXISTS booksales
2  COMMENT 'Aggregates book sales data from AmazingBooks and MediocreBooks systems';

Weiter geht's mit der Tabelle für die Daten von AmazingBooks – diese bezeichnen wir nachfolgend als "system a":

1CREATE EXTERNAL TABLE booksales.system_a(
2transaction_date string,
3qty bigint,
4product_info struct<
5    type: string,
6    isbn: string,
7    title: string>
8)
9ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
10LOCATION 's3://booksales-merge-athena/system-a/'

Mit Ausnahme der Angaben zum Serializer/Deserializer sowie des Speicherorts der Daten liest sich das ganze wie eine ziemlich gewöhnliche Tabellendefinition. Falls wir uns nicht wohl damit fühlen, die Tabellen selbst zu definieren, könnten wir das Ganze auch mithilfe eines Glue-Crawlers automatisiert tun lassen.

Es geht weiter mit der Tabelle für die nach Geschäftstag zusammengefassten Verkaufsdaten von MediocreBooks, welche wir nachfolgend als "system b" bezeichnen:

1CREATE EXTERNAL TABLE booksales.system_b(
2business_day string,
3book_sales ARRAY<
4    struct<
5        qty: bigint,
6        isbn: string
7        >
8>
9)
10ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
11LOCATION 's3://booksales-merge-athena/system-b/'

Auch hier nichts Besonderes: Die Tabellendefinition richtet sich sehr stark nach dem Format, in dem die JSON-Rohdaten angeliefert werden. Damit ist unsere Vorbereitung beendet und wir können uns an die SQL Queries machen.

Nach Ausführung der DDL-Statements finden wir folgende Tabellendefinitionen in der Athena-Konsole vor (auf den ebenfalls aufgelisteten View gehen wir im nächsten Abschnitt ein):

Auflistung der Athena Tabellen und Views in der Athena Konsole

Damit haben wir die Vorbereitung abgeschlossen und können beginnen, die Datenbestände abzufragen.

It's query time 🔍

AmazingBooks hat uns die Liste aller Verkaufstransaktionen zur Verfügung gestellt. Damit wir die Bücher nach ISBN und Monat aggregieren können, müssen wir eine Filterung nach Produkttyp 'book' vornehmen und ein wenig mit dem Transaktionsdatum herumspielen, um zu einer monatsweise aggregierten Liste zu kommen:

1-- query the AmazingBooks dataset
2SELECT
3   "date_format"(CAST("date_parse"(transaction_date, '%Y-%m-%dT%H:%i:%s') AS date), '%Y-%m') month, 
4   product_info.isbn, 
5   "sum"(qty) number_sold
6FROM
7   system_a
8WHERE (product_info.type = 'book')
9GROUP BY 
10   product_info.isbn, 
11   "date_format"(CAST("date_parse"(transaction_date, '%Y-%m-%dT%H:%i:%s') AS date), '%Y-%m'))

Für die Zusammenführung mit dem Datenexport von MediocreBooks können wir auf die ISBN als gemeinsamen, eindeutigen Identifier eines Buches zurückgreifen.

Ein Problem müssen wir allerdings noch lösen: Während im Datenexport von AmazingBooks ein JSON-Datensatz genau eine Verkaufstransaktion zu genau einem Buch repräsentiert, fasst MediocreBooks alle Verkäufe eines Tages in einem Array zusammen: Ein JSON-Datensatz enthält also diverse Verkäufe zu verschiedenen Büchern. Wie vereinen wir das?

Ein Ansatz ist, das Array flachzuklopfen. Hierfür bietet sich die CROSS JOIN -Operation in gemeinsamer Verwendung mit UNNEST an. Aus einer Zeile pro Verkaufstag mit allen Verkaufstransaktionen erhalten wir n Zeilen – jeder Verkaufstag erscheint nun einmal je Verkaufstransaktion. Ein Beispiel:

  • Datensatz vor der Operation (1 Zeile, 2 Verkaufstransaktionen):
    • 2022-01-03, [{qty=9, isbn=9780062316097}, {qty=6, isbn=9781942788331}]
  • Datensatz nach der Anwendung von CROSS JOIN UNNEST (2 Zeilen, eine Verkaufstransaktion je Zeile):
    • 2022-01-03, {qty=9, isbn=9780062316097}
    • 2022-01-03, {qty=6, isbn= 9781942788331}

Eingebettet in die Query für den MediocreBooks-Export gelangen wir zu folgender Abfrage:

1-- query the MediocreBooks dataset
2SELECT
3"date_format"("date_parse"(business_day, '%Y-%m-%d'), '%Y-%m') month, 
4book_sale.isbn, 
5"sum"(book_sale.qty) number_sold
6FROM
7(
8    system_b CROSS JOIN UNNEST(book_sales) t (book_sale)
9)
10GROUP BY 
11"date_format"("date_parse"(business_day, '%Y-%m-%d'), '%Y-%m'), 
12book_sale.isbn

Zum Zusammenführen beider Exporte fehlt nun noch eine Query, die beide Abfragen vereint und die Ergebnisse aufsummiert. Zur Vereinfachung künftiger Abfragen kapseln wir die Logik in einer View:

1-- query both datasets and aggregate the results
2CREATE OR REPLACE VIEW "book_sales_total_by_month" AS 
3SELECT
4  month
5, isbn
6, "sum"(number_sold) number_sold_total
7FROM
8  (
9    (
10      -- AmazingBooks
11      SELECT
12        "date_format"(CAST("date_parse"(transaction_date, '%Y-%m-%dT%H:%i:%s') AS date), '%Y-%m') month
13      , product_info.isbn
14      , "sum"(qty) number_sold
15      FROM
16        system_a
17      WHERE (product_info.type = 'book')
18      GROUP BY product_info.isbn, "date_format"(CAST("date_parse"(transaction_date, '%Y-%m-%dT%H:%i:%s') AS date), '%Y-%m')
19    ) UNION ALL (
20      -- MediocreBooks
21      SELECT
22        "date_format"("date_parse"(business_day, '%Y-%m-%d'), '%Y-%m') month
23      , book_sale.isbn
24      , "sum"(book_sale.qty) number_sold
25      FROM
26        (system_b
27      CROSS JOIN UNNEST(book_sales) t (book_sale))
28      GROUP BY "date_format"("date_parse"(business_day, '%Y-%m-%d'), '%Y-%m'), book_sale.isbn
29   ) ) 
30GROUP BY month, isbn
31ORDER BY 1 DESC

Geschafft – mit einem simplen SELECT * FROM "booksales"."book_sales_total_by_month" haben wir nun die Gesamtzahl aller Buchverkäufe aus beiden Systemen. Führen wir die Query in der Athena Konsole aus, erhalten wir folgendes Resultat:

Athena Console mit dem Query-Resultat nach der Abfrage des erzeugten Views

Schritt 3 – Ergebnisse bereitstellen

Wie schaffen wir es nun, dass der Report auf Tagesbasis neu generiert wird und die Kolleg*innen der Marketingabteilung möglichst einfach Zugriff auf die Daten erhalten?

Amazon bietet mit QuickSight ein Tool an, das Athena als Datenquelle verwenden und darauf aufbauend Reports bereitstellen kann – das hat allerdings seinen Preis, wenn wir den Personenkreis nicht stark begrenzen wollen, denn QuickSight wird je User bezahlt.

Hinzu kommt, dass wir die vielen Features wie Dashboards und Visualisierungen der Daten für diesen Use Case gar nicht benötigen. Es ist für unser Beispielszenario komplett ausreichend, den Kolleg*innen die Ergebnisdaten als CSV-File zur Verfügung zu stellen, damit diese die Daten in Excel, Numbers etc. importieren können.

Das trifft sich gut, denn Athena speichert die Ergebnismengen sämtlicher Queries standardmäßig im CSV-Format in S3. Da dies bereits ausreichend ist, fehlt nur eine komfortable Bereitstellungsmöglichkeit.

Hier kommt GitLab wieder ins Spiel. Mit GitLab Pages können wir auf einfachem Weg eine statische Website direkt in unserer GitLab-Instanz hosten. Da wir kein HTML oder CSS selbst schreiben wollen, verwenden wir zur Generierung der Website MkDocs. Zum Triggern unserer Athena-Query verwenden wir den von AWS empfohlenen Weg über eine Lambda-Funktion.

Beginnen wir mit dem Aufruf der Lambda-Funktion aus unserer GitLab-Pipeline. Wir verwenden ähnlich wie bei der Aufbereitung der Daten die AWS CLI:

1trigger-athena-query:
2  image:
3    name: amazon/aws-cli
4    entrypoint: [ "" ]
5  variables:
6    AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY
7    AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY
8  stage: trigger-query
9  dependencies:
10    - prepare-book-sales-data-system-a
11    - prepare-book-sales-data-system-b
12  script:
13    - |
14      aws lambda invoke --function-name arn:aws:lambda:eu-central-1:123456789123:function:triggerBookSaleTotalsReport  --invocation-type RequestResponse --cli-binary-format raw-in-base64-out --payload '{ "queryBy": "month" }' lambda-response.json
15  artifacts:
16    paths:
17      - lambda-response.json

Wir rufen die Lambda-Funktion über dessen ARN auf. Zusätzlich übergeben wir eine Payload, um unterschiedliche Reports triggern zu können. Um für die Generierung der statischen Website das richtige Query-Result zu erwischen, speichern wir uns die Antwort der AWS-CLI. Diese werden wir im nächsten Schritt benötigen.

Die implementierte Lambda-Funktion orientiert sich größtenteils an der von AWS empfohlenen Weise zum Triggern von Athena-Queries, sie akzeptiert zusätzlich die beschriebene Payload, um verschiedene Reports triggern zu können:

1import boto3
2
3database = 'booksales'
4query_totals_by_month = 'SELECT * FROM "booksales"."book_sales_total_by_month"'
5query_totals_by_year = 'SELECT * FROM "booksales"."book_sales_total_by_year"'
6output_bucket='s3://booksales-merge-athena'
7output_dir='query-results/lambda-triggered'
8
9def lambda_handler(event, context):
10    client = boto3.client('athena')
11
12    # Specify the kind of query aggregation level - either by month or by year
13    query_by = event['queryBy']
14    query_string = ""
15    if query_by == 'month':
16        query_string = query_totals_by_month
17    elif query_by == 'year':
18        query_string = query_totals_by_year
19    else:
20        raise ValueError(f"The specified aggregation level '{query_by}' is not recognized. Please specify a valid aggregation level ('month'; 'year').")
21        
22    # Input is ok, fire off the Athena query
23    response = client.start_query_execution(
24        QueryString=query_string,
25        QueryExecutionContext={
26            'Database': database
27        },
28        ResultConfiguration={
29            'OutputLocation': "{}/{}".format(output_bucket, output_dir)
30        }
31    )
32
33    return response

Ist die Query erst einmal getriggert, müssen wir nur noch das Ergebnis einsammeln. Um herauszufinden, welche CSV-Datei das Ergebis zu unserer Abfrage enthält, extrahieren wir die Query Execution ID aus dem vorhergegangenen Lambda-Aufruf und exportieren die Ergebnisdatei als Build-Artefakt, damit wir es auf der statischen Website bereitstellen können.

1collect-query-result:
2  variables:
3    AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY
4    AWS_SECRET_ACCESS_KEY: $AWS_SECRET_ACCESS_KEY
5  stage: collect-results
6  dependencies:
7    - trigger-athena-query
8  before_script:
9    - apk update && apk add --no-cache jq && apk add --no-cache aws-cli
10  script:
11    - ATHENA_QUERY_EXEC_ID=$(cat lambda-response.json|jq -r '.QueryExecutionId')
12    - aws s3 cp s3://booksales-merge-athena/query-results/lambda-triggered/$ATHENA_QUERY_EXEC_ID.csv book-sales-by-month.csv
13  artifacts:
14    paths:
15      - book-sales-by-month.csv

Jetzt müssen wir lediglich eine minimale Website bauen. Durch die Verwendung von MkDocs reichen dafür ein wenig Markdown und ein paar Zeilen GitLab YAML:

1generate-static-page:
2  image: python:3.8-buster
3  stage: deliver-results
4  rules:
5    - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH
6  before_script:
7    - pip install -r requirements.txt
8  script: # Place build artifacts in the respective subdirectories after the static pages have been generated
9    - mkdocs build
10    - cp book-sales-by-month.csv public/book-sales-by-month/
11  artifacts:
12    paths:
13      - public

Die eigentliche MkDocs-Konfiguration (in mkdocs.yaml) ist ebenfalls sehr minimal gehalten:

1site_name: AmazingBooks Inc. Book Sales Reporting
2site_dir: public
3
4theme:
5  name: material
6  palette:
7    primary: teal

Damit generieren wir mit jedem Durchlauf der Pipeline eine Website mit dem aktuellsten Query-Ergebnis, das unsere Kolleg*innen aus der Marketingabteilung herunterladen können:

Statische Webseite mit dem Athena-Resultat

Die komplette Data Pipeline im Überblick

Unsere fertige Data Pipeline besteht aus fünf Schritten. Mit Abschluss der Pipeline haben wir das aktuellste Query-Ergebnis eingesammelt und via GitLab Pages zur Verfügung gestellt.

Illustration der ausgeführten GitLab Pipeline, nachdem alle Steps durchlaufen wurden

Unter der Prämisse, dass die Quelldaten regelmäßig aktualisiert angeliefert werden, könnten wir die Seite täglich neu erzeugen lassen. Alles, was wir dafür brauchen, ist ein Pipeline Schedule.

Damit sind wir fertig! Die Kolleg*innen aus dem Marketing können endlich starten, den Launch vorzubereiten.

Fazit: Sollte man das so bauen?

Bewerten wir die Lösung streng nach den Kriterien unseres Use Cases – kurzfristiges Enablement mit minimalem Overhead – sind wir ganz gut unterwegs. Durch den Einsatz serverloser Cloud-Services zusammen mit der bestehenden GitLab-Infrastruktur können wir eine Menge Geschwindigkeit und Flexibilität gewinnen. Da wir auf QuickSight verzichten und Athena rein nach gescannter Datenmenge gezahlt wird, brauchen wir uns zudem um die Betriebskosten kaum Gedanken machen (Athena kostet derzeit in Frankfurt je gescanntem TB 5 USD).

ABER: Wenn wir von sauber entwickelten Data Pipelines sprechen, sind wir schon etwas hacky unterwegs – GitLab ist kein optimales Werkzeug, um Datenanalysen durchzuführen. Zudem haben wir sämtliche Ressourcen in diesem Beispiel händisch angelegt. Eine saubere Lösung sollte mit einem Infrastructure-as-Code-Tool wie beispielsweise Terraform angelegt werden.

Insgesamt würde ich empfehlen, diese Lösung lediglich während der Fusionsphase der beiden Unternehmen weiterzuverwenden und zu pflegen. Besteht nach Abschluss der Fusion weiterhin Bedarf für die Auswertung von Verkaufs- und Konsumentendaten (hoffentlich!), sollten wir auf einem weißen Blatt Papier beginnen und eine dazu passende Lösung aufbauen. In diesem Zuge wäre es sinnvoll, RedShift als Data-Warehouse-Lösung noch einmal genauer unter die Lupe zu nehmen.

Weiterführende Literatur

Letzte Worte

Vielen Dank, dass du dir die Zeit genommen hast, diesen Blogpost zu lesen! Ich hoffe, dass du etwas Wertvolles für dich mitnehmen konntest 😊

Beitrag teilen

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.