Page MenuHomePhabricator

Implement an Airflow operator for moving data from point A to B
Open, HighPublic

Description

In this task, we want to implement a new Airflow operator for moving data from point A to B.

The first use case will be to move data from HDFS to the clouddumps servers.

Something like:

publish = PublishData(
  source = "hdfs://wmf/data/archives/file_export",
  targets = ["sftp://clouddumps1001", "sftp://clouddumps1002"]
)

Context:

We need to add the step that syncs from hdfs to public. Not sure how that should be done.

In HDFS, we have folder /wmf/data/archive where you can move your files to. Let's say you do it to /wmf/data/archive/cirrus-search-index/{date}/blah.

Then you can set an hdfs_tools::hdfs_rsync_job in puppet to rsync from that HDFS path to the clouddumps* nodes that serve the dumps (examples here).

I'd say that there are some other options to be considered, too. That puppet based mechanism that calls hdfs-rsync will work, but it's maybe a bit of a legacy way to do it.
When we migrated the dumps v1 to Airflow recently, we needed to find a way to publish from the CephFS mount point /mnt/dumpsdata to the clouddumps hosts.

We created a sync-utils container image and we then add specific tasks into our DAG that is responsible for publishing the files created.
In the case of the dumps, we found that the best option was to use parallel-rsync and specify both clouddumps1001 and clouddumps1002 as the targets.
This allows us to have one task that either successfully publishes to both target locations, or it fails.

So for example, if you look at the current cirrussearch dumps, you will see that they have a sync_cirrussearch_dumps task, which calls parallel-rsync with these custom arguments.

image.png (1,902×577 px, 149 KB)

There are many options around how you would schedule and trigger these publishing tasks, so they don't all need to be sequential like the example shown here.

However, for this requirement it would be a little different, because the source files are presumably going to be created on HDFS, rather than CephFS.
This means that we won't be able to have the source directory mounted as a locally available file system.

I think that we have at least a few ways that we could tackle this, though.
One that occurs to me immediately is that we could use rclone instead of parallel-rsync

rclone already has an hdfs remote capability built-in, so we could use this for one side of the connection and an sftp remote on the other side.
We would be able to give it access to the kerberos credential cache and Hadoop configuration files for the HDFS connection, and the SSH private key for the SFTP connection.

Then we could just execute an rclone sync command and supply the source and destination paths.

@xcollazo - I can see this being a good option for T384381: Airflow jobs to do monthly XML dumps as well.

Details

Related Changes in Gerrit:
Related Changes in GitLab:
TitleReferenceAuthorSource BranchDest Branch
Draft: Run rclone in the task worker pod, not a KPOrepos/data-engineering/airflow-dags!2502aquT405360_rclone_operator_usageT405360_rclone_operator_instrumentation_2
Move rclone blubber image to airflow reporepos/data-engineering/airflow-dags!2499aquT405360_rclone_operator_switch_deploy_strategymain
Draft: Add stream_refine_to_iceberg_hourly DAGrepos/data-engineering/airflow-dags!2469aquT405360_rclone_operator_instrumentation_refineT405360_rclone_operator_instrumentation_2
Add rclone job instrumentationrepos/data-engineering/airflow-dags!2420aquT405360_rclone_operator_instrumentation_2main
Add analytics/airflow/job_stats event schemarepos/data-engineering/schemas-event-secondary!129aquT405360_general_rclone_operatormaster
Draft: General rclone operator POCrepos/data-engineering/airflow-dags!2367aquT405360_general_rclone_operatorT427457_gcs_to_hdfs_rclone_operator
Customize query in GitLab

Related Objects

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
Ottomata added a subscriber: Htriedman.

cc @Htriedman I think there may be an WME use case here?

@Ottomata Yes correct! I'm not 100% sure on the details but likely some kind of S3 bucket sync to WME's infra

We discussed this work on today's DPE SRE / DE sync up.

