diff --git a/lambda/transfer-medpass-data/Dockerfile b/lambda/transfer-medpass-data/Dockerfile new file mode 100644 index 00000000..19b9594f --- /dev/null +++ b/lambda/transfer-medpass-data/Dockerfile @@ -0,0 +1,19 @@ +# AWS公式のDockerイメージを利用 +FROM public.ecr.aws/lambda/python:3.12 + +# pythonの標準出力をバッファリングしないフラグ +ENV PYTHONUNBUFFERED=1 +# pythonのバイトコードを生成しないフラグ +ENV PYTHONDONTWRITEBYTECODE=1 + +# 必要なファイルをイメージにコピー +COPY Pipfile Pipfile.lock main.py ./ + +# ライブラリインストール +RUN pip install --upgrade pip wheel setuptools && \ + pip install pipenv --no-cache-dir && \ + pipenv install --system --deploy && \ + pip uninstall -y pipenv virtualenv-clone virtualenv + +# lambdaハンドラを起動 +CMD [ "main.handler" ] diff --git a/lambda/transfer-medpass-data/Pipfile b/lambda/transfer-medpass-data/Pipfile new file mode 100644 index 00000000..4f1166b7 --- /dev/null +++ b/lambda/transfer-medpass-data/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +boto3 = "*" +pyzipper = "*" + +[dev-packages] +autopep8 = "*" +flake8 = "*" + +[requires] +python_version = "3.12" diff --git a/lambda/transfer-medpass-data/Pipfile.lock b/lambda/transfer-medpass-data/Pipfile.lock new file mode 100644 index 00000000..1bae414a --- /dev/null +++ b/lambda/transfer-medpass-data/Pipfile.lock @@ -0,0 +1,172 @@ +{ + "_meta": { + "hash": { + "sha256": "d8b79fd5be60005b43448511c67536c114e5fd73722a17e77a5e60a9283aea25" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "boto3": { + "hashes": [ + "sha256:0314e6598f59ee0f34eb4e6d1a0f69fa65c146d2b88a6e837a527a9956ec2731", + "sha256:d41037e2c680ab8d6c61a0a4ee6bf1fdd9e857f43996672830a95d62d6f6fa79" + ], + "index": "pypi", + "version": "==1.34.136" + }, + "botocore": { + "hashes": [ + "sha256:7f7135178692b39143c8f152a618d2a3b71065a317569a7102d2306d4946f42f", + "sha256:c63fe9032091fb9e9477706a3ebfa4d0c109b807907051d892ed574f9b573e61" + ], + "markers": "python_version >= '3.8'", + "version": "==1.34.136" + }, + "jmespath": { + "hashes": [ + "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", + "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe" + ], + "markers": "python_version >= '3.7'", + "version": "==1.0.1" + }, + "pycryptodomex": { + "hashes": [ + "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1", + "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305", + "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c", + "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458", + "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed", + "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc", + "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c", + "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc", + "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079", + "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb", + "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa", + "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427", + "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5", + "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64", + "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6", + "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e", + "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43", + "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3", + "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499", + "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8", + "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b", + "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623", + "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7", + "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc", + "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4", + "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e", + "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a", + "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781", + "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794", + "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea", + "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b", + "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==3.20.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.9.0.post0" + }, + "pyzipper": { + "hashes": [ + "sha256:0adca90a00c36a93fbe49bfa8c5add452bfe4ef85a1b8e3638739dd1c7b26bfc", + "sha256:6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87" + ], + "index": "pypi", + "version": "==0.3.6" + }, + "s3transfer": { + "hashes": [ + "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", + "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69" + ], + "markers": "python_version >= '3.8'", + "version": "==0.10.2" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "urllib3": { + "hashes": [ + "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3", + "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429" + ], + "markers": "python_version < '3.10'", + "version": "==1.26.19" + } + }, + "develop": { + "autopep8": { + "hashes": [ + "sha256:8d6c87eba648fdcfc83e29b788910b8643171c395d9c4bcf115ece035b9c9dda", + "sha256:a203fe0fcad7939987422140ab17a930f684763bf7335bdb6709991dd7ef6c2d" + ], + "index": "pypi", + "version": "==2.3.1" + }, + "flake8": { + "hashes": [ + "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a", + "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5" + ], + "index": "pypi", + "version": "==7.1.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c", + "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4" + ], + "markers": "python_version >= '3.8'", + "version": "==2.12.0" + }, + "pyflakes": { + "hashes": [ + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" + ], + "markers": "python_version >= '3.8'", + "version": "==3.2.0" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + } + } +} diff --git a/lambda/transfer-medpass-data/main.py b/lambda/transfer-medpass-data/main.py new file mode 100644 index 00000000..19b6a95f --- /dev/null +++ b/lambda/transfer-medpass-data/main.py @@ -0,0 +1,236 @@ +import datetime +import logging +import os +from zoneinfo import ZoneInfo + +import boto3 +import pyzipper +from pyzipper.zipfile import BadZipFile + +# 環境変数 +DATA_IMPORT_BUCKET = os.environ["DATA_IMPORT_BUCKET"] +HCP_WEB_TARGET_FOLDER = os.environ["HCP_WEB_TARGET_FOLDER"] +HCP_WEB_BACKUP_BUCKET = os.environ["HCP_WEB_BACKUP_BUCKET"] +BACKUP_ZIPFILE_FOLDER = os.environ["BACKUP_ZIPFILE_FOLDER"] +BACKUP_DATA_IMPORT_FOLDER = os.environ["BACKUP_DATA_IMPORT_FOLDER"] +DATA_IMPORT_FILENAME = os.environ["DATA_IMPORT_FILENAME"] +MEDPASS_ZIP_PASSWORD_PARAMETER_STORE_KEY = os.environ["MEDPASS_ZIP_PASSWORD_PARAMETER_STORE_KEY"] + +LOG_LEVEL = os.environ["LOG_LEVEL"] +TZ = os.environ["TZ"] + +# 定数 +# 多重起動抑制用のコントロールファイルの拡張子 +EXCLUSIVE_CONTROL_FILE_EXT = '.doing' +# tmpフォルダパス +PATH_TMP = '/tmp' +# 拡張子 +ZIP_FILE_EXT = 'zip' +CSV_FILE_EXT = 'csv' + +# S3クライアント +s3_client = boto3.client('s3') +# SystemsManagerクライアント +ssm_client = boto3.client('ssm') + +# logger設定 +logger = logging.getLogger() + + +def log_datetime_convert_tz(*arg): + """ログに出力するタイムスタンプのロケールを変更する(JST指定)""" + return datetime.datetime.now(ZoneInfo(TZ)).timetuple() + + +formatter = logging.Formatter( + '[%(levelname)s]\t%(asctime)s\t%(message)s\n', + '%Y-%m-%d %H:%M:%S' +) +formatter.converter = log_datetime_convert_tz +for handler in logger.handlers: + handler.setFormatter(formatter) + +level = logging.getLevelName(LOG_LEVEL) +if not isinstance(level, int): + level = logging.INFO +logger.setLevel(level) + + +def extract_zip_with_password(zip_filepath: str, extract_to_folder: str, password: str) -> os.path: + """ + 暗号化ZIPを解凍する。 + + :param zip_filepath: ZIPファイルが保管されているフォルダパス + :param extract_to_folder: ZIPファイルの解凍先フォルダ + :param password: ZIPパスワード + + :return 解凍されたファイルパス + """ + # ZIPを解凍 + try: + with pyzipper.AESZipFile(zip_filepath) as z: + # ZIP内のファイルは1つのみ + inner_filename = z.filelist[0].filename + z.extractall(path=extract_to_folder, pwd=password.encode()) + except Exception as e: + raise e + + return os.path.join(extract_to_folder, inner_filename) + + +def get_s3_event_parameter(event: dict) -> tuple[str, str, str, str]: + s3_event = event["Records"][0]["s3"] + event_bucket_name: str = s3_event["bucket"]["name"] + event_object_key: str = s3_event["object"]["key"] + event_file_name: str = os.path.basename(event_object_key) + event_folder_name: str = os.path.dirname(event_object_key).split('/')[0] + + return event_bucket_name, event_object_key, event_file_name, event_folder_name + + +def get_ssm_params(parameter_key: str, with_decryption: bool = True) -> str: + """SSMパラメータストアから指定されたパラメータ名の値を取得する""" + response = ssm_client.get_parameter( + Name=parameter_key, WithDecryption=with_decryption) + parameter_value: str = response['Parameter']['Value'] + return parameter_value + + +def delete_doing_file(event: dict) -> None: + """.doingファイルをバケット上から削除する""" + # イベント情報を取得 + ( + event_bucket_name, + event_object_key, + _, + _ + ) = get_s3_event_parameter(event) + # ⑨ メモリに保持したバケット名/フォルダ名内の「受信データファイル名.doing」ファイルを削除する + s3_client.delete_object( + Bucket=event_bucket_name, Key=f'{event_object_key}{EXCLUSIVE_CONTROL_FILE_EXT}') + + +def handler(event, context) -> None: + try: + # ① 処理開始ログを出力する + logger.info('I-01-01 処理開始 medパスデータ解凍・復号化・転送処理') + + # ② 処理開始時に受け取ったイベント情報をログに出力する + # バケット名・フォルダ名・受信データファイル名をメモリに保持 + ( + event_bucket_name, + event_object_key, + event_file_name, + event_folder_name + ) = get_s3_event_parameter(event) + logger.info(f'I-02-01 受信バケット:{event_bucket_name}') + logger.info(f'I-02-01 フォルダ名:{event_folder_name}') + logger.info(f'I-02-01 ファイル名:{event_file_name}') + # ③ S3イベントによるLambdaの重複発火防止の為、メモリに保持したバケット名/フォルダ名内に、「受信データファイル名.doing」ファイルが存在するかチェックする + try: + s3_client.head_object( + Bucket=event_bucket_name, Key=f'{event_object_key}{EXCLUSIVE_CONTROL_FILE_EXT}') + logger.error( + f'E-01-01 {event_bucket_name}/{event_object_key}は現在処理中です。処理を終了します。') + return + except Exception: + # .doingファイルが見つからなかった場合は、処理を続行する + # メモリに保持したバケット名/フォルダ名内に、「受信データファイル名.doing」ファイルを作成する + logger.info('I-03-01 medパスデータの解凍・復号化・転送を開始します') + s3_client.put_object( + Bucket=event_bucket_name, Key=f'{event_object_key}{EXCLUSIVE_CONTROL_FILE_EXT}', Body=b'') + + # ④ S3から暗号化ZIPファイルを読み込む + try: + logger.info( + f'I-04-01 暗号化ZIPファイル読込 読込元:{event_bucket_name}/{event_object_key}') + s3_client.download_file( + event_bucket_name, event_object_key, os.path.join(PATH_TMP, event_file_name)) + logger.info('I-04-02 暗号化ZIPファイルをダウンロードしました') + except Exception as e: + logger.exception(f'E-04-01 暗号化ZIPファイルのダウンロードに失敗しました エラー内容:{e}') + delete_doing_file(event) + return + + # ⑤ ZIP解凍パスワードをSSM パラメータストアから取得する + try: + logger.info('I-05-01 ZIP解凍パスワードを読込') + zip_password = get_ssm_params( + MEDPASS_ZIP_PASSWORD_PARAMETER_STORE_KEY) + except Exception as e: + logger.exception(f'E-05-01 ZIP解凍パスワードの読み込みに失敗しました エラー内容:{e}') + delete_doing_file(event) + return + + # ⑥ ZIPファイルを解凍してローカルに保存 + try: + logger.info(f'I-05-02 ZIP解凍開始') + extracted_zip_file_path = extract_zip_with_password( + os.path.join(PATH_TMP, event_file_name), PATH_TMP, zip_password) + except RuntimeError as e: + if 'password' in str(e).lower(): + # パスワードが間違っている場合のエラー + logger.exception( + f'E-05-02 ZIPのパスワードが不正のため、解凍に失敗しました エラー内容:{e}') + delete_doing_file(event) + return + else: + # 想定外のエラー + raise e + # ZIPファイルが壊れている場合のエラー + except BadZipFile as e: + logger.exception(f'E-05-03 ZIPの形式が不正のため、解凍に失敗しました エラー内容:{e}') + delete_doing_file(event) + return + + # データ登録用にファイルをリネーム + # ZIPファイル名がyyyymmdd.zipのため、年月日部分をデータ登録用ファイル名の末尾につけ、拡張子をCSVに変更 + data_import_file_name = f'{DATA_IMPORT_FILENAME}_{event_file_name.lower().replace(ZIP_FILE_EXT, CSV_FILE_EXT)}' + logger.info(f'I-05-03 ZIP解凍成功') + + # ⑥ 受信した暗号化ZIPファイルと解凍後のファイルをバックアップする + backup_copy_source = { + 'Bucket': event_bucket_name, 'Key': event_object_key} + execute_date_yyyymmdd = datetime.date.today().strftime('%Y/%m/%d') + + # ZIPファイルのバックアップ + s3_client.copy_object( + Bucket=HCP_WEB_BACKUP_BUCKET, + Key=f'{BACKUP_ZIPFILE_FOLDER}/{execute_date_yyyymmdd}/{event_file_name}', + CopySource=backup_copy_source + ) + logger.info( + f'I-06-01 medパス受信データのバックアップ完了:{HCP_WEB_BACKUP_BUCKET}/{BACKUP_ZIPFILE_FOLDER}/{execute_date_yyyymmdd}/{event_file_name}') + + # 解凍後ファイルのバックアップ + s3_client.upload_file( + extracted_zip_file_path, + Bucket=HCP_WEB_BACKUP_BUCKET, + Key=f'{BACKUP_DATA_IMPORT_FOLDER}/{execute_date_yyyymmdd}/{data_import_file_name}' + ) + logger.info( + f'I-06-02 medパス解凍後データのバックアップ完了:{HCP_WEB_BACKUP_BUCKET}/{BACKUP_DATA_IMPORT_FOLDER}/{execute_date_yyyymmdd}/{data_import_file_name}') + + # ⑦ 解凍後のファイルをデータ登録バケットに転送する + data_import_copy_source = {'Bucket': HCP_WEB_BACKUP_BUCKET, + 'Key': f'{BACKUP_DATA_IMPORT_FOLDER}/{execute_date_yyyymmdd}/{data_import_file_name}'} + s3_client.copy_object( + Bucket=DATA_IMPORT_BUCKET, + Key=f'{HCP_WEB_TARGET_FOLDER}/{data_import_file_name}', + CopySource=data_import_copy_source + ) + + # アップロード後、元のバケットからは削除する + s3_client.delete_object(Bucket=event_bucket_name, Key=event_object_key) + logger.info( + f'I-07-01 medパス解凍後データの転送完了:{DATA_IMPORT_BUCKET}/{HCP_WEB_TARGET_FOLDER}/{data_import_file_name}') + + # ⑧ メモリに保持したバケット名/フォルダ名内の「受信データファイル名.doing」ファイルを削除する + delete_doing_file(event) + + logger.info('I-08-01 処理終了 medパスデータ解凍・復号化・転送処理') + + except Exception as e: + logger.exception(f'想定外のエラーが発生しました。処理を終了します。 例外内容:{e}') + delete_doing_file(event) + raise e