paint-brush
フロントエンドアプリケーションリリースでの環境変数の操作@egorgrushin
新しい歴史

フロントエンドアプリケーションリリースでの環境変数の操作

Egor Grushin11m2024/10/30
Read on Terminal Reader

長すぎる; 読むには

フロントエンド アプリケーションでは、従来、環境変数がビルド時に埋め込まれるため、環境 (開発、ステージング、本番) ごとに個別のビルドが必要であり、リリース時間が長くなります。私は、フロントエンド アプリケーションを 1 回ビルドし、プレースホルダーとスクリプトを使用してデプロイ時に環境固有の変数を挿入し、デプロイ時に実際の値に置き換えるソリューションを提案します。この方法により、リリース プロセスが合理化され、ビルド時間が短縮され、環境間で一貫性が確保され、ビルド関連のバグのリスクが最小限に抑えられます。さらに、変数挿入後のファイル名ハッシュを管理することで、このアプローチでは効果的なブラウザー キャッシュと全体的なアプリケーション パフォーマンスが維持されます。
featured image - フロントエンドアプリケーションリリースでの環境変数の操作
Egor Grushin HackerNoon profile picture
0-item


数百万のフロントエンド アプリケーションが、環境固有のビルドを管理しています。開発、ステージング、本番環境など、環境ごとにフロントエンド アプリの個別のビルドを作成し、適切な環境変数を設定する必要があります。複数のアプリが関係する場合はビルドの数が増え、フラストレーションがたまります。これは長い間一般的な問題でしたが、環境変数を処理するよりよい方法があります。私はこのプロセスを効率化する方法を見つけました。この記事では、ビルド時間を短縮し、プロジェクトの環境間で一貫性を確保できる効率的なプロセスを作成する方法を段階的に説明します。


環境変数を理解する

始める前に、要約しておくべきだと思います。Web アプリケーションは、ほとんどの場合、 「環境変数」と呼ばれる変数に依存しており、これには内部システム エンドポイント、統合システム、支払いシステム キー、リリース番号などが含まれることがよくあります。当然、これらの変数の値は、アプリケーションが展開される環境によって異なります。


