man  die anoniem blijft
SOFTWAREONTWIKKELINGSECURITY & PRIVACY
22/04/2021 • Jan Eerdekens

Simpele en flexibele op ETL gebaseerde anonimisatie instellen – deel 2

In de eerste reeks van deze serie over op ETL gebaseerd anonimisatie is het onderzoeksgedeelte van het probleem dat ik probeerde op te lossen, behandeld in een proof of concept. Ik besloot Singer te gebruiken als softwareoplossing om een ETL-pijplijn te ontwikkelen. In het tweede gedeelte komen de configuratie van Singer aan bod, de geselecteerde taps en targets en de verbinding daartussen.

Ik zal ook kort aandacht besteden aan het lokaal uitvoeren van een Oracle-database waarmee de Singer Oracle Tap verbinding kan maken en ik zal toelichten welke problemen ik ben tegengekomen.

Singer configureren

Om de ETL-pijplijn te creëren, moeten we allereerst Singer installeren. Singer is in feite niet veel meer dan een verzameling taps en targets. Dit is simpele Python-code die in de Singer JSON-indeling datastromen produceert die naar elkaar kunnen worden omgeleid. De enige installatievereiste is het hebben van een werkende Python 3-installatie op je systeem. Voer onderstaand commando uit om te controleren of je Python 3 al correct hebt geïnstalleerd:

python --version

Als dit werkt en er een 3.x-versie wordt gegenereerd dan ben je al klaar. Als dit niet werkt of als er een 2.x-versie wordt gegenereerd dan moet je eerst Python 3 installeren. Instructies voor installatie op jouw besturingssysteem zijn eenvoudig online te vinden.

Oracle configureren

We gebruiken een Oracle-database die we zullen uitvoeren als een Docker-container. Ik heb voor deze configuratie diverse tests uitgevoerd, lees hier meer over aan het einde van deze post.

We zullen er in dit geval van uitgaan dat een Oracle-database wordt uitgevoerd op localhost:1521 met een SID die OraDoc heet. Het schema in deze database heet ETL en de gebruiker heet etl. Dit schema bevat een zeer eenvoudige testtabel met de naam TEST, met onderstaande structuur en een aantal rijen met daadwerkelijke testgegevens:

FIRST_NAME,LAST_NAME,PERSON_ID,PHONE

"Tap" configureren

Onze Python 3-installatie werkt nu en we kunnen dus verdergaan met de eerste stap van onze ETL-pijplijn: opname van de Oracle-databasegegevens door de Singer Oracle Tap.

De taps en targets bestaan weliswaar uit Python-code, maar de Oracle Tap gebruikt de cx_Oracle-bibliotheek als databasesturing. Voor deze bibliotheek is een Oracle Database Client vereist, genaamd Oracle Instant Client, om verbinding te kunnen maken met de database. Doorloop deze installatie-instructies om de client te installeren.

We kunnen verder met het configureren van de daadwerkelijke Oracle Tap zodra deze client is geïnstalleerd. Installatie is simpel: creëer een Python Virtual Environment, activeer deze en installeer vervolgens de tap-afhankelijkheid via pip. Je kunt hiervoor de volgende commando's gebruiken bij de Oracle Tap:

python3 -m venv ~/.virtualenvs/tap-oracle
source ~/.virtualenvs/tap-oracle/bin/activate
pip3 install tap-oracle

Zodra de tap is geïnstalleerd en de virtuele omgeving is geactiveerd, wordt het tap-oracle-commando beschikbaar:

(tap-oracle) developer@laptop .virtualenvs % tap-oracle usage: tap-oracle [-h] -c CONFIG [-s STATE] [-p PROPERTIES] [--catalog CATALOG] [-d] tap-oracle: error: the following arguments are required: -c/--config

We kunnen uit de melding, die we krijgen als we de tap proberen uit te voeren, opmaken dat de tap eerst verder moet worden geconfigureerd. Die configuratie bestaat in feite uit een eenvoudig JSON-bestand dat informatie bevat over de databaseverbinding en enkele andere parameters om te bepalen hoe de database wordt gesynchroniseerd. Om het overzichtelijk te houden, creëren we dit bestand (config.json) in de bin-map van onze tap-oracle virtuele omgeving met onderstaande content:

