[アップデート] Amazon SageMaker Unified Studio でカスタムビジュアルトランスフォームが利用可能になったので試してみた
クラウド事業統括本部の石川です。Amazon SageMaker Unified Studio の Visual ETL で、自作のカスタムビジュアルトランスフォームを作成・共有・再利用できるようになりましたので、実際に試してみました。
Amazon SageMaker Unified Studio の Visual ETL では、これまであらかじめ用意されたトランスフォームを組み合わせてデータ変換フローを構築していました。今回のアップデートにより、JSON の設定ファイルと Python の実装ファイルを用意することで、独自のトランスフォームをトランスフォームライブラリに追加できるようになりました。
AWS のアナウンスでは、顧客の電話番号を標準化する変換、個人を特定できる情報(PII)をマスキングする変換、組織標準のデータ品質チェックを適用する変換がユースケースとして挙げられています。複数の ETL ジョブにまたがって同じロジックを再利用でき、重複した作業を減らし、結果の一貫性を保ちやすくなります。
カスタムビジュアルトランスフォームとは
カスタムビジュアルトランスフォームは、Visual ETL のトランスフォームノードをユーザー自身が定義する仕組みです。JSON config ファイルにトランスフォームの名前・説明・パラメータを定義し、Python ファイルに変換ロジックを実装します。この 2 つのファイルをプロジェクトの Amazon S3 共有フォルダ配下の transforms フォルダにアップロードすると、Visual ETL のトランスフォームライブラリに表示されます。
同じ考え方の機能は AWS Glue Studio に 2022 年から存在しており、公式ドキュメントには、同一アカウント内で AWS Glue にカスタムビジュアルトランスフォームを作成済みであれば、それらは Amazon SageMaker Unified Studio の Visual ETL でも利用できると記載されています。
やってみた
前提条件
- Amazon SageMaker Unified Studio のドメインとプロジェクトが作成済みであること
- 検証環境: ap-northeast-1、AWS CLI v2.36.2
ファイルの配置から Visual ETL 上での表示確認、生成されるスクリプトの確認、そして実際に ETL ジョブを実行して変換後のデータを確認するところまでを行いました。
Step 1. プロジェクトの Amazon S3 共有フォルダを確認する
公式ドキュメントでは、Amazon S3 のパスをプロジェクト設定の Storage 欄から確認する手順が案内されています。AWS CLI でも取得できましたので、こちらの方法を紹介します。
まずドメインの一覧を取得します。
% aws datazone list-domains \
--region ap-northeast-1 \
--query "items[].{id:id,name:name,status:status}" \
--output table
----------------------------------------------------------------
| ListDomains |
+---------------------+---------------------------+------------+
| id | name | status |
+---------------------+---------------------------+------------+
| dzd-d3w2uawk8vo7a1 | Default_05252026_Domain | AVAILABLE |
+---------------------+---------------------------+------------+
続いてプロジェクトと環境の ID を取得します。
% aws datazone list-projects \
--domain-identifier dzd-d3w2uawk8vo7a1 \
--region ap-northeast-1 \
--query "items[].{id:id,name:name}" \
--output table
--------------------------------------------------
| ListProjects |
+-----------------+------------------------------+
| id | name |
+-----------------+------------------------------+
| c778yzrdr7zy55 | admin-project-123456789012 |
+-----------------+------------------------------+
% aws datazone list-environments \
--domain-identifier dzd-d3w2uawk8vo7a1 \
--project-identifier c778yzrdr7zy55 \
--region ap-northeast-1 \
--query "items[].{id:id,name:name,status:status}" \
--output table
---------------------------------------------------------------------------------
| ListEnvironments |
+----------------+---------------------------------------------------+----------+
| id | name | status |
+----------------+---------------------------------------------------+----------+
| crcf629vztrvh5| AmazonSagemakerEnvironmentConfig-axosp8fvbr1dnd | ACTIVE |
| 4jxormz9fjcgpl| AmazonSagemakerEnvironmentConfig-602yfhroojlyop | ACTIVE |
+----------------+---------------------------------------------------+----------+
環境が 2 つ返ってきました。Amazon S3 共有フォルダのパスは s3BucketPath という名前で環境の詳細に含まれていますが、これを持っているのは片方だけです。どちらか分からないため、全環境を順に確認します。
% for E in $(aws datazone list-environments \
--domain-identifier dzd-d3w2uawk8vo7a1 \
--project-identifier c778yzrdr7zy55 \
--region ap-northeast-1 --query 'items[].id' --output text); do
echo -n "${E}: "
aws datazone get-environment \
--domain-identifier dzd-d3w2uawk8vo7a1 \
--identifier "$E" \
--region ap-northeast-1 \
--query "provisionedResources[?name=='s3BucketPath'].value" \
--output text
done
crcf629vztrvh5: s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared
4jxormz9fjcgpl:
crcf629vztrvh5 の方に共有フォルダのパスがありました。プロジェクトの基盤リソースを払い出すのがこのブループリントで、共有フォルダもここで作られます。バケット名は amazon-sagemaker-<アカウントID>-<リージョン>-<プロジェクトID> という命名規則で、その配下の shared が共有フォルダです。
この時点で transforms フォルダはまだ存在しませんでした。
% aws s3 ls s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared/ --recursive
2026-05-25 02:49:28 0 shared/
2026-05-25 02:49:28 424 shared/.libs.json
Step 2. JSON config ファイルを作成する
作成するファイルは、ローカルの transforms ディレクトリにまとめて置きます。Step 4 でこのディレクトリごとアップロードします。
% mkdir transforms
題材は、PII のマスキングにします。指定したカラムをマスキングするトランスフォームを、必須パラメータ 1 つと省略可能なパラメータ 2 つを持たせて定義しました。
transforms/mask_pii_column.json として保存します。
{
"name": "mask_pii_column",
"displayName": "Mask PII Column (Custom)",
"description": "Masks the values of the specified column. Optionally keeps the last N characters visible.",
"functionName": "mask_pii_column",
"parameters": [
{
"name": "colName",
"displayName": "Column name",
"type": "str",
"description": "Name of the column that holds the value to mask"
},
{
"name": "maskChar",
"displayName": "Mask character",
"type": "str",
"isOptional": true,
"description": "Character used for masking. Defaults to *"
},
{
"name": "keepLastN",
"displayName": "Keep last N characters",
"type": "int",
"isOptional": true,
"description": "Number of trailing characters to keep visible. Defaults to 0"
}
]
}
指定できるフィールドは以下のとおりです。
| フィールド | 必須 | 内容 |
|---|---|---|
name |
必須 | トランスフォームのシステム名。Python の命名規則に従う |
displayName |
任意 | Visual ETL エディタ上での表示名。省略時は name が使われる |
description |
任意 | Visual ETL 上に表示される説明。検索対象になる |
functionName |
必須 | 呼び出す Python 関数名 |
parameters |
任意 | パラメータオブジェクトの配列 |
parameters の各オブジェクトには name(必須)、displayName、type(必須、str / int / float / list / bool)、isOptional、description を指定します。
Step 3. Python 実装ファイルを作成する
JSON ファイルと同じベース名で Python ファイルを作成します。関数名は functionName と一致させる必要があります。
transforms/mask_pii_column.py として保存します。
from pyspark.sql import functions as F
def mask_pii_column(self, colName, maskChar="*", keepLastN=0):
"""指定したカラムの値をマスキングする。keepLastN を指定すると末尾 N 文字だけ残す。
Visual ETL は isOptional のパラメータを未入力のまま実行しても、引数を省略せず
空文字列を明示的に渡してくる。そのため関数定義のデフォルト引数は適用されない。
冒頭の 2 行で既定値へ読み替えている。
"""
maskChar = maskChar or "*"
keepLastN = int(keepLastN or 0)
masked_head = F.regexp_replace(
F.expr(f"left(`{colName}`, greatest(length(`{colName}`) - {keepLastN}, 0))"),
".",
maskChar,
)
tail = F.expr(f"right(`{colName}`, {keepLastN})")
return self.withColumn(colName, F.concat(masked_head, tail))
ここまでで、ローカルは以下の構成になります。独自のトランスフォームをトランスフォームライブラリを追加するためのJSON の設定ファイルと Python の実装ファイルを用意できました。
transforms/
├── mask_pii_column.json
└── mask_pii_column.py
Step 4. transforms フォルダにアップロードする
作成した 2 つのファイルを、共有フォルダ配下の transforms フォルダにアップロードします。transforms フォルダが存在しない場合は、アップロード時に自動で作成されます。
% aws s3 cp transforms/ \
s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared/transforms/ \
--recursive \
--exclude "*" --include "*.json" --include "*.py" \
--region ap-northeast-1
upload: transforms/mask_pii_column.json to s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared/transforms/mask_pii_column.json
upload: transforms/mask_pii_column.py to s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared/transforms/mask_pii_column.py
Step 5. サンプルデータを用意する
動作確認用に、電話番号を含む CSV を作成します。
customer_id,customer_name,phone_number,email
1001,Taro Yamada,090-1234-5678,taro.yamada@example.com
1002,Hanako Suzuki,080-9876-5432,hanako.suzuki@example.com
1003,Jiro Sato,070-1111-2222,jiro.sato@example.com
1004,Yuki Tanaka,090-3333-4444,yuki.tanaka@example.com
1005,Kenji Ito,080-5555-6666,kenji.ito@example.com
1006,Aoi Watanabe,070-7777-8888,aoi.watanabe@example.com
1007,Sora Nakamura,090-2468-1357,sora.nakamura@example.com
1008,Rin Kobayashi,080-1357-2468,rin.kobayashi@example.com
共有フォルダ配下にアップロードします。
% aws s3 cp sample-data/customers.csv \
s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared/sample-data/customers.csv \
--region ap-northeast-1
upload: sample-data/customers.csv to s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared/sample-data/customers.csv
Step 6. データソースを追加する
Amazon SageMaker Unified Studio のポータルにサインインし、左メニューから Visual ETL を開いて ジョブを作成 を選択します。

