From 02d8d8367ae63583ab98732afe659969ea8c21c3 Mon Sep 17 00:00:00 2001 From: Ayman Farhat <823713+aymanfarhat@users.noreply.github.com> Date: Mon, 6 Feb 2023 07:35:40 +0100 Subject: [PATCH] [Feature] Update data platform blue print with Dataflow Flex template (#1105) * Add initial dataflow template code + TF infra * Refactor the datapipeline DAG to use flex template operator, cleanup code * Remove unneeded bash scripts, update README with manual examples * Refactor datapipeline_dc_tags.py and include new Flex template * Update docs to reflect changes * Remove sub-dependencies and keep apache beam * Add missing license headers and update tests * Set resouces to 291 in tests * Update outputs via tfdoc * Update with outputs order and tfdoc * Correct number of resources * Fix to add region into command from var * Enable service account impersonation for running builds * Update example dataflow run command to use orchestrator SA * Remove hard coded values in example * Keep original airflow files, add new which use Flex template as example * Update tests and doc * Fix number of resources in plan * Run tfdoc remove files section in README * Fix number of modules in tfdoc * Update number of resources * Add missin service account * Update DF demo README * Quick rename --------- Co-authored-by: lcaggio Co-authored-by: Ludovico Magnocavallo --- .../data-platform-foundations/03-composer.tf | 1 + .../03-orchestration.tf | 59 +++ .../data-platform-foundations/IAM.md | 6 +- .../data-platform-foundations/README.md | 17 +- .../data-platform-foundations/demo/README.md | 11 +- .../demo/dataflow-csv2bq/.gitignore | 160 ++++++ .../demo/dataflow-csv2bq/Dockerfile | 29 ++ .../demo/dataflow-csv2bq/README.md | 63 +++ .../demo/dataflow-csv2bq/cloudbuild.yaml | 30 ++ .../demo/dataflow-csv2bq/src/csv2bq.py | 79 +++ .../demo/dataflow-csv2bq/src/requirements.txt | 1 + .../demo/datapipeline_dc_tags_flex.py | 461 ++++++++++++++++++ .../demo/datapipeline_flex.py | 225 +++++++++ .../images/df_demo_pipeline.png | Bin 0 -> 58976 bytes .../data-platform-foundations/outputs.tf | 25 +- .../data_platform_foundations/test_plan.py | 5 +- 16 files changed, 1152 insertions(+), 20 deletions(-) create mode 100644 blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/.gitignore create mode 100644 blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/Dockerfile create mode 100644 blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md create mode 100644 blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/cloudbuild.yaml create mode 100644 blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/csv2bq.py create mode 100644 blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/requirements.txt create mode 100644 blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags_flex.py create mode 100644 blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py create mode 100644 blueprints/data-solutions/data-platform-foundations/images/df_demo_pipeline.png diff --git a/blueprints/data-solutions/data-platform-foundations/03-composer.tf b/blueprints/data-solutions/data-platform-foundations/03-composer.tf index 33a21408..f806f0e5 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-composer.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-composer.tf @@ -40,6 +40,7 @@ locals { LOD_SA_DF = module.load-sa-df-0.email ORC_PRJ = module.orch-project.project_id ORC_GCS = module.orch-cs-0.url + ORC_GCS_TMP_DF = module.orch-cs-df-template.url TRF_PRJ = module.transf-project.project_id TRF_GCS_STAGING = module.transf-cs-df-0.url TRF_NET_VPC = local.transf_vpc diff --git a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf index 8e2d0725..a202afdd 100644 --- a/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf +++ b/blueprints/data-solutions/data-platform-foundations/03-orchestration.tf @@ -25,6 +25,11 @@ locals { ? var.network_config.network_self_link : module.orch-vpc.0.self_link ) + + # Note: This formatting is needed for output purposes since the fabric artifact registry + # module doesn't yet expose the docker usage path of a registry folder in the needed format. + orch_docker_path = format("%s-docker.pkg.dev/%s/%s", + var.region, module.orch-project.project_id, module.orch-artifact-reg.name) } module "orch-project" { @@ -44,6 +49,8 @@ module "orch-project" { "roles/iam.serviceAccountUser", "roles/storage.objectAdmin", "roles/storage.admin", + "roles/artifactregistry.admin", + "roles/serviceusage.serviceUsageConsumer", ] } iam = { @@ -65,7 +72,15 @@ module "orch-project" { ] "roles/storage.objectAdmin" = [ module.orch-sa-cmp-0.iam_email, + module.orch-sa-df-build.iam_email, "serviceAccount:${module.orch-project.service_accounts.robots.composer}", + "serviceAccount:${module.orch-project.service_accounts.robots.cloudbuild}", + ] + "roles/artifactregistry.reader" = [ + module.load-sa-df-0.iam_email, + ] + "roles/cloudbuild.serviceAgent" = [ + module.orch-sa-df-build.iam_email, ] "roles/storage.objectViewer" = [module.load-sa-df-0.iam_email] } @@ -81,6 +96,7 @@ module "orch-project" { "compute.googleapis.com", "container.googleapis.com", "containerregistry.googleapis.com", + "artifactregistry.googleapis.com", "dataflow.googleapis.com", "orgpolicy.googleapis.com", "pubsub.googleapis.com", @@ -148,3 +164,46 @@ module "orch-nat" { region = var.region router_network = module.orch-vpc.0.name } + +module "orch-artifact-reg" { + source = "../../../modules/artifact-registry" + project_id = module.orch-project.project_id + id = "${var.prefix}-app-images" + location = var.region + format = "DOCKER" + description = "Docker repository storing application images e.g. Dataflow, Cloud Run etc..." +} + +module "orch-cs-df-template" { + source = "../../../modules/gcs" + project_id = module.orch-project.project_id + prefix = var.prefix + name = "orc-cs-df-template" + location = var.region + storage_class = "REGIONAL" + encryption_key = try(local.service_encryption_keys.storage, null) +} + +module "orch-cs-build-staging" { + source = "../../../modules/gcs" + project_id = module.orch-project.project_id + prefix = var.prefix + name = "orc-cs-build-staging" + location = var.region + storage_class = "REGIONAL" + encryption_key = try(local.service_encryption_keys.storage, null) +} + +module "orch-sa-df-build" { + source = "../../../modules/iam-service-account" + project_id = module.orch-project.project_id + prefix = var.prefix + name = "orc-sa-df-build" + display_name = "Data platform Dataflow build service account" + # Note values below should pertain to the system / group / users who are able to + # invoke the build via this service account + iam = { + "roles/iam.serviceAccountTokenCreator" = [local.groups_iam.data-engineers] + "roles/iam.serviceAccountUser" = [local.groups_iam.data-engineers] + } +} diff --git a/blueprints/data-solutions/data-platform-foundations/IAM.md b/blueprints/data-solutions/data-platform-foundations/IAM.md index 5a1995da..dd898bd7 100644 --- a/blueprints/data-solutions/data-platform-foundations/IAM.md +++ b/blueprints/data-solutions/data-platform-foundations/IAM.md @@ -71,11 +71,13 @@ Legend: + additive, conditional. | members | roles | |---|---| -|gcp-data-engineers
group|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/cloudbuild.builds.editor](https://cloud.google.com/iam/docs/understanding-roles#cloudbuild.builds.editor)
[roles/composer.admin](https://cloud.google.com/iam/docs/understanding-roles#composer.admin)
[roles/composer.environmentAndStorageObjectAdmin](https://cloud.google.com/iam/docs/understanding-roles#composer.environmentAndStorageObjectAdmin)
[roles/iam.serviceAccountUser](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountUser)
[roles/iap.httpsResourceAccessor](https://cloud.google.com/iam/docs/understanding-roles#iap.httpsResourceAccessor)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | +|gcp-data-engineers
group|[roles/artifactregistry.admin](https://cloud.google.com/iam/docs/understanding-roles#artifactregistry.admin)
[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/cloudbuild.builds.editor](https://cloud.google.com/iam/docs/understanding-roles#cloudbuild.builds.editor)
[roles/composer.admin](https://cloud.google.com/iam/docs/understanding-roles#composer.admin)
[roles/composer.environmentAndStorageObjectAdmin](https://cloud.google.com/iam/docs/understanding-roles#composer.environmentAndStorageObjectAdmin)
[roles/iam.serviceAccountUser](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountUser)
[roles/iap.httpsResourceAccessor](https://cloud.google.com/iam/docs/understanding-roles#iap.httpsResourceAccessor)
[roles/serviceusage.serviceUsageConsumer](https://cloud.google.com/iam/docs/understanding-roles#serviceusage.serviceUsageConsumer)
[roles/storage.admin](https://cloud.google.com/iam/docs/understanding-roles#storage.admin)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | |SERVICE_IDENTITY_cloudcomposer-accounts
serviceAccount|[roles/composer.ServiceAgentV2Ext](https://cloud.google.com/iam/docs/understanding-roles#composer.ServiceAgentV2Ext)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | +|SERVICE_IDENTITY_gcp-sa-cloudbuild
serviceAccount|[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | |SERVICE_IDENTITY_service-networking
serviceAccount|[roles/servicenetworking.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#servicenetworking.serviceAgent) +| -|load-df-0
serviceAccount|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | +|load-df-0
serviceAccount|[roles/artifactregistry.reader](https://cloud.google.com/iam/docs/understanding-roles#artifactregistry.reader)
[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor)
[roles/storage.objectViewer](https://cloud.google.com/iam/docs/understanding-roles#storage.objectViewer) | |orc-cmp-0
serviceAccount|[roles/bigquery.jobUser](https://cloud.google.com/iam/docs/understanding-roles#bigquery.jobUser)
[roles/composer.worker](https://cloud.google.com/iam/docs/understanding-roles#composer.worker)
[roles/iam.serviceAccountUser](https://cloud.google.com/iam/docs/understanding-roles#iam.serviceAccountUser)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | +|orc-sa-df-build
serviceAccount|[roles/cloudbuild.serviceAgent](https://cloud.google.com/iam/docs/understanding-roles#cloudbuild.serviceAgent)
[roles/storage.objectAdmin](https://cloud.google.com/iam/docs/understanding-roles#storage.objectAdmin) | |trf-df-0
serviceAccount|[roles/bigquery.dataEditor](https://cloud.google.com/iam/docs/understanding-roles#bigquery.dataEditor) | ## Project trf diff --git a/blueprints/data-solutions/data-platform-foundations/README.md b/blueprints/data-solutions/data-platform-foundations/README.md index a05bbae7..08b24b21 100644 --- a/blueprints/data-solutions/data-platform-foundations/README.md +++ b/blueprints/data-solutions/data-platform-foundations/README.md @@ -219,7 +219,7 @@ module "data-platform" { prefix = "myprefix" } -# tftest modules=39 resources=287 +# tftest modules=43 resources=297 ``` ## Customizations @@ -263,13 +263,14 @@ You can find examples in the `[demo](./demo)` folder. | name | description | sensitive | |---|---|:---:| -| [bigquery-datasets](outputs.tf#L17) | BigQuery datasets. | | -| [demo_commands](outputs.tf#L27) | Demo commands. Relevant only if Composer is deployed. | | -| [gcs-buckets](outputs.tf#L40) | GCS buckets. | | -| [kms_keys](outputs.tf#L53) | Cloud MKS keys. | | -| [projects](outputs.tf#L58) | GCP Projects informations. | | -| [vpc_network](outputs.tf#L84) | VPC network. | | -| [vpc_subnet](outputs.tf#L93) | VPC subnetworks. | | +| [bigquery-datasets](outputs.tf#L16) | BigQuery datasets. | | +| [demo_commands](outputs.tf#L26) | Demo commands. Relevant only if Composer is deployed. | | +| [df_template](outputs.tf#L49) | Dataflow template image and template details. | | +| [gcs-buckets](outputs.tf#L58) | GCS buckets. | | +| [kms_keys](outputs.tf#L71) | Cloud MKS keys. | | +| [projects](outputs.tf#L76) | GCP Projects informations. | | +| [vpc_network](outputs.tf#L102) | VPC network. | | +| [vpc_subnet](outputs.tf#L111) | VPC subnetworks. | | ## TODOs diff --git a/blueprints/data-solutions/data-platform-foundations/demo/README.md b/blueprints/data-solutions/data-platform-foundations/demo/README.md index 97add086..639549fc 100644 --- a/blueprints/data-solutions/data-platform-foundations/demo/README.md +++ b/blueprints/data-solutions/data-platform-foundations/demo/README.md @@ -23,10 +23,11 @@ Below you can find a description of each example: ## Running the demo To run demo examples, please follow the following steps: -- 01: copy sample data to the `drop off` Cloud Storage bucket impersonating the `load` service account. -- 02: copy sample data structure definition in the `orchestration` Cloud Storage bucket impersonating the `orchestration` service account. -- 03: copy the Cloud Composer DAG to the Cloud Composer Storage bucket impersonating the `orchestration` service account. -- 04: Open the Cloud Composer Airflow UI and run the imported DAG. -- 05: Run the BigQuery query to see results. +- 01: Copy sample data to the `drop off` Cloud Storage bucket impersonating the `load` service account. +- 02: Copy sample data structure definition in the `orchestration` Cloud Storage bucket impersonating the `orchestration` service account. +- 03: Copy the Cloud Composer DAG to the Cloud Composer Storage bucket impersonating the `orchestration` service account. +- 04: Build the Dataflow Flex template and image via a Cloud Build pipeline +- 05: Open the Cloud Composer Airflow UI and run the imported DAG. +- 06: Run the BigQuery query to see results. You can find pre-computed commands in the `demo_commands` output variable of the deployed terraform [data pipeline](../). diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/.gitignore b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/.gitignore new file mode 100644 index 00000000..68bc17f9 --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/Dockerfile b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/Dockerfile new file mode 100644 index 00000000..69c6d2ee --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/Dockerfile @@ -0,0 +1,29 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM gcr.io/dataflow-templates-base/python39-template-launcher-base + +ENV FLEX_TEMPLATE_PYTHON_REQUIREMENTS_FILE="/template/requirements.txt" +ENV FLEX_TEMPLATE_PYTHON_PY_FILE="/template/csv2bq.py" + +COPY ./src/ /template + +RUN apt-get update \ + && apt-get install -y libffi-dev git \ + && rm -rf /var/lib/apt/lists/* \ + && pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r $FLEX_TEMPLATE_PYTHON_REQUIREMENTS_FILE \ + && pip download --no-cache-dir --dest /tmp/dataflow-requirements-cache -r $FLEX_TEMPLATE_PYTHON_REQUIREMENTS_FILE + +ENV PIP_NO_DEPS=True diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md new file mode 100644 index 00000000..44f178fa --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/README.md @@ -0,0 +1,63 @@ +## Pipeline summary +This demo serves as a simple example of building and launching a Flex Template Dataflow pipeline. The code mainly focuses on reading a CSV file as input along with a JSON schema file as side input. The pipeline Parses both inputs and writes the data to the relevant BigQuery table while applying the schema passed from input. + +![Dataflow pipeline overview](../../images/df_demo_pipeline.png "Dataflow pipeline overview") + +## Example build run + +Below is an example for triggering the Dataflow flex template build pipeline defined in `cloudbuild.yaml`. The Terraform output provides an example as well filled with the parameters values based on the generated resources in the data platform. + +``` +GCP_PROJECT="[ORCHESTRATION-PROJECT]" +TEMPLATE_IMAGE="[REGION].pkg.dev/[ORCHESTRATION-PROJECT]/[REPOSITORY]/csv2bq:latest" +TEMPLATE_PATH="gs://[DATAFLOW-TEMPLATE-BUCKEt]/csv2bq.json" +STAGIN_PATH="gs://[ORCHESTRATION-STAGING-BUCKET]/build" +LOG_PATH="gs://[ORCHESTRATION-LOGS-BUCKET]/logs" +REGION="[REGION]" +BUILD_SERVICE_ACCOUNT=orc-sa-df-build@[SERVICE_PROJECT_ID].iam.gserviceaccount.com + +gcloud builds submit \ + --config=cloudbuild.yaml \ + --project=$GCP_PROJECT \ + --region=$REGION \ + --gcs-log-dir=$LOG_PATH \ + --gcs-source-staging-dir=$STAGIN_PATH \ + --substitutions=_TEMPLATE_IMAGE=$TEMPLATE_IMAGE,_TEMPLATE_PATH=$TEMPLATE_PATH,_DOCKER_DIR="." \ + --impersonate-service-account=$BUILD_SERVICE_ACCOUNT +``` + +**Note:** For the scope of the demo, the launch of this build is manual, but in production, this build would be launched via a configured cloud build trigger when new changes are merged into the code branch of the Dataflow template. + +## Example Dataflow pipeline launch in bash (from flex template) + +Below is an example of launching a dataflow pipeline manually, based on the built template. When launched manually, the Dataflow pipeline would be launched via the orchestration service account, which is what the Airflow DAG is also using in the scope of this demo. + +**Note:** In the data platform demo, the launch of this Dataflow pipeline is handled by the airflow operator (DataflowStartFlexTemplateOperator). + +``` +#!/bin/bash + +PROJECT_ID=[LOAD-PROJECT] +REGION=[REGION] +ORCH_SERVICE_ACCOUNT=orchestrator@[SERVICE_PROJECT_ID].iam.gserviceaccount.com +SUBNET=[SUBNET-NAME] + +PIPELINE_STAGIN_PATH="gs://[LOAD-STAGING-BUCKET]/build" +CSV_FILE=gs://[DROP-ZONE-BUCKET]/customers.csv +JSON_SCHEMA=gs://[ORCHESTRATION-BUCKET]/customers_schema.json +OUTPUT_TABLE=[DESTINATION-PROJ].[DESTINATION-DATASET].customers +TEMPLATE_PATH=gs://[ORCHESTRATION-DF-GCS]/csv2bq.json + + +gcloud dataflow flex-template run "csv2bq-`date +%Y%m%d-%H%M%S`" \ + --template-file-gcs-location $TEMPLATE_PATH \ + --parameters temp_location="$PIPELINE_STAGIN_PATH/tmp" \ + --parameters staging_location="$PIPELINE_STAGIN_PATH/stage" \ + --parameters csv_file=$CSV_FILE \ + --parameters json_schema=$JSON_SCHEMA\ + --parameters output_table=$OUTPUT_TABLE \ + --region $REGION \ + --project $PROJECT_ID \ + --subnetwork="regions/$REGION/subnetworks/$SUBNET" \ + --service-account-email=$ORCH_SERVICE_ACCOUNT +``` diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/cloudbuild.yaml b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/cloudbuild.yaml new file mode 100644 index 00000000..11354c2e --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/cloudbuild.yaml @@ -0,0 +1,30 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +steps: +- name: gcr.io/cloud-builders/gcloud + id: "Build docker image" + args: ['builds', 'submit', '--tag', '$_TEMPLATE_IMAGE', '.'] + dir: '$_DOCKER_DIR' + waitFor: ['-'] +- name: gcr.io/cloud-builders/gcloud + id: "Build template" + args: ['dataflow', + 'flex-template', + 'build', + '$_TEMPLATE_PATH', + '--image=$_TEMPLATE_IMAGE', + '--sdk-language=PYTHON' + ] + waitFor: ['Build docker image'] diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/csv2bq.py b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/csv2bq.py new file mode 100644 index 00000000..0f8ad127 --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/csv2bq.py @@ -0,0 +1,79 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import apache_beam as beam +from apache_beam.io import ReadFromText, Read, WriteToBigQuery, BigQueryDisposition +from apache_beam.options.pipeline_options import PipelineOptions, SetupOptions +from apache_beam.io.filesystems import FileSystems +import json +import argparse + + +class ParseRow(beam.DoFn): + """ + Splits a given csv row by a seperator, validates fields and returns a dict + structure compatible with the BigQuery transform + """ + + def process(self, element: str, table_fields: list, delimiter: str): + split_row = element.split(delimiter) + parsed_row = {} + + for i, field in enumerate(table_fields['BigQuery Schema']): + parsed_row[field['name']] = split_row[i] + + yield parsed_row + +def run(argv=None, save_main_session=True): + parser = argparse.ArgumentParser() + parser.add_argument('--csv_file', + type=str, + required=True, + help='Path to the CSV file') + parser.add_argument('--json_schema', + type=str, + required=True, + help='Path to the JSON schema') + parser.add_argument('--output_table', + type=str, + required=True, + help='BigQuery path for the output table') + + args, pipeline_args = parser.parse_known_args(argv) + pipeline_options = PipelineOptions(pipeline_args) + pipeline_options.view_as( + SetupOptions).save_main_session = save_main_session + + with beam.Pipeline(options=pipeline_options) as p: + + def get_table_schema(table_path, table_schema): + return {'fields': table_schema['BigQuery Schema']} + + csv_input = p | 'Read CSV' >> ReadFromText(args.csv_file) + schema_input = p | 'Load Schema' >> beam.Create( + json.loads(FileSystems.open(args.json_schema).read())) + + table_fields = beam.pvalue.AsDict(schema_input) + parsed = csv_input | 'Parse and validate rows' >> beam.ParDo( + ParseRow(), table_fields, ',') + + parsed | 'Write to BigQuery' >> WriteToBigQuery( + args.output_table, + schema=get_table_schema, + create_disposition=BigQueryDisposition.CREATE_IF_NEEDED, + write_disposition=BigQueryDisposition.WRITE_TRUNCATE, + schema_side_inputs=(table_fields, )) + +if __name__ == "__main__": + run() diff --git a/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/requirements.txt b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/requirements.txt new file mode 100644 index 00000000..21c569a0 --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/dataflow-csv2bq/src/requirements.txt @@ -0,0 +1 @@ +apache-beam==2.44.0 diff --git a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags_flex.py b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags_flex.py new file mode 100644 index 00000000..f911e335 --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_dc_tags_flex.py @@ -0,0 +1,461 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# -------------------------------------------------------------------------------- +# Load The Dependencies +# -------------------------------------------------------------------------------- + +import datetime +import json +import os +import time + +from airflow import models +from airflow.operators import dummy +from airflow.providers.google.cloud.operators.dataflow import DataflowStartFlexTemplateOperator +from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator, BigQueryUpsertTableOperator, BigQueryUpdateTableSchemaOperator +from airflow.utils.task_group import TaskGroup + +# -------------------------------------------------------------------------------- +# Set variables - Needed for the DEMO +# -------------------------------------------------------------------------------- +BQ_LOCATION = os.environ.get("BQ_LOCATION") +DATA_CAT_TAGS = json.loads(os.environ.get("DATA_CAT_TAGS")) +DWH_LAND_PRJ = os.environ.get("DWH_LAND_PRJ") +DWH_LAND_BQ_DATASET = os.environ.get("DWH_LAND_BQ_DATASET") +DWH_LAND_GCS = os.environ.get("DWH_LAND_GCS") +DWH_CURATED_PRJ = os.environ.get("DWH_CURATED_PRJ") +DWH_CURATED_BQ_DATASET = os.environ.get("DWH_CURATED_BQ_DATASET") +DWH_CURATED_GCS = os.environ.get("DWH_CURATED_GCS") +DWH_CONFIDENTIAL_PRJ = os.environ.get("DWH_CONFIDENTIAL_PRJ") +DWH_CONFIDENTIAL_BQ_DATASET = os.environ.get("DWH_CONFIDENTIAL_BQ_DATASET") +DWH_CONFIDENTIAL_GCS = os.environ.get("DWH_CONFIDENTIAL_GCS") +DWH_PLG_PRJ = os.environ.get("DWH_PLG_PRJ") +DWH_PLG_BQ_DATASET = os.environ.get("DWH_PLG_BQ_DATASET") +DWH_PLG_GCS = os.environ.get("DWH_PLG_GCS") +GCP_REGION = os.environ.get("GCP_REGION") +DRP_PRJ = os.environ.get("DRP_PRJ") +DRP_BQ = os.environ.get("DRP_BQ") +DRP_GCS = os.environ.get("DRP_GCS") +DRP_PS = os.environ.get("DRP_PS") +LOD_PRJ = os.environ.get("LOD_PRJ") +LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING") +LOD_NET_VPC = os.environ.get("LOD_NET_VPC") +LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET") +LOD_SA_DF = os.environ.get("LOD_SA_DF") +ORC_PRJ = os.environ.get("ORC_PRJ") +ORC_GCS = os.environ.get("ORC_GCS") +ORC_GCS_TMP_DF = os.environ.get("ORC_GCS_TMP_DF") +TRF_PRJ = os.environ.get("TRF_PRJ") +TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING") +TRF_NET_VPC = os.environ.get("TRF_NET_VPC") +TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET") +TRF_SA_DF = os.environ.get("TRF_SA_DF") +TRF_SA_BQ = os.environ.get("TRF_SA_BQ") +DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "") +DF_REGION = os.environ.get("GCP_REGION") +DF_ZONE = os.environ.get("GCP_REGION") + "-b" + +# -------------------------------------------------------------------------------- +# Set default arguments +# -------------------------------------------------------------------------------- + +# If you are running Airflow in more than one time zone +# see https://airflow.apache.org/docs/apache-airflow/stable/timezone.html +# for best practices +yesterday = datetime.datetime.now() - datetime.timedelta(days=1) + +default_args = { + 'owner': 'airflow', + 'start_date': yesterday, + 'depends_on_past': False, + 'email': [''], + 'email_on_failure': False, + 'email_on_retry': False, + 'retries': 1, + 'retry_delay': datetime.timedelta(minutes=5), +} + +dataflow_environment = { + 'serviceAccountEmail': LOD_SA_DF, + 'workerZone': DF_ZONE, + 'stagingLocation': f'{LOD_GCS_STAGING}/staging', + 'tempLocation': f'{LOD_GCS_STAGING}/tmp', + 'subnetwork': LOD_NET_SUBNET, + 'kmsKeyName': DF_KMS_KEY, + 'ipConfiguration': 'WORKER_IP_PRIVATE' +} + +# -------------------------------------------------------------------------------- +# Main DAG +# -------------------------------------------------------------------------------- + +with models.DAG('data_pipeline_dc_tags_dag_flex', + default_args=default_args, + schedule_interval=None) as dag: + start = dummy.DummyOperator(task_id='start', trigger_rule='all_success') + + end = dummy.DummyOperator(task_id='end', trigger_rule='all_success') + + # Bigquery Tables created here for demo porpuse. + # Consider a dedicated pipeline or tool for a real life scenario. + with TaskGroup('upsert_table') as upsert_table: + upsert_table_customers = BigQueryUpsertTableOperator( + task_id="upsert_table_customers", + project_id=DWH_LAND_PRJ, + dataset_id=DWH_LAND_BQ_DATASET, + impersonation_chain=[TRF_SA_DF], + table_resource={ + "tableReference": { + "tableId": "customers" + }, + }, + ) + + upsert_table_purchases = BigQueryUpsertTableOperator( + task_id="upsert_table_purchases", + project_id=DWH_LAND_PRJ, + dataset_id=DWH_LAND_BQ_DATASET, + impersonation_chain=[TRF_SA_BQ], + table_resource={"tableReference": { + "tableId": "purchases" + }}, + ) + + upsert_table_customer_purchase_curated = BigQueryUpsertTableOperator( + task_id="upsert_table_customer_purchase_curated", + project_id=DWH_CURATED_PRJ, + dataset_id=DWH_CURATED_BQ_DATASET, + impersonation_chain=[TRF_SA_BQ], + table_resource={ + "tableReference": { + "tableId": "customer_purchase" + } + }, + ) + + upsert_table_customer_purchase_confidential = BigQueryUpsertTableOperator( + task_id="upsert_table_customer_purchase_confidential", + project_id=DWH_CONFIDENTIAL_PRJ, + dataset_id=DWH_CONFIDENTIAL_BQ_DATASET, + impersonation_chain=[TRF_SA_BQ], + table_resource={ + "tableReference": { + "tableId": "customer_purchase" + } + }, + ) + + # Bigquery Tables schema defined here for demo porpuse. + # Consider a dedicated pipeline or tool for a real life scenario. + with TaskGroup('update_schema_table') as update_schema_table: + update_table_schema_customers = BigQueryUpdateTableSchemaOperator( + task_id="update_table_schema_customers", + project_id=DWH_LAND_PRJ, + dataset_id=DWH_LAND_BQ_DATASET, + table_id="customers", + impersonation_chain=[TRF_SA_BQ], + include_policy_tags=True, + schema_fields_updates=[{ + "mode": "REQUIRED", + "name": "id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "name", + "type": "STRING", + "description": "Name", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "surname", + "type": "STRING", + "description": "Surname", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "timestamp", + "type": "TIMESTAMP", + "description": "Timestamp" + }]) + + update_table_schema_purchases = BigQueryUpdateTableSchemaOperator( + task_id="update_table_schema_purchases", + project_id=DWH_LAND_PRJ, + dataset_id=DWH_LAND_BQ_DATASET, + table_id="purchases", + impersonation_chain=[TRF_SA_BQ], + include_policy_tags=True, + schema_fields_updates=[{ + "mode": "REQUIRED", + "name": "id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "customer_id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "item", + "type": "STRING", + "description": "Item Name" + }, { + "mode": "REQUIRED", + "name": "price", + "type": "FLOAT", + "description": "Item Price" + }, { + "mode": "REQUIRED", + "name": "timestamp", + "type": "TIMESTAMP", + "description": "Timestamp" + }]) + + update_table_schema_customer_purchase_curated = BigQueryUpdateTableSchemaOperator( + task_id="update_table_schema_customer_purchase_curated", + project_id=DWH_CURATED_PRJ, + dataset_id=DWH_CURATED_BQ_DATASET, + table_id="customer_purchase", + impersonation_chain=[TRF_SA_BQ], + include_policy_tags=True, + schema_fields_updates=[{ + "mode": "REQUIRED", + "name": "customer_id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "purchase_id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "name", + "type": "STRING", + "description": "Name", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "surname", + "type": "STRING", + "description": "Surname", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "item", + "type": "STRING", + "description": "Item Name" + }, { + "mode": "REQUIRED", + "name": "price", + "type": "FLOAT", + "description": "Item Price" + }, { + "mode": "REQUIRED", + "name": "timestamp", + "type": "TIMESTAMP", + "description": "Timestamp" + }]) + + update_table_schema_customer_purchase_confidential = BigQueryUpdateTableSchemaOperator( + task_id="update_table_schema_customer_purchase_confidential", + project_id=DWH_CONFIDENTIAL_PRJ, + dataset_id=DWH_CONFIDENTIAL_BQ_DATASET, + table_id="customer_purchase", + impersonation_chain=[TRF_SA_BQ], + include_policy_tags=True, + schema_fields_updates=[{ + "mode": "REQUIRED", + "name": "customer_id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "purchase_id", + "type": "INTEGER", + "description": "ID" + }, { + "mode": "REQUIRED", + "name": "name", + "type": "STRING", + "description": "Name", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "surname", + "type": "STRING", + "description": "Surname", + "policyTags": { + "names": [DATA_CAT_TAGS.get('2_Private', None)] + } + }, { + "mode": "REQUIRED", + "name": "item", + "type": "STRING", + "description": "Item Name" + }, { + "mode": "REQUIRED", + "name": "price", + "type": "FLOAT", + "description": "Item Price" + }, { + "mode": "REQUIRED", + "name": "timestamp", + "type": "TIMESTAMP", + "description": "Timestamp" + }]) + + customers_import = DataflowStartFlexTemplateOperator( + task_id='dataflow_customers_import', + project_id=LOD_PRJ, + location=DF_REGION, + body={ + 'launchParameter': { + 'jobName': f'dataflow-customers-import-{round(time.time())}', + 'containerSpecGcsPath': f'{ORC_GCS_TMP_DF}/csv2bq.json', + 'environment': { + 'serviceAccountEmail': LOD_SA_DF, + 'workerZone': DF_ZONE, + 'stagingLocation': f'{LOD_GCS_STAGING}/staging', + 'tempLocation': f'{LOD_GCS_STAGING}/tmp', + 'subnetwork': LOD_NET_SUBNET, + 'kmsKeyName': DF_KMS_KEY, + 'ipConfiguration': 'WORKER_IP_PRIVATE' + }, + 'parameters': { + 'csv_file': + f'{DRP_GCS}/customers.csv', + 'json_schema': + f'{ORC_GCS}/customers_schema.json', + 'output_table': + f'{DWH_LAND_PRJ}:{DWH_LAND_BQ_DATASET}.customers', + } + } + }) + + purchases_import = DataflowStartFlexTemplateOperator( + task_id='dataflow_purchases_import', + project_id=LOD_PRJ, + location=DF_REGION, + body={ + 'launchParameter': { + 'jobName': f'dataflow-purchases-import-{round(time.time())}', + 'containerSpecGcsPath': f'{ORC_GCS_TMP_DF}/csv2bq.json', + 'environment': { + 'serviceAccountEmail': LOD_SA_DF, + 'workerZone': DF_ZONE, + 'stagingLocation': f'{LOD_GCS_STAGING}/staging', + 'tempLocation': f'{LOD_GCS_STAGING}/tmp', + 'subnetwork': LOD_NET_SUBNET, + 'kmsKeyName': DF_KMS_KEY, + 'ipConfiguration': 'WORKER_IP_PRIVATE' + }, + 'parameters': { + 'csv_file': + f'{DRP_GCS}/purchases.csv', + 'json_schema': + f'{ORC_GCS}/purchases_schema.json', + 'output_table': + f'{DWH_LAND_PRJ}:{DWH_LAND_BQ_DATASET}.purchases', + } + } + }) + + join_customer_purchase = BigQueryInsertJobOperator( + task_id='bq_join_customer_purchase', + gcp_conn_id='bigquery_default', + project_id=TRF_PRJ, + location=BQ_LOCATION, + configuration={ + 'jobType': 'QUERY', + 'query': { + 'query': + """SELECT + c.id as customer_id, + p.id as purchase_id, + c.name as name, + c.surname as surname, + p.item as item, + p.price as price, + p.timestamp as timestamp + FROM `{dwh_0_prj}.{dwh_0_dataset}.customers` c + JOIN `{dwh_0_prj}.{dwh_0_dataset}.purchases` p ON c.id = p.customer_id + """.format( + dwh_0_prj=DWH_LAND_PRJ, + dwh_0_dataset=DWH_LAND_BQ_DATASET, + ), + 'destinationTable': { + 'projectId': DWH_CURATED_PRJ, + 'datasetId': DWH_CURATED_BQ_DATASET, + 'tableId': 'customer_purchase' + }, + 'writeDisposition': + 'WRITE_TRUNCATE', + "useLegacySql": + False + } + }, + impersonation_chain=[TRF_SA_BQ]) + + confidential_customer_purchase = BigQueryInsertJobOperator( + task_id='bq_confidential_customer_purchase', + gcp_conn_id='bigquery_default', + project_id=TRF_PRJ, + location=BQ_LOCATION, + configuration={ + 'jobType': 'QUERY', + 'query': { + 'query': + """SELECT + customer_id, + purchase_id, + name, + surname, + item, + price, + timestamp + FROM `{dwh_cur_prj}.{dwh_cur_dataset}.customer_purchase` + """.format( + dwh_cur_prj=DWH_CURATED_PRJ, + dwh_cur_dataset=DWH_CURATED_BQ_DATASET, + ), + 'destinationTable': { + 'projectId': DWH_CONFIDENTIAL_PRJ, + 'datasetId': DWH_CONFIDENTIAL_BQ_DATASET, + 'tableId': 'customer_purchase' + }, + 'writeDisposition': + 'WRITE_TRUNCATE', + "useLegacySql": + False + } + }, + impersonation_chain=[TRF_SA_BQ]) + +start >> upsert_table >> update_schema_table >> [ + customers_import, purchases_import +] >> join_customer_purchase >> confidential_customer_purchase >> end diff --git a/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py new file mode 100644 index 00000000..34ff10cc --- /dev/null +++ b/blueprints/data-solutions/data-platform-foundations/demo/datapipeline_flex.py @@ -0,0 +1,225 @@ +# Copyright 2022 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# -------------------------------------------------------------------------------- +# Load The Dependencies +# -------------------------------------------------------------------------------- + +import datetime +import json +import os +import time + +from airflow import models +from airflow.providers.google.cloud.operators.dataflow import DataflowStartFlexTemplateOperator +from airflow.operators import dummy +from airflow.providers.google.cloud.operators.bigquery import BigQueryInsertJobOperator + +# -------------------------------------------------------------------------------- +# Set variables - Needed for the DEMO +# -------------------------------------------------------------------------------- +BQ_LOCATION = os.environ.get("BQ_LOCATION") +DATA_CAT_TAGS = json.loads(os.environ.get("DATA_CAT_TAGS")) +DWH_LAND_PRJ = os.environ.get("DWH_LAND_PRJ") +DWH_LAND_BQ_DATASET = os.environ.get("DWH_LAND_BQ_DATASET") +DWH_LAND_GCS = os.environ.get("DWH_LAND_GCS") +DWH_CURATED_PRJ = os.environ.get("DWH_CURATED_PRJ") +DWH_CURATED_BQ_DATASET = os.environ.get("DWH_CURATED_BQ_DATASET") +DWH_CURATED_GCS = os.environ.get("DWH_CURATED_GCS") +DWH_CONFIDENTIAL_PRJ = os.environ.get("DWH_CONFIDENTIAL_PRJ") +DWH_CONFIDENTIAL_BQ_DATASET = os.environ.get("DWH_CONFIDENTIAL_BQ_DATASET") +DWH_CONFIDENTIAL_GCS = os.environ.get("DWH_CONFIDENTIAL_GCS") +DWH_PLG_PRJ = os.environ.get("DWH_PLG_PRJ") +DWH_PLG_BQ_DATASET = os.environ.get("DWH_PLG_BQ_DATASET") +DWH_PLG_GCS = os.environ.get("DWH_PLG_GCS") +GCP_REGION = os.environ.get("GCP_REGION") +DRP_PRJ = os.environ.get("DRP_PRJ") +DRP_BQ = os.environ.get("DRP_BQ") +DRP_GCS = os.environ.get("DRP_GCS") +DRP_PS = os.environ.get("DRP_PS") +LOD_PRJ = os.environ.get("LOD_PRJ") +LOD_GCS_STAGING = os.environ.get("LOD_GCS_STAGING") +LOD_NET_VPC = os.environ.get("LOD_NET_VPC") +LOD_NET_SUBNET = os.environ.get("LOD_NET_SUBNET") +LOD_SA_DF = os.environ.get("LOD_SA_DF") +ORC_PRJ = os.environ.get("ORC_PRJ") +ORC_GCS = os.environ.get("ORC_GCS") +ORC_GCS_TMP_DF = os.environ.get("ORC_GCS_TMP_DF") +TRF_PRJ = os.environ.get("TRF_PRJ") +TRF_GCS_STAGING = os.environ.get("TRF_GCS_STAGING") +TRF_NET_VPC = os.environ.get("TRF_NET_VPC") +TRF_NET_SUBNET = os.environ.get("TRF_NET_SUBNET") +TRF_SA_DF = os.environ.get("TRF_SA_DF") +TRF_SA_BQ = os.environ.get("TRF_SA_BQ") +DF_KMS_KEY = os.environ.get("DF_KMS_KEY", "") +DF_REGION = os.environ.get("GCP_REGION") +DF_ZONE = os.environ.get("GCP_REGION") + "-b" + +# -------------------------------------------------------------------------------- +# Set default arguments +# -------------------------------------------------------------------------------- + +# If you are running Airflow in more than one time zone +# see https://airflow.apache.org/docs/apache-airflow/stable/timezone.html +# for best practices +yesterday = datetime.datetime.now() - datetime.timedelta(days=1) + +default_args = { + 'owner': 'airflow', + 'start_date': yesterday, + 'depends_on_past': False, + 'email': [''], + 'email_on_failure': False, + 'email_on_retry': False, + 'retries': 1, + 'retry_delay': datetime.timedelta(minutes=5), +} + +dataflow_environment = { + 'serviceAccountEmail': LOD_SA_DF, + 'workerZone': DF_ZONE, + 'stagingLocation': f'{LOD_GCS_STAGING}/staging', + 'tempLocation': f'{LOD_GCS_STAGING}/tmp', + 'subnetwork': LOD_NET_SUBNET, + 'kmsKeyName': DF_KMS_KEY, + 'ipConfiguration': 'WORKER_IP_PRIVATE' +} + +# -------------------------------------------------------------------------------- +# Main DAG +# -------------------------------------------------------------------------------- + +with models.DAG('data_pipeline_dag_flex', + default_args=default_args, + schedule_interval=None) as dag: + + start = dummy.DummyOperator(task_id='start', trigger_rule='all_success') + + end = dummy.DummyOperator(task_id='end', trigger_rule='all_success') + + # Bigquery Tables automatically created for demo purposes. + # Consider a dedicated pipeline or tool for a real life scenario. + customers_import = DataflowStartFlexTemplateOperator( + task_id='dataflow_customers_import', + project_id=LOD_PRJ, + location=DF_REGION, + body={ + 'launchParameter': { + 'jobName': f'dataflow-customers-import-{round(time.time())}', + 'containerSpecGcsPath': f'{ORC_GCS_TMP_DF}/csv2bq.json', + 'environment': dataflow_environment, + 'parameters': { + 'csv_file': + f'{DRP_GCS}/customers.csv', + 'json_schema': + f'{ORC_GCS}/customers_schema.json', + 'output_table': + f'{DWH_LAND_PRJ}:{DWH_LAND_BQ_DATASET}.customers', + } + } + }) + + purchases_import = DataflowStartFlexTemplateOperator( + task_id='dataflow_purchases_import', + project_id=LOD_PRJ, + location=DF_REGION, + body={ + 'launchParameter': { + 'jobName': f'dataflow-purchases-import-{round(time.time())}', + 'containerSpecGcsPath': f'{ORC_GCS_TMP_DF}/csv2bq.json', + 'environment': dataflow_environment, + 'parameters': { + 'csv_file': + f'{DRP_GCS}/purchases.csv', + 'json_schema': + f'{ORC_GCS}/purchases_schema.json', + 'output_table': + f'{DWH_LAND_PRJ}:{DWH_LAND_BQ_DATASET}.purchases', + } + } + }) + + join_customer_purchase = BigQueryInsertJobOperator( + task_id='bq_join_customer_purchase', + gcp_conn_id='bigquery_default', + project_id=TRF_PRJ, + location=BQ_LOCATION, + configuration={ + 'jobType': 'QUERY', + 'query': { + 'query': + """SELECT + c.id as customer_id, + p.id as purchase_id, + p.item as item, + p.price as price, + p.timestamp as timestamp + FROM `{dwh_0_prj}.{dwh_0_dataset}.customers` c + JOIN `{dwh_0_prj}.{dwh_0_dataset}.purchases` p ON c.id = p.customer_id + """.format( + dwh_0_prj=DWH_LAND_PRJ, + dwh_0_dataset=DWH_LAND_BQ_DATASET, + ), + 'destinationTable': { + 'projectId': DWH_CURATED_PRJ, + 'datasetId': DWH_CURATED_BQ_DATASET, + 'tableId': 'customer_purchase' + }, + 'writeDisposition': + 'WRITE_TRUNCATE', + "useLegacySql": + False + } + }, + impersonation_chain=[TRF_SA_BQ]) + + confidential_customer_purchase = BigQueryInsertJobOperator( + task_id='bq_confidential_customer_purchase', + gcp_conn_id='bigquery_default', + project_id=TRF_PRJ, + location=BQ_LOCATION, + configuration={ + 'jobType': 'QUERY', + 'query': { + 'query': + """SELECT + c.id as customer_id, + p.id as purchase_id, + c.name as name, + c.surname as surname, + p.item as item, + p.price as price, + p.timestamp as timestamp + FROM `{dwh_0_prj}.{dwh_0_dataset}.customers` c + JOIN `{dwh_0_prj}.{dwh_0_dataset}.purchases` p ON c.id = p.customer_id + """.format( + dwh_0_prj=DWH_LAND_PRJ, + dwh_0_dataset=DWH_LAND_BQ_DATASET, + ), + 'destinationTable': { + 'projectId': DWH_CONFIDENTIAL_PRJ, + 'datasetId': DWH_CONFIDENTIAL_BQ_DATASET, + 'tableId': 'customer_purchase' + }, + 'writeDisposition': + 'WRITE_TRUNCATE', + "useLegacySql": + False + } + }, + impersonation_chain=[TRF_SA_BQ]) + + start >> [ + customers_import, purchases_import + ] >> join_customer_purchase >> confidential_customer_purchase >> end diff --git a/blueprints/data-solutions/data-platform-foundations/images/df_demo_pipeline.png b/blueprints/data-solutions/data-platform-foundations/images/df_demo_pipeline.png new file mode 100644 index 0000000000000000000000000000000000000000..541532b41d18c74fe6ef6724858be07ed5667b71 GIT binary patch literal 58976 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

}lX$$UfO8lX4}z{O;dL30t&5@_5~ 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 literal 0 HcmV?d00001 diff --git a/blueprints/data-solutions/data-platform-foundations/outputs.tf b/blueprints/data-solutions/data-platform-foundations/outputs.tf index 2394fe09..ae853da0 100644 --- a/blueprints/data-solutions/data-platform-foundations/outputs.tf +++ b/blueprints/data-solutions/data-platform-foundations/outputs.tf @@ -13,7 +13,6 @@ # limitations under the License. # tfdoc:file:description Output variables. - output "bigquery-datasets" { description = "BigQuery datasets." value = { @@ -30,13 +29,32 @@ output "demo_commands" { 01 = "gsutil -i ${module.drop-sa-cs-0.email} cp demo/data/*.csv gs://${module.drop-cs-0.name}" 02 = try("gsutil -i ${module.orch-sa-cmp-0.email} cp demo/data/*.j* gs://${module.orch-cs-0.name}", "Composer not deployed.") 03 = try("gsutil -i ${module.orch-sa-cmp-0.email} cp demo/*.py ${google_composer_environment.orch-cmp-0[0].config[0].dag_gcs_prefix}/", "Composer not deployed") - 04 = try("Open ${google_composer_environment.orch-cmp-0[0].config.0.airflow_uri} and run uploaded DAG.", "Composer not deployed") - 05 = <