{
    "host": "localhost",
    "port": 1521,
    "user": "etl",
    "password": "admin123",
    "sid": "OraDoc",
    "filter_schemas": "ETL",
    "default_replication_method": "FULL_TABLE"
}

De eerste 5 configuratieparameters zijn vereist en vrij vanzelfsprekend, behalve misschien de laatste twee:

  • filter_schemas: een optionele parameter die een door komma's gescheiden overzicht met schemanamen bevat waarmee je alleen de tabelgegevens van specifieke schema's kunt repliceren in plaats van alle schema's.
  • default_replication_method: dit bepaalt hoe de geselecteerde schema's worden gerepliceerd. Er zijn 3 mogelijke waardes: LOG_BASED, FULL_TABLE en INCREMENTAL. Omdat we het simpel willen houden en we een kleine testdatabase gebruiken, zullen we voor dit POC FULL_TABLE gebruiken. Voor de modus LOG_BASED moet je ook zorgen dat onze Oracle-database overeenkomstig is geconfigureerd , dit valt buiten het bereik van deze POC en de op Docker gebaseerde Oracle-database.

We kunnen na het voltooien van deze configuratie, de tap uitvoeren in de detectiemodus. De tap gebruikt het configuratiebestand in deze modus om verbinding te maken met de database en om de geselecteerde schema's over de beschikbare tabellen en hun structuur te zoeken. We moeten deze gegevens (catalog) opslaan op een ander JSON-bestand. Om het bestand catalog.json te maken, voer je onderstaand commando uit vanuit de bin-map van de tap-oracle virtuele omgeving:

(tap-oracle) developer@laptop bin % tap-oracle -c config.json -d > catalog.json
INFO starting discovery
INFO dsn: (DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SID=OraDoc)))
INFO fetching row counts
INFO fetching tables:
   SELECT owner, table_name
   FROM all_tables
   WHERE owner != 'SYS' AND owner IN (:0) ['ETL']
INFO fetching views
INFO fetching column info
INFO fetching pk constraints

Je zult zien dat er niets is gesynchroniseerd als je de tap uitvoert met de config en de catalog:

(tap-oracle) developer@laptop bin % tap-oracle -c config.json --catalog catalog.json
INFO Selected streams: []
INFO No currently_syncing found

Om te zorgen dat de tap daadwerkelijk zaken synchroniseert, moet je het bestand catalog enigszins bewerken door de streams te selecteren die je wilt synchroniseren:

{
  "streams": [
    {
      "tap_stream_id": "ETL-TEST",
      "table_name": "TEST",
      "schema": {
        "properties": {
          "FIRST_NAME": {
            "type": [
              "null",
              "string"
            ]
          },
          ...
        },
        "type": "object"
      },
      "stream": "TEST",
      "metadata": [
        {
          "breadcrumb": [],
          "metadata": {
            "table-key-properties": [
              "PERSON_ID"
            ],
            "schema-name": "ETL",
            "database-name": "ORADOC",
            "is-view": false,
            "row-count": 0,
            "selected": true
          }
        },
        {
          "breadcrumb": [
            "properties",
            "FIRST_NAME"
          ],
          "metadata": {
            "sql-datatype": "VARCHAR2",
            "inclusion": "available",
            "selected-by-default": true
          }
        },
        {
          "breadcrumb": [
            "properties",
            "LAST_NAME"
          ],
          "metadata": {
            "sql-datatype": "VARCHAR2",
            "inclusion": "available",
            "selected-by-default": true
          }
        },
        {
          "breadcrumb": [
            "properties",
            "PERSON_ID"
          ],
          "metadata": {
            "sql-datatype": "NUMBER",
            "inclusion": "automatic",
            "selected-by-default": true
          }
        },
        {
          "breadcrumb": [
            "properties",
            "PHONE"
          ],
          "metadata": {
            "sql-datatype": "VARCHAR2",
            "inclusion": "available",
            "selected-by-default": true
          }
        }
      ]
    }
  ]
}

