はじめに
私は2年目のデータエンジニアです!
既存コードの修正から機能の作成に挑戦範囲を広げた際に、設定ファイルで躓いたポイントを自分の振り返りがてら書いていきます。
1. 起きたこと:設定ファイルを使っているのに「ハードコード」?
ある日レビュワーから「S3パスは、設定ファイルを使って」と言われました。
「なるほど!今後の機能拡張を考えると設定ファイルを用いて汎用的な処理にしないといけないのか!」
理解した気になって作成したコードが以下でした。
config.json(設定ファイル)
{
"bucket_name": "my-data-bucket",
"table_name": "target_table_name"
}
main.py(プログラム)
# 当時の私のコード
# バケット名やテーブル名はJSONから読み込み、パスを組み立てる
path = f"s3://{conf['bucket_name']}/{year}/{month}/{day}/{conf['table_name']}/"
これで、バケット名が環境ごとに変わっても、JSONを直せば対応できる。
しかも日付の部分も動的に作成しているので拡張性はバッチリ!
と思いきやこんなレビューコメントが
「S3パスを作成する部分は、ハードコードではなく設定ファイルを使うように言ったよね?」
「えっ、JSONから値を読み込んでいるのに、これもハードコードなの……?」
2. 調べてわかったこと:ハードコードの真の正体
なぜ「変数を使っているのにハードコード」と言われたのか。
調べていくうちに、私は「ハードコード」という言葉のより深い意味を理解しました。
「値」は外にあるが「構造」が固定されていた
私のコードの最大の問題点は、パスの組み立てルール(階層の順番や構成)がプログラム内に書き込まれていたことでした。
パスの組み立てルールに変化があった場合
- 階層が変わったら
- 「年/月/日」の順序を入れ替えたり、特定のフォルダを間に挟む必要が出た場合、プログラム(.pyファイル)自体を修正し、再テスト・再デプロイが必要になる
- 保存先が変わったら
- S3ではなく別のストレージに保存したくなった場合、s3:// という接頭辞から組み立てているロジックそのものが足かせになる
このように、「設定が変わるたびに、コードを書き換えないと対応できない状態」こそが、
ハードコードであると気づきました。
設定ファイル化で意識すべきこと
設定ファイルを使う際、本当に大事なのは「文字列の置き換え」をすることではなく、
「どこまでをプログラム(抽象)に任せ、どこからを設定ファイル(具体)に追い出すか」という境界線を意識することにあります。
今回のケースで言えば、プログラムの責務は「与えられたパスに対してデータを書き込むこと」に集中させるべきで、S3のパス構造そのものは設定ファイルが受け持つべき「具体的な値」でした。この境界線の引き方こそが、汎用的なツールを作る鍵となります。
3. どうすればよかったのか:パスを「抽象化」して管理する
今回はプロジェクトの修正範囲の影響を考慮し、既存のロジックを維持する判断をしましたが、今後のために「過去分を含めたバッチ処理で日付をループさせる」という要件に対して、本来あるべきだった理想の構成を検討しました。
理想の構成:パスの「テンプレート化」
日付のようにプログラム側で動的に制御したい部分は**プレースホルダー({year}など)**として設定ファイルに残し、それ以外の「バケット名」や「固定の階層構造」を完全に外に出す方法が最適でした。
config.json(理想の形)
{
"target_datasets": [
{
"table": "user_logs",
"path_template": "s3://{bucket}/raw/users/{year}/{month}/{day}/"
},
{
"table": "order_logs",
"path_template": "s3://{bucket}/standard/orders/v2/{year}/{month}/{day}/"
}
]
}
main.py(理想のロジック)
import os
import json
# 設定の読み込み
with open('config.json') as f:
conf = json.load(f)
# バケット名は環境変数(samconfig等)から取得
s3_bucket = os.environ.get('S3_BUCKET')
# 1. テーブルごとにループ
for dataset in conf['target_datasets']:
table_name = dataset['table']
template = dataset['path_template']
# 2. 日付ごとにループ(イテレーション)
for dt in date_list:
# 設定ファイルのテンプレートに値を流し込むformat()を使用するだけ
path = template.format(
bucket=s3_bucket,
table=table_name,
year=dt.strftime('%Y'),
month=dt.strftime('%m'),
day=dt.strftime('%d')
)
# 実行
process_data(table_name, path)
このように、「パスの構造(どのフォルダの下に置くか)」は設定ファイルで管理しつつ、「どの日付分を処理するか」という実行制御はプログラム側で柔軟に行う。
この役割分担が、ハードコードから脱却するための一つの解でした。
4. 学び:設定の分離は「未来の変更への準備」
この経験を通して得た学びを3つ挙げます。
1. ハードコードの真意と「具体」の切り出し
文字列を直接書かないことだけでなく、階層構造のような「具体的なルール」をコードが持ちすぎないことが大切だと知りました。
2. 境界線をレビュアーとすり合わせる
何を設定ファイルに追い出し、どこをプログラムで制御するのか。その「境界線」の引き方に唯一の正解はありません。だからこそ、設計の段階で「今回はここを抽象化したい」という意図をレビュアーとすり合わせることが、手戻りを防ぐ最善策だと学びました。
3. 設定ファイルの強みは、使用しているリソースが何か一目でわかること
1箇所に、具体的なリソースのパスなどを集めることで、
設定ファイルさえ見れば、使用しているS3バケットやフォルダが把握できます。
条件分岐をコードに散りばめないことで、全体像を把握しやすくなります
今回は時間の都合で全ての修正は行いませんでしたが、今回の学びを今後の実装に活かしていきます。
次は、設定ファイルを見ただけで処理の流れがイメージできるような、
そんな美しいパイプラインを作っていきたいです!
おまけ:AWS SAM を使用している場合の設定管理
今回のプロジェクトでは AWS SAM を使用しており、samconfig.toml という設定ファイルも存在していました。 当初は混乱しましたが、学んでいくうちに以下のような使い分けが理想だと分かりました。samconfig.toml(環境変数/インフラ設定)とconfig.json(アプリケーション設定/動作パラメータ)の使い分け
samconfig.toml(インフラ設定): バケット名など、AWSリソースに直接関係する「デプロイ環境(dev/prd)ごとに変わる値」を管理し、Lambdaの環境変数としてプログラムに渡す。
主に管理する内容: S3バケット名、メモリサイズ、タイムアウト値
config.json(アプリケーション設定): パスのテンプレート構造やテーブル名など、プログラムの挙動に関する「ロジックに近い設定」を管理する。
主に管理する内容: テーブル一覧、パスのテンプレート、リトライ回数
理想的には、バケット名すらも config.json に書くのではなく、os.environ['S3_BUCKET'] のように環境変数から取得するようにすれば、他のサービスへの移行なども簡単になりプログラムの汎用性はさらに高まるようです。