T384382: Production-level file export (aka dump) of MW Content in XML, for which we wanted to use this as a first use case, will be shipping soon. However, DPE SRE folks are committed with other work.

Thus we have decided to not block T384382 on this new mechanism, as we have an existing puppet-based mechanism that we can use for the time being.

Once DPE SRE folks have more time we can explore this idea.

As an experiment, I'm going to build a version of sync-utils that has support for hdfs-fuse mounts.

I think that this might be a quicker win than getting rclone to work, because if we can get a FUSE mount working for HDFS then we can reuse all of the parallel-rsync stuff that we already have in place for dumps_v1.

I'm just flagging here an investigation that I looked at as part of T405360.
In T402943#11297764 we can see that we currently use hdfs-rsync with an NFS source (clouddumps1002) and an HDFS target.

Each of these system timers running on an-launcher1003 is importing files that are generated using the dumps_v1 mechanism into HDFS.
There's a massive inefficiency here, because the files are already available on a cephfs volume, before being published to the clouddumps100[1-2] servers using rsync over ssh.

The systemd timer is then pulling them from the clouddumps servers over NFS, via an-launcher1003 and then writing them to HDFS.

We could skip the middle-man and have either a DAG or a task that simply copies the completed dumps from cephfs to HDFS.

Adding another use case: At SDS we decided that we'd use FR Tech's airflow instance to push data from P&T's data lake to FR Tech's data lake (we use minio- s3 compatible api)

Hey you know, Bento (formerly Benthos/RedPanda Connect) has an HDFS source connector. Just a thought!

Change #1319089 had a related patch set uploaded (by Aqu; author: Aqu):

[operations/mediawiki-config@master] Add airflow.job_stats event stream

https://gerrit.wikimedia.org/r/1319089

Change #1319887 had a related patch set uploaded (by Aqu; author: Aqu):

[analytics/refinery/source@master] Add stream refine to Iceberg job (Spark 3.5)

https://gerrit.wikimedia.org/r/1319887

Change #1319089 abandoned by Aqu:

[operations/mediawiki-config@master] Add airflow.job_stats event stream

Reason:

We are going to switch to opentelemetry at some point.
Waiting for the new model.

https://gerrit.wikimedia.org/r/1319089

Gehel subscribed.

Moved to "Waiting", Data-Engineering is working on implementation.

Change #1326290 had a related patch set uploaded (by Aqu; author: Aqu):

[operations/puppet@production] hadoop/spark35: Disable SQL options redaction regex

https://gerrit.wikimedia.org/r/1326290

While converting our existing sync tasks to the new rclone operator,
two design decisions came up that deserve a wider, documented discussion rather than
being settled inside an MR thread.

There are two related-but-separable axes:

1. KPO vs. task-pod execution

KPO (status quo, e.g. sync-utils image):

  • Extra layer of runtime isolation — the transfer (and its outbound internet access) happens in a separate pod from the Airflow worker.
  • No image variant to maintain in airflow-dags; decoupled from our Airflow release cadence.
  • Secrets that aren't a k8s Secret must be templated into the pod spec, which relies on Airflow's obfuscation mechanism and the user understanding its limits (see axis 2).
  • In dev, injecting ad-hoc credentials means bespoke env wiring, which complicates the code and moves the dev path further from prod.

Task-pod / in-process (proposed):

  • rclone runs in the worker; Python sets up the env vars, reading a connection or a k8s Secret indistinctly at runtime.
  • No templating — Airflow queries the connection at runtime to fill env vars, so there's no leak surface on the operator side by design.
  • Dev can use a connection, exactly like prod.
  • Cost: we drop the KPO isolation layer, and we maintain an rclone image variant built on top of the prod Airflow image (baked into the airflow-dags CI so it rebuilds automatically on each Airflow upgrade).

2. Airflow connection vs. k8s Secret

Both are managed in operations/deployment-charts in production — the difference is in
*how* the value reaches rclone, and whether it can leak into logs / rendered task fields.