Voer het commando nu opnieuw uit en je zult zien dat de tap content van de geselecteerde stream synchroniseert:

(tap-oracle) developer@laptop bin % tap-oracle -c config.json --catalog catalog.json
INFO Selected streams: ['ETL-TEST']
INFO No currently_syncing found
INFO Beginning sync of stream(ETL-TEST) with sync method(full)
INFO Stream ETL-TEST is using full_table replication
{"type": "SCHEMA", "stream": "TEST", "schema": {"properties": {"FIRST_NAME": {"type": ["null", "string"]}, "LAST_NAME": {"type": ["null", "string"]}, "PERSON_ID": {"format": "singer.decimal", "type": ["string"]}, "PHONE": {"type": ["null", "string"]}}, "type": "object"}, "key_properties": ["PERSON_ID"]}
INFO dsn: (DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SID=OraDoc)))
{"type": "STATE", "value": {"bookmarks": {"ETL-TEST": {"last_replication_method": "FULL_TABLE", "version": 1618344226757}}, "currently_syncing": "ETL-TEST"}}
{"type": "ACTIVATE_VERSION", "stream": "TEST", "version": 1618344226757}
INFO select SELECT  "FIRST_NAME" , "LAST_NAME" , "PERSON_ID" , "PHONE" , ORA_ROWSCN
                                FROM ETL.TEST
                               ORDER BY ORA_ROWSCN ASC
{"type": "RECORD", "stream": "TEST", "record": {"FIRST_NAME": "John", "LAST_NAME": "Doe", "PERSON_ID": "1", "PHONE": "0499010203"}, "version": 1618344226757, "time_extracted": "2021-04-13T20:03:46.757794Z"}
{"type": "RECORD", "stream": "TEST", "record": {"FIRST_NAME": "Jane", "LAST_NAME": "Doe", "PERSON_ID": "1", "PHONE": "0499040506"}, "version": 1618344226757, "time_extracted": "2021-04-13T20:03:46.757794Z"}
INFO METRIC: {"type": "counter", "metric": "record_count", "value": 2, "tags": {}}
{"type": "ACTIVATE_VERSION", "stream": "TEST", "version": 1618344226757}
{"type": "STATE", "value": {"bookmarks": {"ETL-TEST": {"last_replication_method": "FULL_TABLE", "version": 1618344226757, "ORA_ROWSCN": null}}, "currently_syncing": null}}

Dat was al behoorlijk veel werk, maar het moeilijkste gedeelte hebben we gelukkig gehad!

phew meme

'Target' configureren

We hebben nu een tap ingesteld om databasegegevens op te halen in de Singer-indeling. We slaan de transformatiestappen in eerste instantie over en zullen eerst een target voor de pipe instellen. We kunnen zodoende snel de Extract- en Load-delen van de ETL-pijplijn verifiëren en al een leesbaar CSV-resultaat bekijken in plaats van Singer JSON-bestanden.

We gebruiken de Pipelinewise S3 CSV Target. De configuratie van dit target is vergelijkbaar met hoe we eerder de tap hebben geconfigureerd. Je moet wederom een virtuele omgeving creëren, deze activeren en de target-afhankelijkheid installeren via pip:

python3 -m venv ~/.virtualenvs/target-s3-csv
source ~/.virtualenvs/target-s3-csv/bin/activate
pip3 install pipelinewise-target-s3-csv

Voor dit target is ook een klein configuratiebestand vereist in de vorm van een config.json-bestand dat de naam van de bucket bevat waar we de CSV-bestanden naartoe willen sturen en de referenties die zijn vereist om de bucket te openen:

{
  "s3_bucket": "anonymized-data-bucket",
  "aws_access_key_id": "your_own_aws_access_key_id_value",
  "aws_secret_access_key": "your_own_aws_secret_access_key_value"
}

