diff --git a/.gitignore b/.gitignore index 579e4fb8..e4e7feaa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,11 @@ node_modules/ .env # pythonのキャッシュファイル __pycache__/ + +# StepFunctionsステートメント定義変換後のフォルダ +stepfunctions/*/build **/.vscode/settings.json # python test .coverage -.report/ \ No newline at end of file +.report/ diff --git a/stepfunctions/README.md b/stepfunctions/README.md new file mode 100644 index 00000000..361689c8 --- /dev/null +++ b/stepfunctions/README.md @@ -0,0 +1,57 @@ +# AWS StepFunctions + +## 概要 + +- MeDaCa基板上でバッチ処理を行うStepFunctionsステートマシン定義を格納する +- ステートマシン定義を格納する際のフォルダ構成は以下とする + - `ステートマシン名/ステートマシン名.json` +- Git管理するファイルは雛形とし、実際の環境にデプロイする定義は`/TOOLS`フォルダ内のツールで生成したものを使用する +- ステートマシン定義の雛形内の環境固有の値は`#{値を表すキー名}`の形式で記述すること + +## バージョン情報 + +- Python3.9.x + +## フォルダ構成 + +```text +. +├── TOOLS -- 環境ごとのステートマシン定義生成ツール置き場 +│   ├── convert.conf.yaml -- ステートマシンの変換定義ファイル +│   ├── convert_definition.py -- 定義変換ツールの本体 +└── r-crm-datafetch-state -- CRMデータ取得処理のステートマシン定義置き場 + ├── build -- 【Git管理対象外】TOOLSによって生成され、環境名のフォルダに、環境ごとの値に置き換えたステートマシン定義を配置する + │   ├── product + │   └── staging + └── r-crm-datafetch-state.json -- CRMデータ取得処理のステートマシン定義 +``` + +## ツールの利用方法 + +### 事前準備 + +- `/TOOLS`フォルダに移動し、以下のコマンドを実行して、Pythonの仮想環境を作成する + +```sh +cd ./TOOLS +# 仮想環境を作成 +python -m venv ./.venv +# 仮想環境を有効化 +# Mac/Linuxの場合 +source ./.venv/bin/activate +# Windowsの場合 +.\.venv\Scripts\activate +``` + +- 以下のコマンドを実行し、YAMLのパッケージをインストールする + +```sh +pip install PyYAML +``` + +### 起動方法 + +```sh +cd ./TOOLS +python convert_definition.py r-crm-datafetch-state staging +``` diff --git a/stepfunctions/TOOLS/convert_config.yaml b/stepfunctions/TOOLS/convert_config.yaml new file mode 100644 index 00000000..2b6487ea --- /dev/null +++ b/stepfunctions/TOOLS/convert_config.yaml @@ -0,0 +1,64 @@ +# 構成 +# config: ルートとなる要素 +# <ステートマシン名>: ステートマシン定義名 +# <環境名>: stagingかproductのみ +# <ステートマシンの雛形内のプレースホルダー名>:置き換え後の値を設定する +resource: + # 共通定義 + common: + # AWSアカウントID + - &AWS_ACCOUNT_ID "826466435614" + # 東京リージョン + - ®ION_AP_NORTHEAST_1 "ap-northeast-1" + # ステージング環境 + staging: + # サブネット(PrivateSubnet1) + - &STG_SUBNET_PRI_1A "subnet-0a47b12f6899ab19e" + # サブネット(PrivateSubnet2) + - &STG_SUBNET_PRI_1D "subnet-0ecb92c12eb49ebc3" + # セキュリティグループ(ecs-all) + - &STG_SG_ECS_ALL "sg-051e0fb9925539592" + # セキュリティグループ(ecs-crm-datafetch) + - &STG_SG_CRM_DATAFETCH "sg-0b20b7bb1cb1ab886" + # 本番環境 + product: + # サブネット(PrivateSubnet1) + - &PRD_SUBNET_PRI_1A "subnet-0d9bf8cd421cf2489" + # サブネット(PrivateSubnet2) + - &PRD_SUBNET_PRI_1D "subnet-0595f52cf6fd9b9e7" + # セキュリティグループ(ecs-all) + - &PRD_SG_ECS_ALL "sg-05df4823fc789b0fa" + # セキュリティグループ(ecs-crm-datafetch) + # TODO: 本番環境のセキュリティグループを作成したら下記のIDを置き換える + - &PRD_SG_CRM_DATAFETCH "sg-XXXXXXXXXXXXXXXXX" + +config: + r-crm-datafetch-state: + # ステージング環境 + staging: + # AWSアカウントID + AWS_ACCOUNT_ID: *AWS_ACCOUNT_ID + # 東京リージョン + REGION_AP_NORTHEAST_1: *REGION_AP_NORTHEAST_1 + # サブネット(PrivateSubnet1) + SUBNET_PRI_1A: *STG_SUBNET_PRI_1A + # サブネット(PrivateSubnet2) + SUBNET_PRI_1D: *STG_SUBNET_PRI_1D + # セキュリティグループ(ecs-all) + SG_ECS_ALL: *STG_SG_ECS_ALL + # セキュリティグループ(ecs-crm-datafetch) + SG_CRM_DATAFETCH: *STG_SG_CRM_DATAFETCH + # 本番環境 + product: + # AWSアカウントID + AWS_ACCOUNT_ID: *AWS_ACCOUNT_ID + # 東京リージョン + REGION_AP_NORTHEAST_1: *REGION_AP_NORTHEAST_1 + # サブネット(PrivateSubnet1) + SUBNET_PRI_1A: *PRD_SUBNET_PRI_1A + # サブネット(PrivateSubnet2) + SUBNET_PRI_1D: *PRD_SUBNET_PRI_1D + # セキュリティグループ(ecs-all) + SG_ECS_ALL: *PRD_SG_ECS_ALL + # セキュリティグループ(ecs-crm-datafetch) + SG_CRM_DATAFETCH: *PRD_SG_CRM_DATAFETCH diff --git a/stepfunctions/TOOLS/convert_definition.py b/stepfunctions/TOOLS/convert_definition.py new file mode 100644 index 00000000..4297e703 --- /dev/null +++ b/stepfunctions/TOOLS/convert_definition.py @@ -0,0 +1,142 @@ +import os +import sys +from string import Template + +# you need to install module `PyYAML` +import yaml + +CONF_FOLDER_PATH = '.' +DEFINITION_FOLDER_BASE = '..' +CONVERTED_FOLDER_BASE = 'build' +CONVERT_CONF = 'convert_config.yaml' +CHAR_CODE = 'utf-8' +PRD_NAME = 'product' +STG_NAME = 'staging' + +class StateMachineTemplate(Template): + """StepFunctionsステートマシンの置き換えるためのテンプレート + + 生のTemplateクラスはプレースホルダーを表す文字列が`$`で、ステートマシン定義内で常用する文字と被ってしまうため、サブクラス化 + """ + + # `#{プレースホルダー}`という形式が使えるようになる + delimiter = "#" + def __init__(self, template: str) -> None: + super().__init__(template) + +def main(args=None): + """ + args1: StepFunctionsステートマシン名 + args2: 変換先環境名(product or staging) + """ + + # カレントディレクトリ移動 + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + # 引数チェック + state_name, env_name = check_args(args) + + print('引数確認OK') + + # ファイル存在チェック + check_file_exist(state_name) + + print('ファイル存在確認OK') + + # フォルダがなければ作る + env_name_folder = f'{DEFINITION_FOLDER_BASE}/{state_name}/{CONVERTED_FOLDER_BASE}/{env_name}' + if not os.path.isdir(env_name_folder): + os.makedirs(env_name_folder, exist_ok=True) + print(f'定義生成用フォルダを作成しました。フォルダ名:{env_name_folder}') + + # 変換 + converted_file_name = convert_definition(state_name, env_name) + + print(f'変換が完了しました ファイル名:{converted_file_name}') + + return + + +def check_args(args): + try: + check_length(args) + + check_aws_environment(args) + + return args[1], args[2] + + except Exception as e: + raise Exception(f'引数確認に失敗しました {e}') + + +def check_length(args): + if len(args) != 3: + raise Exception('引数の数が異常です') + + return + + +def check_aws_environment(args): + if args[2] not in [STG_NAME, PRD_NAME]: + raise Exception('第2引数が不正です') + + return + + + +def check_file_exist(state_name): + if not os.path.isfile(f'{CONF_FOLDER_PATH}/{CONVERT_CONF}'): + raise Exception('変換定義ファイルが存在しません') + + if not os.path.isfile(f'{DEFINITION_FOLDER_BASE}/{state_name}/{state_name}.json'): + raise Exception('変換元のステートマシン定義が存在しません') + + return + + +def convert_definition(state_name, env_name): + + try: + # 定義フォルダのパス生成 + from_folder = f'{DEFINITION_FOLDER_BASE}/{state_name}' + to_folder = f'{from_folder}/{CONVERTED_FOLDER_BASE}/{env_name}' + + # 変換定義の読み込み + convert_config = read_env_specific_config(state_name, env_name) + print(convert_config) + + # テンプレートとなるファイルを読み込み + with open(f'{from_folder}/{state_name}.json', mode='r', encoding=CHAR_CODE) as from_file: + state_json = from_file.read() + state_json_template = StateMachineTemplate(state_json) + # 環境固有の値を置き換え + substituted_state_json = state_json_template.substitute(ENV_NAME=env_name, **convert_config) + + # 書き込みファイルオープン + with open(f'{to_folder}/{state_name}.json', mode='w', + encoding=CHAR_CODE, newline='\n') as to_file: + to_file.write(substituted_state_json) + + return f'{to_folder}/{state_name}.json' + + except Exception as e: + raise Exception(f'変換に失敗しました {e}') + + +def read_env_specific_config(state_name, env_name): + + try: + # 変換定義ファイルオープン + with open(f'{CONF_FOLDER_PATH}/{CONVERT_CONF}', mode='r', encoding=CHAR_CODE) as convert_conf_file: + config_yaml = yaml.load(convert_conf_file, yaml.Loader) + + # 変換定義からステートマシンの環境固有の値を取得 + env_specific_config = config_yaml['config'][state_name][env_name] + return env_specific_config + + except Exception as e: + Exception('変換定義ファイルの読み込みに失敗しました') + + +if __name__ == '__main__': + main(args=sys.argv) diff --git a/stepfunctions/r-crm-datafetch-state/r-crm-datafetch-state.json b/stepfunctions/r-crm-datafetch-state/r-crm-datafetch-state.json new file mode 100644 index 00000000..f1d3bf4f --- /dev/null +++ b/stepfunctions/r-crm-datafetch-state/r-crm-datafetch-state.json @@ -0,0 +1,129 @@ +{ + "Comment": "crm-datafetch root job", + "StartAt": "params", + "States": { + "params": { + "Comment": "パラメータ設定", + "Type": "Pass", + "Parameters": { + "sns": { + "TopicArn": "arn:aws:sns:#{REGION_AP_NORTHEAST_1}:#{AWS_ACCOUNT_ID}:nds-notice-#{ENV_NAME}" + }, + "ecs": { + "Cluster": "arn:aws:ecs:#{REGION_AP_NORTHEAST_1}:#{AWS_ACCOUNT_ID}:cluster/mbj-newdwh2021-#{ENV_NAME}-crm-ecs", + "LaunchType": "FARGATE", + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "Subnets": [ + "#{SUBNET_PRI_1A}", + "#{SUBNET_PRI_1D}" + ], + "SecurityGroups": [ + "#{SG_ECS_ALL}", + "#{SG_CRM_DATAFETCH}" + ], + "AssignPublicIp": "DISABLED" + } + } + } + }, + "ResultPath": "$.params", + "Next": "crm-datafetch-diff" + }, + "crm-datafetch-diff": { + "Comment": "CRMデータ取得", + "Type": "Task", + "Resource": "arn:aws:states:::ecs:runTask.sync", + "Parameters": { + "Cluster.$": "$.params.ecs.Cluster", + "LaunchType.$": "$.params.ecs.LaunchType", + "TaskDefinition": "arn:aws:ecs:#{REGION_AP_NORTHEAST_1}:#{AWS_ACCOUNT_ID}:task-definition/mbj-newdwh2021-#{ENV_NAME}-task-crm-datafetch", + "NetworkConfiguration.$": "$.params.ecs.NetworkConfiguration" + }, + "Retry": [ + { + "ErrorEquals": ["States.ALL"], + "BackoffRate": 2, + "IntervalSeconds": 3, + "MaxAttempts": 3 + } + ], + "Catch": [ + { + "ErrorEquals": ["States.ALL"], + "ResultPath": "$.result", + "Next": "ErrorEnd" + } + ], + "ResultPath": "$.result", + "Next": "Wait" + }, + "Wait": { + "Type": "Wait", + "Seconds": 5, + "Next": "crm-datafetch-all" + }, + "crm-datafetch-all": { + "Comment": "CRMデータ全量取得", + "Type": "Task", + "Resource": "arn:aws:states:::ecs:runTask.sync", + "Parameters": { + "Cluster.$": "$.params.ecs.Cluster", + "LaunchType.$": "$.params.ecs.LaunchType", + "TaskDefinition": "arn:aws:ecs:#{REGION_AP_NORTHEAST_1}:#{AWS_ACCOUNT_ID}:task-definition/mbj-newdwh2021-#{ENV_NAME}-task-crm-datafetch", + "NetworkConfiguration.$": "$.params.ecs.NetworkConfiguration", + "Overrides": { + "ContainerOverrides": [ + { + "Name": "mbj-newdwh2021-#{ENV_NAME}-container-crm-datafetch", + "Environment": [ + { + "Name": "OBJECT_INFO_FILENAME", + "Value": "crm_object_list_all.json" + } + ] + } + ] + } + }, + "Retry": [ + { + "ErrorEquals": ["States.ALL"], + "BackoffRate": 2, + "IntervalSeconds": 3, + "MaxAttempts": 3 + } + ], + "Catch": [ + { + "ErrorEquals": ["States.ALL"], + "ResultPath": "$.result", + "Next": "ErrorEnd" + } + ], + "ResultPath": "$.result", + "Next": "SuccessNotice" + }, + "SuccessNotice": { + "Comment": "正常終了通知", + "Type": "Task", + "Resource": "arn:aws:states:::sns:publish", + "Parameters": { + "TopicArn.$": "$.params.sns.TopicArn", + "Subject": "StepFunctions正常終了通知", + "Message.$": "States.Format('CRMデータ取得処理が完了しました。\n\n 対象のステートマシン:{} \n 実行ID:{}', $$.StateMachine.Id, $$.Execution.Name)" + }, + "Next": "NormalEnd" + }, + "NormalEnd": { + "Comment": "正常終了", + "Type": "Succeed" + }, + "ErrorEnd": { + "Comment": "異常終了", + "Type": "Fail", + "Error": "StatesError", + "Cause": "StepFunctions ErrorEnd" + } + } +}