Here's how each credential type behaves depending on the execution model:

CredentialPassed to KPO as env varSet in the Python task (in-process)
k8s Secret✅ safe — secretKeyRef, resolved by the kubelet, never templated✅ safe — secretKeyRef into the task pod, never templated
Connection / password (plain text)✅ masked — Airflow obfuscation matches & hides it✅ safe — never templated
Connection / password (JSON, e.g. GCP)❌ leaks — escaping changes the byte sequence, so obfuscation silently fails✅ safe — never templated
Connection / login (e.g. S3 key id)❌ leaks — the login field isn't obfuscated at all✅ safe — never templated

Two concrete cases we hit:

  • GCP (GSC use case): the credential is a JSON blob with a private key. Passed through a KPO pod spec it needs escaping, which changes the character sequence — and masking matches the exact sequence, so it silently fails to mask. We deliberately used a k8s Secret there for obfuscation.
  • S3 (fr-tech / minio use case): the access key id lives in the connection login field, which Airflow doesn't obfuscate at all.

Another fact: in operations/deployment/charts the mainstream approach to secret management in Airflow instances is to use connections (~25).

My inclination is to support both credential sources and run in the task pod, valuing
flexibility (and closing the leak surface by design) over the KPO isolation layer for this
operator. But I'd like input before we lock it in.

Thoughts welcome.

Thanks Antoine! Qs:

❌ leaks — the login field isn't obfuscated at all

IIUC, this could be obfuscated via config

My inclination is to support both credential sources and run in the task pod, valuing flexibility

In the MR, I noted I was more worried about the precedent this sets than this specific solution. If we go this route, then our solution for any operator that needs secrets to be set in a non-obfuscatable field is to bake the job dependencies into an airflow sub-image and run the 'job logic' in the airflow task python process.

The option to put the secrets into k8s-secrets, even if more cumbersome, is a solution to the non-obfuscatable problem. But as you noted, there may be implications for how this would work in dev envs.

So the options we are weighing are k8s-secrets vs non-obfuscatable operators must run in airflow task process, ya? Agree, let's see what DPE-SRE thinks.

Task-pod / in-process (proposed):

  • Cost: we drop the KPO isolation layer, and we maintain an rclone image variant built on top of the prod Airflow image (baked into the airflow-dags CI so it rebuilds automatically on each Airflow upgrade).

Is it actually worth maintaining a separate airflow image for this single tool addition? Should we just add it to the base image?
I suppose we could consider it unnecessarily risky from a security perspective to have rclone installed in every airflow image, so that could be considered a benefit to doing it.

I suppose we could consider it unnecessarily risky from a security perspective to have rclone installed in every airflow image, so that could be considered a benefit to doing it.

My worry isn't so much about this single rclone use case, it is the precedent that this solution sets. "How do we solve non-obfuscatable problem?" -> always run in airflow task python process. Each time we do this we will add more job dependencies to the airflow (sub?)image and require that jobs be run within airflow tasks.

Each time we do this we will add more job dependencies to the airflow (sub?)image and require that jobs be run within airflow tasks.

+1 to not require that jobs be run within airflow. I'd rather pay the dev cost of more complicated testing if this means independence between workload and Airflow.

In dev, injecting ad-hoc credentials means bespoke env wiring, which complicates the code and moves the dev path further from prod.

True, but we can add sensible dev defaults, and we can document use of airflow-devenv to allow this kind of testing. AFAIK, we can indeed inject k8s configs today with the --extra-values flag :

$ airflow-devenv create --help
...
--extra-values FILE             Additional values file, relative from /srv/deployment-
                                  charts/helmfile.d/dse-k8s-services/_airflow_common_ or absolute
...

CC @brouberol

Change #1319887 abandoned by Aqu:

[analytics/refinery/source@master] Add stream refine to Iceberg job (Spark 3.5)

Reason:

Tech decision needed to replace Gobblin

https://gerrit.wikimedia.org/r/1319887