Met deze simpele configuratie hebben we alle benodigdheden om te zorgen dat de target werkt. We kunnen het resultaat van de Oracle Tap nu omleiden naar de target en, afhankelijk van de geselecteerde streams, zorgen dat één of meer momenteel nog niet-geanonimiseerde CSV-bestanden in onze S3-bucket terechtkomen. Als we onderstaand commando uitvoeren, vanuit de bin-map van de geactiveerde tap-oracle virtuele omgeving, verschijnt er een volledige tabelsynchronisatie:

(tap-oracle) developer@laptop bin % tap-oracle --config config.json --catalog catalog.json | ~/.virtualenvs/target-s3-csv/bin/target-s3-csv --config ~/.virtualenvs/target-s3-csv/bin/config.json
INFO Selected streams: ['ETL-TEST']
INFO No currently_syncing found
INFO Beginning sync of stream(ETL-TEST) with sync method(full)
INFO Stream ETL-TEST is using full_table replication
INFO dsn: (DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SID=OraDoc)))
time=2021-04-13 22:23:29 name=target_s3_csv level=INFO message=Attempting to create AWS session
INFO select SELECT  "FIRST_NAME" , "LAST_NAME" , "PERSON_ID" , "PHONE" , ORA_ROWSCN
                                FROM ETL.TEST
                               ORDER BY ORA_ROWSCN ASC
INFO METRIC: {"type": "counter", "metric": "record_count", "value": 2, "tags": {}}
time=2021-04-13 22:23:29 name=target_s3_csv level=INFO message=Uploading /var/folders/5k/86hrfqsd60b8pnh_81fqfy8h0000gn/T/TEST-20210413T222329.csv to bucket anonymized-data-bucket at TEST-20210413T222329.csv
{"bookmarks": {"ETL-TEST": {"last_replication_method": "FULL_TABLE", "version": 1618345409645, "ORA_ROWSCN": null}}, "currently_syncing": null}

Je kunt het resultaat controleren in S3-webinterface of met de AWS Command Line-client. Voer onderstaande commando's uit om de CSV-bestanden in de bucket te bekijken en op te halen.

aws s3 ls s3://anonymized-data-bucket/ --profile your_aws_profile_name
aws s3 cp s3://anonymized-data-bucket/<a filename output by the previous command>.csv test.csv --profile your_aws_profile_name

Je kunt ook iets met cat doen:

aws s3 cp s3://anonymized-data-bucket/TEST-20210413T223846.csv --profile your_aws_profile_name - | cat
FIRST_NAME,LAST_NAME,PERSON_ID,PHONE
John,Doe,1,0499010203
Jane,Doe,2,0499040506

almost there meme

Transformatie / anonimisatie configureren

We kunnen nu gegevens uit onze Oracle-database ophalen, de gewenste streams selecteren en opslaan als CSV-bestanden in S3. Er ontbreekt nog één kleine, maar essentiële stap: het anonimiseren van de gegevens tijdens de overdracht tussen tap en target.

We gebruiken Pipelinewise Transform Field voor het transformeren/anonimiseren. Deze software biedt het T-gedeelte dat ontbreekt om van de EL-tool van Singer een echte ETL-tool te maken.

We begeven ons op bekend gebied bij het configureren van het transformatiegedeelte. Configureer een nieuwe virtuele omgeving, activeer deze, installeer via pip een afhankelijkheid en creëer een configuratiebestand.

python3 -m venv ~/.virtualenvs/transform-field
source ~/.virtualenvs/transform-field/bin/activate
pip install pipelinewise-transform-field

Creëer een config.json-bestand in de bin-map van deze virtuele omgeving. Dit bestand bevat een overzichtelijke lijst met transformaties. Een dergelijke transformatie definieert simpelweg het soort transformatie dat moet worden toegepast op een specifiek veld in een specifieke stream. De configuratie in onderstaand voorbeeld bevat het resultaat van het HASHen van het veld FIRST_NAME in de stream TEST:

{
  "transformations": [
      {
          "field_id": "FIRST_NAME",
          "tap_stream_name": "TEST",
          "type": "HASH"
      }
  ]
}