たとえば、支払いゲートウェイとやり取りするアプリケーションを想像してください。開発環境では、支払いゲートウェイの URL はテスト用のサンドボックス (https://sandbox.paymentgateway.com) を指し、実稼働環境ではライブ サービス (https://live.paymentgateway.com) を指している可能性があります。同様に、データ セキュリティを確保し、環境の混在を避けるために、環境ごとに異なる API キーまたはその他の環境固有の設定が使用されます。


フロントエンド開発の課題

バックエンド アプリケーションを構築する場合、これは問題になりません。これらの変数の値はバックエンドがデプロイされているサーバー環境に保存されるため、アプリケーション コードでこれらの変数を宣言するだけで十分です。このようにして、バックエンド アプリケーションは起動時にそれらにアクセスします。


ただし、フロントエンド アプリケーションの場合は、状況が多少複雑になります。フロントエンド アプリケーションはユーザーのブラウザーで実行されるため、特定の環境変数の値にアクセスできません。この問題に対処するために、通常、これらの変数の値はビルド時にフロントエンド アプリケーションに「組み込まれ」ます。このため、アプリケーションがユーザーのブラウザーで実行されるときに、必要な値はすべてフロントエンド アプリケーションにすでに埋め込まれています。


このアプローチには、他の多くのアプローチと同様に、注意点があります。各ビルドにそれぞれの値が含まれるように、環境ごとに同じフロントエンド アプリケーションの個別のビルドを作成する必要があります。


たとえば、次の 3 つの環境があるとします。

  • 内部テスト用の開発。

  • 統合テストの段階。

  • 顧客向けの生産。


作業をテストに提出するには、アプリをビルドして開発環境にデプロイします。内部テストが完了したら、アプリを再度ビルドしてステージにデプロイし、さらに再度ビルドして本番環境にデプロイする必要があります。プロジェクトに複数のフロントエンド アプリケーションが含まれている場合、このようなビルドの数が大幅に増加します。さらに、これらのビルド間でコードベースは変更されません。2 番目と 3 番目のビルドは同じソース コードに基づいています。


これらすべてにより、リリース プロセスは煩雑で、時間がかかり、コストがかかるだけでなく、品質保証のリスクも生じます。ビルドは開発環境で十分にテストされているかもしれませんが、ステージ ビルドは技術的に新しいため、新たなエラーが発生する可能性があります。


例:ビルド時間が X 秒と Y 秒のアプリケーションが 2 つあるとします。これら 3 つの環境では、両方のアプリケーションのビルド時間は3X + 3Yかかります。ただし、各アプリケーションを 1 回だけビルドし、すべての環境で同じビルドを使用できれば、合計時間はX + Y秒に短縮され、ビルド時間は 3 分の 1 に短縮されます。

これは、リソースが限られており、ビルド時間が数分から 1 時間を超えるフロントエンド パイプラインでは大きな違いを生む可能性があります。この問題は、世界中のほぼすべてのフロントエンド アプリケーションに存在しており、解決方法がないことがよくあります。しかし、これは特にビジネスの観点からは深刻な問題です。

3 つのビルドを別々に作成する代わりに、1 つのビルドを作成してすべての環境に展開できたらすばらしいと思いませんか? 実は、まさにそれを実現する方法を見つけました。


フロントエンドのデプロイメントの最適化。マニュアル

環境変数の設定


  1. まず、フロントエンド プロジェクトのリポジトリに必要な環境変数をリストするファイルを作成する必要があります。これらは開発者がローカルで使用するものです。通常、このファイルは.env.localと呼ばれ、ほとんどの最新のフロントエンド フレームワークで読み取ることができます。次に、このようなファイルの例を示します。


     CLIENT_ID='frontend-development' API_URL=/api/v1' PUBLIC_URL='/' COMMIT_SHA=''


    注: フレームワークによって、環境変数の命名規則が異なります。たとえば、React では、変数名の先頭にREACT_APP_付ける必要があります。このファイルには、アプリケーションに直接影響する変数を必ずしも含める必要はありません。また、役立つデバッグ情報を含めることもできますCOMMIT_SHA変数を追加しました。この変数は、後でビルド ジョブから取得して、このビルドのベースとなったコミットを追跡します。


  2. 次に、 environment.jsというファイルを作成し、必要な環境変数を定義します。フロントエンド フレームワークがこれらを挿入します。たとえば、React の場合、これらはprocess.envオブジェクトに保存されます。


     const ORIGIN_ENVIRONMENTS = window.ORIGIN_ENVIRONMENTS = { CLIENT_ID: process.env.CLIENT_ID, API_URL: process.env.API_URL, PUBLIC_URL: process.env.PUBLIC_URL, COMMIT_SHA: process.env.COMMIT_SHA }; export const ENVIRONMENT = { clientId: ORIGIN_ENVIRONMENTS.CLIENT_ID, apiUrl: ORIGIN_ENVIRONMENTS.API_URL, publicUrl: ORIGIN_ENVIRONMENTS.PUBLIC_URL ?? "/", commitSha: ORIGIN_ENVIRONMENTS.COMMIT_SHA, };


  1. ここでは、 window.ORIGIN_ENVIRONMENTSオブジェクト内の変数の初期値をすべて取得します。これにより、ブラウザのコンソールでそれらを表示できます。さらに、それらをENVIRONMENTオブジェクトにコピーする必要があります。ここで、いくつかのデフォルトを設定することもできます。たとえば、 publicUrlデフォルトで / であると想定します。アプリケーションでこれらの変数が必要なときはいつでも、 ENVIRONMENTオブジェクトを使用します。


    この段階では、ローカル開発に必要なすべての要件が満たされています。ただし、目標はさまざまな環境を処理することです。


  2. これを行うには、次の内容の.envファイルを作成します。

 CLIENT_ID='<client_id>' API_URL='<api_url>' PUBLIC_URL='<public_url>' COMMIT_SHA=$COMMIT_SHA

このファイルでは、環境に依存する変数のプレースホルダーを指定します。一意であり、ソースコードと重複しない限り、任意のものを使用できます。さらに確実にするために、以下を使用することもできます。 UUID – ユニバーサルユニーク識別子。


環境間で変化しない変数 (コミット ハッシュなど) については、実際の値を直接書き込むか、ビルド ジョブ中に使用できる値 ( $COMMIT_SHAなど) を使用できます。フロントエンド フレームワークは、ビルド プロセス中にこれらのプレースホルダーを実際の値に置き換えます。


ファイル
ファイル

  1. ここで、プレースホルダーの代わりに実際の値を入れることができます。これを行うには、まずプレースホルダーと変数名のマッピングを含むinject.pyファイルを作成します (ここでは Python を選択しましたが、この目的には任意のツールを使用できます)。


 replacement_map = { "<client_id>": "CLIENT_ID", "<api_url>": "API_URL", "<public_url>": "PUBLIC_URL", "%3Cpublic_url%3E": "PUBLIC_URL" }

public_urlが 2 回リストされており、2 番目のエントリにはエスケープされた括弧があることに注意してください。これは、CSS および HTML ファイルで使用されるすべての変数に必要です。


  1. 次に、変更するファイルのリストを追加しましょう (これは Nginx の例です)。


 base_path = 'usr/share/nginx/html' target_files = [ f'{base_path}/static/js/main.*.js', f'{base_path}/static/js/chunk.*.js', f'{base_path}/static/css/main.*.css', f'{base_path}/static/css/chunk.*.css', f'{base_path}/index.html' ]


  1. 次に、 injector.pyファイルを作成します。ここで、ビルド成果物ファイル (JS、HTML、CSS ファイルなど) のマッピングとリストを受け取り、プレースホルダーを現在の環境の変数の値に置き換えます。


 import os import glob def inject_envs(filename, replacement_map): with open(filename) as r: lines = r.read() for key, value in replacement_map.items(): lines = lines.replace(key, os.environ.get(value) or '') with open(filename, "w") as w: w.write(lines) def inject(target_files, replacement_map, base_path): for target_file in target_files: for filename in glob.glob(target_file.glob): inject_envs(filename, replacement_map)


次に、 inject.pyファイルに次の行を追加します ( injector.pyをインポートすることを忘れないでください)。

 injector.inject(target_files, replacement_map, base_path)


  1. ここで、 inject.pyスクリプトがデプロイメント中にのみ実行されるようにする必要があります。Python をインストールしてすべての成果物をコピーした後、 CMDコマンドでDockerfileにスクリプトを追加できます。
 RUN apk add python3 COPY nginx/default.conf /etc/nginx/conf.d/default.conf COPY --from=build /app/ci /ci COPY --from=build /app/build /usr/share/nginx/html CMD ["/bin/sh", "-c", "python3 ./ci/inject.py && nginx -g 'daemon off;'"]That's it! This way, during each deployment, the pre-built files will be used, with variables specific to the deployment environment injected into them.


これで完了です。このように、各デプロイメント中に、デプロイメント環境に固有の変数が挿入された、事前に構築されたファイルが使用されるようになります。


ファイル: ファイル


適切なブラウザキャッシュのためのファイル名ハッシュの処理

1 つ注意点があります。ビルド成果物のファイル名にコンテンツ ハッシュが含まれている場合、この挿入はファイル名に影響を与えず、ブラウザーのキャッシュに問題が発生する可能性があります。これを修正するには、挿入された変数を含むファイルを変更した後、次の操作を行う必要があります。


  1. 更新されたファイルの新しいハッシュを生成します。
  2. この新しいハッシュをファイル名に追加すると、ブラウザはそれらを新しいファイルとして扱います。
  3. コード内の古いファイル名への参照 (インポート ステートメントなど) を新しいファイル名と一致するように更新します。


これを実装するには、ハッシュ ライブラリのインポート ( import hashlib ) と次の関数をinject.pyファイルに追加します。


 def sha256sum(filename): h = hashlib.sha256() b = bytearray(128 * 1024) mv = memoryview(b) with open(filename, 'rb', buffering=0) as f: while n := f.readinto(mv): h.update(mv[:n]) return h.hexdigest() def replace_filename_imports(filename, new_filename, base_path): allowed_extensions = ('.html', '.js', '.css') for path, dirc, files in os.walk(base_path): for name in files: current_filename = os.path.join(path, name) if current_filename.endswith(allowed_extensions): with open(current_filename) as f: s = f.read() s = s.replace(filename, new_filename) with open(current_filename, "w") as f: f.write(s) def rename_file(fullfilename): dirname = os.path.dirname(fullfilename) filename, ext = os.path.splitext(os.path.basename(fullfilename)) digest = sha256sum(fullfilename) new_filename = f'{filename}.{digest[:8]}' new_fullfilename = f'{dirname}/{new_filename}{ext}' os.rename(fullfilename, new_fullfilename) return filename, new_filename


ただし、すべてのファイルの名前を変更する必要はありません。たとえば、 index.htmlファイル名は変更しない必要があります。これを実現するには、名前の変更が必要かどうかを示すフラグを格納するTargetFileクラスを作成します。


 class TargetFile: def __init__(self, glob, should_be_renamed = True): self.glob = glob self.should_be_renamed = should_be_renamed


ここで、 inject.py内のファイル パスの配列をTargetFileクラス オブジェクトの配列に置き換えるだけです。

 target_files = [ injector.TargetFile(f'{base_path}/static/js/main.*.js'), injector.TargetFile(f'{base_path}/static/js/chunk.*.js'), injector.TargetFile(f'{base_path}/static/css/main.*.css'), injector.TargetFile(f'{base_path}/static/css/chunk.*.css'), injector.TargetFile(f'{base_path}/index.html', False) ]


そして、フラグが設定されている場合、ファイルの名前を変更するようにinjector.pyinject関数を更新します。


 def inject(target_files, replacement_map, base_path): for target_file in target_files: for filename in glob.glob(target_file.glob): inject_envs(filename, replacement_map) if target_file.should_be_renamed: filename, new_filename = rename_file(filename) replace_filename_imports(filename, new_filename, base_path)


その結果、アーティファクト ファイルは、 <origin-file-name> . <injection-hash> . <extension>という命名形式に従います。


挿入前のファイル名:

挿入前のファイル名


挿入後のファイル名:挿入後のファイル名


同じ環境変数は同じファイル名を生成するため、ユーザーのブラウザはファイルを正しくキャッシュできます。これらの変数の正しい値がブラウザ キャッシュに保存されることが保証され、結果としてクライアントのパフォーマンスが向上します。


効率的な導入のためのソリューション

各環境ごとに個別のビルドを作成する従来のアプローチでは、いくつかの重大な非効率性が生じ、リソースが限られているチームにとっては問題となる可能性があります。


これで、長いデプロイメント時間、過剰なビルド、フロントエンド アプリケーションの品質保証におけるリスク増大など、すべてを解決できるリリース プロセスの青写真ができました。同時に、すべての環境で新しいレベルの一貫性保証を導入します。


N 個のビルドが必要になる代わりに、1 個だけ必要になります。今後のリリースでは、すでにテスト済みのビルドを展開するだけで済みます。これにより、すべての環境で同じビルドが使用されるため、潜在的なバグの問題を解決するのにも役立ちます。さらに、このスクリプトの実行速度は、最も最適化されたビルドと比べても比較にならないほど高速です。たとえば、MacBook 14 PRO、M1、32GB のローカル ベンチマークは次のとおりです。


私のアプローチは、リリース プロセスを簡素化し、効果的なキャッシュ戦略を可能にすることでアプリケーションのパフォーマンスを維持し、ビルド関連のバグが環境に侵入しないようにします。さらに、これまで面倒なビルド タスクに費やしていたすべての時間と労力を、さらに優れたユーザー エクスペリエンスの作成に集中できるようになりました。気に入らない点はありません。


ビルド関連のバグが他の環境のアプリに紛れ込まないようにします。ビルド システムの不完全性により、幻のバグが発生する場合があります。可能性は低いですが、実際に存在します。