先にデータソースを置きます。Add(+)アイコンから Add nodes メニューを開き、Data sources タブで Amazon S3 を選択します。

トランスフォームから先に置くこともできますが、変換ノードは変換元となる親ノードを必要とするため、単体で配置するとデータプレビューがエラーになります。データソースから置く方が引っかかりがありません。
配置したノードを選択し、S3 URI に Step 5 でアップロードした CSV のパス(s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared/sample-data/customers.csv)を入力します。しばらくするとデータプレビューに中身が表示されました。

ヘッダー行が列名として認識され、customer_id は LONG として型が推論されています。
Step 7. トランスフォームを追加する
配置した Amazon S3 ノードを選択すると、ノードの右側に ⊕ アイコンが現れます。これをクリックすると、下流に追加するノードを選ぶメニューが開きます。

Transforms タブが選択された状態でメニューが開きます。ビルトインの Add current timestamp や Aggregate に混ざって、自作のカスタムトランスフォームが並んでいました。piiと入力すると、Mask PII Column (Custom)のみが表示されます。アップロード後に特別な操作は不要で、Visual ETL を開いた時点で反映されています。
マスクするカラム名phone_numberを指定します。

Step 8. データターゲットを追加する
先にデータターゲットを置きます。Add(+)アイコンから Add nodes メニューを開き、Data sources タブで Amazon S3 を選択します。