Dit zijn de beschikbare transformaties in de stap Pipelinewise Transform Field:

  • SET-NULL: alle input wordt getransformeerd naar NULL
  • HASH: string-input wordt getransformeerd naar hash
  • HASH-SKIP-FIRST-n: string-input wordt naar hash getransformeerd waarbij de eerste n tekens worden overgeslagen, bijv. HASH-SKIP- FIRST-2
  • MASK-DATE: vervangt de maanden en dagen in datumkolommen zodat deze altijd 1 januari zijn
  • MASK-NUMBER: transformeert alle numerieke waarden naar nul
  • MASK-HIDDEN: transformeert alle strings naar ‘hidden’

Deze transformaties voldoen weliswaar voor de anonimisatiebehoeften van onze POC, maar ze kunnen ook eenvoudig worden aangepast. Je kunt zelfs aangepaste transformaties toevoegen. Bewerk hiervoor het bestand transform.py in de map lib/python3.9/site- packages/transform_field van de huidige virtuele omgeving.

Alles bij elkaar brengen

We kunnen deze drie virtuele omgevingen nu met elkaar verbinden om het gewenste eindresultaat te bereiken. Bij het uitvoeren van onderstaand commando moet de ETL-pijplijn het geselecteerde schema synchroniseren, bepaalde delen anonimiseren en het eindresultaat in onze S3-bucket opslaan als een CSV-bestand.

(tap-oracle) developer@laptop bin % tap-oracle --config config.json --catalog catalog.json | ~/.virtualenvs/transform-field/bin/transform-field --config ~/.virtualenvs/transform-field/bin/config.json | ~/.virtualenvs/target-s3-csv/bin/target-s3-csv --config ~/.virtualenvs/target-s3-csv/bin/config.json
INFO Selected streams: ['ETL-TEST']
INFO No currently_syncing found
INFO Beginning sync of stream(ETL-TEST) with sync method(full)
INFO Stream ETL-TEST is using full_table replication
INFO dsn: (DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521))(CONNECT_DATA=(SID=OraDoc)))
time=2021-04-13 22:38:46 name=target_s3_csv level=INFO message=Attempting to create AWS session
INFO select SELECT  "FIRST_NAME" , "LAST_NAME" , "PERSON_ID" , "PHONE" , ORA_ROWSCN
                                FROM ETL.TEST
                               ORDER BY ORA_ROWSCN ASC
INFO METRIC: {"type": "counter", "metric": "record_count", "value": 2, "tags": {}}
time=2021-04-13 22:38:46 name=transform_field level=INFO message=Exiting normally
time=2021-04-13 22:38:46 name=target_s3_csv level=INFO message=Uploading /var/folders/5k/86hrfqsd60b8pnh_81fqfy8h0000gn/T/TEST-20210413T223846.csv to bucket cjm-sandbox-anonymized-data at TEST-20210413T223846.csv
{"bookmarks": {"ETL-TEST": {"last_replication_method": "FULL_TABLE", "version": 1618346326464, "ORA_ROWSCN": null}}, "currently_syncing": null}

Als we nu de CSV bekijken in onze bucket:

aws s3 cp s3://anonymized-data-bucket/TEST-20210413T224646.csv --profile your_aws_profile_name - | cat
FIRST_NAME,LAST_NAME,PERSON_ID,PHONE
fd53ef835b15485572a6e82cf470dcb41fd218ae5751ab7531c956a2a6bcd3c7,Doe,1,0499010203
500501150a08713128839ca1465bfdc8a426268d6d0a576a16a80c13929f3faa,Doe,2,0499040506

i love it when a plan comes together meme

Conclusie

Je hebt in deze blog gezien dat we Singer heel eenvoudig kunnen gebruiken om de doelen van de POC te behalen die in de vorige blog zijn besproken. Je hebt er slechts wat eenvoudige zaken zoals Python en een Oracle-client voor nodig en het kan dan ook

op vrijwel alle systemen en in bijna alle omgevingen worden geïnstalleerd. Er wordt daarnaast gebruikgemaakt van enkele simpele op JSON gebaseerde configuratiebestanden die eenvoudig zijn af te stemmen op uiteenlopende use-cases. De bron van de taps en targets is ook beschikbaar en is doorgaans gebruiksvriendelijk en indien nodig aan te passen. Kun je geen geschikte tap of target vinden? Er zelf één schrijven moet niet al te moeilijk zijn.

