【AWS CDK】既存のASLを有効活用したStep FunctionsのCDK実装 ~ASLパラメータ置換編~

## はじめに
Step Functionsを実装する時どうされていますでしょうか？
私は、検討しながら、試しながらマネージメントコンソールからポチポチと作ってしまうのが大半です。
このポチポチで作るとASL(Amazon States Language)で出力することができるので、正式な環境を作る時うまい具合に流用したいのですが、出力されたASLだと連携するリソースのARN等がハードコーディングされており、リファクタリングしないと流用できないです。

また、正式な環境を作る時は、IaCを活用したいので、CDKでStep Functionsを実装する場合、連携するリソースも併せて実装する都合上普通なら作成したASLを元にCDK L2 Constructsで検討し直すアプローチだと思います。
[CDK Constructsに関して](https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/constructs.html)

個人的に、ポチポチで作ったベースがあるのにがっつり流用できないのはもったいないなと思ってしまいました。
ここから、ポチポチで作ったASLを元にASLのベーステンプレート手動で作成し、そのベーステンプレートからパラメータを置換する処理をCDKに組み込めば、作ったASLを有効活用できると思いつき、作成してみましたので、ご紹介させて頂きます。

本内容はフォーカスするポイントが2つあるので、2つの記事に分割させて頂きます。
- ASLパラメータ置換編
既存ASLからベーステンプレートを作成し、そのベーステンプレートからパラメータを置換するTypeScriptの処理のサンプルコード
- ASL読み込み/実装編
「ASLパラメータ置換編」で作成した処理をCDKに組み込み、CDK Deployに合わせて置換させる力技CDK

本記事では、ASLパラメータ置換に関してご紹介させて頂きます。
※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。

## サンプルコード概要
ベーステンプレートからパラメータを置換するTypeScriptの処理のサンプルコードは以下6つのセクションで構成されています。
- 置換対応list
- ASLファイルの読み込みを行う関数
- 文字列の置換を行う関数
- 文字列置換が完了したASLファイルを作成する関数
- 新しいASLファイルとして書き込みを行う関数
- 上記関数を実行する箇所

それぞれのセクションで分けてご紹介させて頂きます。

## 置換対応list
~~~typescript:asl-replace-sample.ts
export interface asllist {
    source: string;
    value: string;
};
const replacementList: asllist[] = [
    {source: '$myFunction', value: 'hogehoge1'},
    {source: 'myTopic', value: 'hogehoge2'},
    
];
~~~~
ここでは、置換対応表として用いるreplacementListという配列の定義しています。
パーツに分解し、ご紹介させて頂きます

**interface asllist**
asllistという名前でinterface定義をしています。asllistはsource、valueという2つのプロパティを持つオブジェクトです。
source プロパティは、置換対象となる文字列です。
value プロパティは、置換対象の置換後の文字列です。

**replacementList: asllist[]**
asllistの要素を持つ配列です。
置換対象文字列と、置換対象の置換後の文字列のセットです。


この定義が、実処理で文字列置換を行う際の元ネタとなります。


## ASLファイルの読み込みを行う関数
~~~typescript:asl-replace-sample.ts
// ASLファイルの読み込みを行う関数
function readJSONFile(path: string): Promise<any> {
   return new Promise((resolve, reject) => {
       fs.readFile(path, 'utf8', (err, data) => {
           if (err) {
               reject(err);
           } else {
               try {
                   const json = JSON.parse(data);
                   resolve(json);
               } catch (parseErr) {
                   reject(parseErr);
               }
           }
       });
   });
}
~~~
ここでは、指定されたパスのJSONファイルを非同期に読み込む関数を定義しています。
パーツに分解し、ご紹介させて頂きます。