配置したノードを選択し、S3 URI に CSV の出力パス(s3://amazon-sagemaker-517444948157-ap-northeast-1-c778yzrdr7zy55/shared/output/masked-customers/)を入力します。

最終的には、以下のフローになります。

Step 9. 生成されるスクリプトを確認する
Visual ETL では、組み立てたフローから実行用の Python スクリプトが生成されます。画面右上の [保存] ボタンを押します。生成されたスクリプトのうち、カスタムトランスフォームに関連する部分を抜粋します。
import sys
from pyspark.context import SparkContext
from pyspark.sql import SparkSession
from mask_pii_column import mask_pii_column
from pyspark.sql.dataframe import DataFrame
import pyspark.sql.functions as F
sc = SparkContext.getOrCreate()
spark = SparkSession.builder.getOrCreate()
# Script generated for node S3DataSource
S3DataSource_1785076328528 = spark.read.format("csv") \
.option("inferschema", "true") \
.option("multiLine", "true") \
.option("header", "true") \
.option("recursiveFileLookup", "false") \
.option("sep", ",") \
.load("s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared/sample-data/customers.csv")
def is_blank_df(df):
# Indicates if the DataFrame has no schema and no rows.
return not df.schema.fieldNames() and not df.take(1)
def enrich_df(name, function):
def transform_df(self, *args, **kwargs):
if is_blank_df(self):
return self # No data to transform, return as is
return function(self, *args, **kwargs)
setattr(DataFrame, name, transform_df)
# Script generated for node CustomTransformTransform
enrich_df('mask_pii_column', mask_pii_column)
params_mask_pii_column = {"colName": "phone_number", "maskChar": "", "keepLastN": 0}
CustomTransformTransform_1785076404196 = S3DataSource_1785076328528.mask_pii_column(**params_mask_pii_column)
# Script generated for node S3DataSink
CustomTransformTransform_1785076404196.write.format("csv") \
.option("header", True) \
.mode("append") \
.save("s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared/output/masked-customers/")
Step 10. ジョブを実行して結果を確認する
画面右上の [ジュブの実行] ボタンを押して、ジョブを実行します。

ジョブは成功しました。出力されたデータを見てみます。
% aws s3 cp s3://amazon-sagemaker-123456789012-ap-northeast-1-c778yzrdr7zy55/shared/output/masked-customers-fixed/part-00000-xxxxxxxx-c000.csv - \
--region ap-northeast-1
customer_id,customer_name,phone_number,email
1001,Taro Yamada,*********5678,taro.yamada@example.com
1002,Hanako Suzuki,*********5432,hanako.suzuki@example.com
1003,Jiro Sato,*********2222,jiro.sato@example.com
1004,Yuki Tanaka,*********4444,yuki.tanaka@example.com
1005,Kenji Ito,*********6666,kenji.ito@example.com
1006,Aoi Watanabe,*********8888,aoi.watanabe@example.com
1007,Sora Nakamura,*********1357,sora.nakamura@example.com
1008,Rin Kobayashi,*********2468,rin.kobayashi@example.com
電話番号がマスキングされ、末尾 4 桁だけが残りました。自作したトランスフォームが期待どおりに動作しています。Mask character を未入力にしたにもかかわらず * でマスクされている点が、Step 3 のガードが働いた証拠です。
最後に
Amazon SageMaker Unified Studio の Visual ETL で、カスタムビジュアルトランスフォームを作成・共有・再利用できるようになりました。JSON config ファイルと Python 実装ファイルをプロジェクトの Amazon S3 共有フォルダの transforms フォルダに置くだけで、Visual ETL のトランスフォームライブラリに自作のノードが並びます。特別なデプロイ操作は不要でした。
Python を書けるデータエンジニアが変換ロジックを実装し、GUI で ETL を組み立てるメンバーはそれをノードとして選ぶだけ、という分担が可能になります。PII のマスキングやデータ品質チェックのように、実装のばらつきが後から効いてくる処理ほど効果が大きい機能です。
同様の処理を実現する手段としては、Visual ETL のビルトインである Custom code ノードや、ノートブックでの実装もあります。ただしそれらはジョブごとに実装が閉じてしまうため、組織で共通化したいロジックについては、今回のカスタムビジュアルトランスフォームが適していると考えられます。
既存の ETL ジョブの中で何度も実装している変換処理を 1 つ選び、transforms フォルダに置いてみるところから始めてみてはいかがでしょうか。この記事がどなたかのお役に立てば幸いです。