Ik zal in de laatste reeks van deze serie laten zien hoe je het hele systeem enigszins kunt vereenvoudigen en zodanig kunt verpakken dat het beter geschikt is voor cloud-native gebruik/implementatie.

Side quest: Oracle in Docker

Omdat ik geen toegang had tot een bestaande Oracle database, moest ik er zelf een draaien. Je zou denken dat dat gemakkelijk en snel zou gaan, maar het bleek ingewikkelder en behoorlijk frustrerend. Vooral in vergelijking met het uitvoeren van een PostgreSQL- of MySQL/MariaDB-container met Docker.

Oracle XE werd een hele tijd geleden al beschikbaar gesteld. Het is een gratis versie van een Oracle-database met enkele beperkingen, maar geen daarvan was problematisch voor mij. Oracle XE kan native op Windows en Linux worden geïnstalleerd, maar omdat ik op een Mac werk en mijn andere databases via Docker gebruik, wilde ik het uitvoeren met een Docker image.

Hiervoor moest ik zelf een Oracle docker image bouwen. Er bestaat een Github repository die veel Dockerfiles en scripts bevat die door Oracle worden gedeeld. In deze repository is er ook een sectie voor Oracle databases: Oracle Database container afbeeldingen. Je kan deze repository bekijken, door naar de directory OracleDatabase/SingleInstance/dockerfiles te gaan en het script buildContainerImage.sh uit te voeren om een ​​echte Oracle Docker database container te bouwen.

Ik wilde een Oracle 12 opzetten die past bij de installatie van de klant, maar daarvoor moest ik eerst wat database installatiebestanden downloaden. Het lijkt er echter op dat die niet meer publiekelijk beschikbaar zijn (je kan ze wel krijgen via een Oracle contract). Dus ik heb Oracle 18.4.0 XE gekozen:

./buildContainerImage.sh -v 18.4.0 -x
...takes about 5 minutes...
docker run -p 1521:1521 -p 5500:5500 -e ORACLE_PWD=admin123 oracle/database:18.4.0-xe
... takes 30+ minutes...

waiting for dockerized oracle xe to start

Op mijn MacBook Pro duurt het opstarten van deze Docker container meer dan 30 minuten. Bovendien stopt het meestal na een paar uur met werken, waardoor een herstart nodig is die weer enorm veel tijd kost. Ik heb een aantal zaken geprobeerd, zoals het koppelen van volumes om alle gegevens op te slaan die tijdens de eerste start worden gegenereerd, maar dat leek niet te helpen. Het verbruikt ook veel resources op mijn apparaat.

Zelfs nadat de container aan de gang was, was het verbinden met de database erin ook niet echt eenvoudig vanwege de pluggable database zaken. Zelfs wanneer deze verbonden was, was het maken van een gebruiker en database specifiek voor onze POC en het verbinden met die ene opnieuw een hoop moeite: servicenaam versus SID en geen servicenaamondersteuning in de Oracle Python bibliotheek die door de tap wordt gebruikt.

simple etl pipeline in singer

Dus uiteindelijk, na wat meer Googlen, vond ik een Docker repository van Oracle die een oude Oracle 12 docker container bevat. Je hebt een gratis Oracle account nodig om de docker login te kunnen doen. Er is geen volume ondersteuning, dus in veel gevallen verlies je je gegevens, maar dit is gelukkig eenvoudig in te stellen, start consistent in een paar minuten en je kan er verbinding mee maken via een SID. Ook het toevoegen van een gebruiker en database was niet zo moeilijk en dus gebruikte ik dit in plaats van de nieuwere Github repository zaken.

docker login container-registry.oracle.com
docker pull container-registry.oracle.com/database/standard:12.1.0.2
docker run -d --env-file ./env -p 1521:1521 -p 5500:5500 -it --name dockerDB --shm-size="4g" container-registry.oracle.com/database/standard:12.1.0.2
Jan Eerdekens