**readJSONFile(path: string): Promise<any> {**
引数として 文字列でソースとなるASLの配置pathを受け取ります。この関数は、Promiseを返す非同期の処理を行い、resolveかrejectが返却されます。
なぜPromise型かは、「上記関数を実行する箇所」のパーツに関わってきます。

**return new Promise((resolve, reject) =>  {**
Promiseインスタンスを作成し、その中で非同期処理を実行しています。非同期処理完了後、結果をresolveかreject に渡します。

**fs.readFile(path, 'utf8', (err, data) => {**
Node.jsのfsモジュールのreadFile関数を利用しています。指定されたPathのファイルをutf8形式でテキストとして読み込んでいます。

**if (err) {**
ファイルの読み込みに失敗した場合、エラーオブジェクトをreject() に渡してPromiseを拒否します。

**const json = JSON.parse(data);**
ファイルの読み込みに成功した場合、JSON.parse() を使用してデータをJSON形式に解析します。解析結果のJSONデータを resolve() に渡してPromiseを解決します。

**catch (parseErr) {**
JSONの解析中にエラーが発生した場合、エラーオブジェクトをreject() に渡してPromiseを拒否します。

つまり、この関数は指定されたパスのJSONファイルを読み込んで、解析されたJSONデータをPromiseとして返します。

## 文字列の置換を行う関数
~~~typescript:asl-replace-sample.ts
// 文字列内での置換を行う関数
function replaceString(str: string, replacementList: asllist[]): string {
  let replacedStr = str;
  replacementList.forEach((replacement) => {
    replacedStr = replacedStr.replace(replacement.source, replacement.value);
  });
  return replacedStr;
}
~~~
ここでは、文字列内での置換を行うための関数を定義しています。
パーツに分解し、ご紹介させて頂きます。

**replaceString(str: string, replacementList: asllist[]): string**
引数として、置換対象を含む文字列と置換のリストreplacementListを受け取ります。
戻り値として置換後の文字列を返します。

**let replacedStr = str;**
replacedStrの初期値に引数で渡された文字列strを入れています。

**replacementList.forEach((replacement) => {**
forEach メソッドを使ってreplacementList 配列の各要素に対して処理を行います。

**replacedStr = replacedStr.replace(replacement.source, replacement.value);**
replace() Methodを用いて、置換対象となる文字列とマッチすると置換対象の置換後の文字列に置換処理を行います。

**return replacedStr;**
置換対象となる文字列とマッチする場合は、置換され、文字列を返します。

つまり、この関数では渡された文字列を置換対応listに従い置換し、結果の文字列を返します。
この関数は、「文字列置換が完了したASLファイルを作成する関数」内で呼ばれます。

## 文字列置換が完了したASLファイルを作成する関数
~~~typescript:asl-replace-sample.ts
// JSON内の文字列を置換する関数
function replaceStringValues(json: any, replacementList: asllist[]): any {
  if (typeof json === 'object') {
    if (Array.isArray(json)) {
      return json.map((item) => replaceStringValues(item, replacementList));
    }
    const replacedJson: any = {};
    for (const prop in json) {
      if (json.hasOwnProperty(prop)) {
        const value = json[prop];
        if (typeof value === 'string') {
          replacedJson[prop] = replaceString(value, replacementList);
        } else if (typeof value === 'object') {
          replacedJson[prop] = replaceStringValues(value, replacementList);
        } else {
          replacedJson[prop] = value;
        }
      }
    }
    return replacedJson;
  }
  return json;
}
~~~
ここでは、JSONオブジェクト内の文字列を置換するための関数を定義しています。
実際の置換処理では、「文字列の置換を行う関数」と連携します。

パーツに分解し、ご紹介させて頂きます。

**replaceStringValues(json: any, replacementList: asllist[]): any**
引数として置換処理を行いたいJSONオブジェクトと置換対応Listを受け取ります。
戻り値として置換後のJSONオブジェクトを返します。

**if (typeof json === 'object') {**
引数のJSONオブジェクトがオブジェクト型かどうかを確認し、オブジェクト型の場合は後段の処理に繋げます。
JSONオブジェクトがオブジェクト型でない場合、そのままJSONを返します。
**if (Array.isArray(json)) {**
引数のJSONオブジェクトが配列であるかどうかをチェックし、配列の場合は後段の処理に繋げます。

**return json.map((item) => replaceStringValues(item, replacementList));**
置換したいのは、JSONのvalueなので、配列の場合はまだ分解する必要があります。そのため、map()  Methodを使用して、配列の各要素に対して再帰的replaceStringValues関数を実行します。

**const replacedJson: any = {**
置換後のJSONオブジェクトを格納するための変数を用意します。

**for (const prop in json) {**
JSONオブジェクト内の各プロパティに対してループ処理させます。

**if (json.hasOwnProperty(prop)) {**
JSONオブジェクトが抽出したプロパティを持っているかを確認し、持つ場合は後段の処理に繋げます。

**const value = json[prop];**
プロパティ名をKeyにvalueを取得し、変数valueに格納します。

**if (typeof value === 'string') {**
valueが文字列の場合、文字列単位で分解が出来たので、「文字列の置換を行う関数」を使用して文字列置換処理を行い、結果をreplacedJsonに格納します。

**else if (typeof value === 'object') {**
valueがオブジェクトである場合、まだ分解する必要があります。そのため、再帰的に replaceStringValues関数を実行します。

**else {**
上記の条件に当てはまらない場合、元の値をそのままreplacedJsonに格納します。

**return replacedJson;**
JSONの全要素に対して、処理が全て完了後、置換されたJSONオブジェクトであるreplacedJsonを返します。

つまり、この関数ではJSONオブジェクト内の文字列を再帰的に置換して、最終的なJSONオブジェクトを形成しています。
JSONオブジェクト内の値の型によって、テキスト置換が行われ、オブジェクトの場合はまだ分解する必要があるので、再帰的に処理が実行されます。
再帰的な処理が含まれているため、入れ子になったJSON構造に対しても置換処理を実行することができます。

### 新しいASLファイルとして書き込みを行う関数
~~~typescript:asl-replace-sample.ts
// JSONファイルの書き込み関数
function writeJSONFile(path: string, data: any): Promise<void> {
  return new Promise((resolve, reject) => {
    fs.writeFile(path, JSON.stringify(data, null, 2), 'utf-8', (err) => {
      if (err) reject(err);
      resolve();
    });
  });
}
~~~
ここでは、JSONファイルを書き込むための関数を定義しています。
実際の置換処理では、「ASLファイル内の文字列を置換する箇所を探す関数」と連携します。

パーツに分解し、ご紹介させて頂きます。

**writeJSONFile(path: string, data: any): Promise<void>**
引数として、置換後のASLファイルを書き込む先となるpath、書き込むための元データを受け取ります。
また、戻り値としてPromiseを返します。
なぜPromise型かは、「上記関数を実行する箇所」のパーツに関わってきます。

**return new Promise((resolve, reject) => {**
Promiseインスタンスを作成し、その中で非同期処理を実行しています。非同期処理完了後、結果をresolveかreject に渡します。

**fs.writeFile(path, JSON.stringify(data, null, 2), 'utf-8', (err) => {**
Node.jsのfsモジュールのwriteFile関数を利用しています。
第二引数にnullを指定し、replacerを使用せずそのまま、第三引数に2を指定し、インデントは2スペースでファイルを第一引数で指定されたpathにutf8形式のJSONで書き出します。
書き出しが正常に完了するとresolve()を呼び、Promiseを解決します。

**if (err) reject(err)**
エラーオブジェクトをreject() に渡してPromiseを拒否します。

つまり、この関数は指定されたパスにJSONデータを非同期で書き込みます。
エラーハンドリングは、promiseのresolveか、reject()かで実施します。

## 上記関数を実行する箇所
// JSONファイル読み込みと置換処理の実行
~~~typescript:asl-replace-sample.ts
readJSONFile(inputPath)
  .then((data) => {
    const replacedData = replaceStringValues(data, replacementList);
    return writeJSONFile(outputPath, replacedData);
  })
  .then(() => {
    console.log('置換されたJSONファイルが生成されました。');
  })
  .catch((error) => {
    console.error('エラーが発生しました:', error);
  });
~~~
ここでは、メインの処理となり、上記関数を呼び出しています。

パーツに分解し、ご紹介させて頂きます。

**readJSONFile(inputPath)**
置換元となるJSONファイルを非同期で読み込むためreadJSONFileを呼び出しています。
この関数はPromiseを返します。なので、後段でthen() Methodを利用します。

**.then((data) => {**
読み込まれたJSONファイルはdataという変数で渡されます。

**const replacedData = replaceStringValues(data, replacementList);:**
replaceStringValues関数を呼び出し、読み込まれたデータに対して文字列置換を行います。
また、結果をreplacedDataという変数に格納します。

**return writeJSONFile(outputPath, replacedData);:**
writeJSONFile関数を呼び出し、置換されたデータを出力JSONファイルに非同期で書き込みます。
この関数はPromiseを返します。なので、後段でthen() Methodを利用します

**.then(() => {**
置換結果として新しいJSONファイルの書き出しが成功した場合、指定されたメッセージがコンソールに出力します。

**.catch((error) => {**
どこかしらで処理が失敗すると、エラーメッセージがコンソールに出力されます。

つまり、ここでは置換元JSONファイルの読み込み、文字列置換、置換結果として新しいJSONファイルの書き出しを順番に実行します。
各処理は、Promiseのチェーンを通じて行います。

## まとめ
ASLパラメータ置換編は以上になります。
ASL等別のツール等で作成したものをベーステンプレート化し、置換しつつIaCに組み込みたいといったニーズはあるかと思われます。
その際に、ご紹介させて頂いた処理を参考にして頂けると幸いです。

以下のリンクの記事にて、この処理を組み込んだ力業CDKの作りをご紹介させて頂いておりますので、ご興味を持って頂けましたら、合わせてご参照ください。
[【AWS CDK】既存のASLを有効活用したStep FunctionsのCDK実装 ~ASL読み込み/実装編~](https://qiita.com/akwayne/items/cf203dde93a976565135)

※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。