AWS StepFunctionsによるXLSX・PDF作成フローを構築した話

# 概要

LITALICOでは障害福祉サービス事業所をサポートするサービスを開発しています。その中の一つに帳票出力機能があります。以下は福祉事業における帳票の一例です。

- 請求書・明細書
- 実績記録表
- 利用料請求書

<img width="200px" alt="帳票サンプル" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104643/d3fc7844-b361-f726-7128-62b73a67afb3.png">


これらは利用者の方にお渡ししたり、事業所のある市区町村に紙媒体として提出する必要があります。サービスとしてはPDFやXLSXファイルのダウンロード、またはブラウザから印刷を行うための表示機能が求められます。

帳票出力は福祉事業者向けのサービスとして必須の機能です。弊社では各種技術を用いてこれらを実現してきましたが、開発コストやパフォーマンスの面で課題もありました。今回新たに帳票出力機能を開発する機会があり、いくつかの新しい試みを導入することが出来ました。技術的にはサーバレスアーキテクチャを採用しています。設計の核となるのはAWS-StepFunctionsによるワークフロー管理です。トピックは主に以下になります。

:::note info
- 最終的な出力形態（になることが多い）であるPDFの変換元となるXLSXファイルの生成方法
- XLSXファイルを生成する為のデータが、いくつものサービス・APIに分かれておりどの様にデータ収集し統合するか
- ワークフローの分割。処理委譲とメインフロー回帰
:::

弊社におけるサーバレスアーキテクチャ活用の一例を紹介させて頂き、LITALICOのエンジニアリングや事業に関心を持って頂けたら嬉しく思います。StepFunctionsの利用方法等についても得るものがありましたら幸いです。

# XLSXファイルを生成する

XLSXファイルはPDFを作成する元ネタとなります。PDFを作る過程で生まれるものでものありますし、XLSXファイル自体を目的に生成することもあります。XLSXファイルを扱える表計算ソフトはMicrosoftExcelを始め広く普及しているので、一部内容を編集したい場合はこのフォーマットが好まれます。また、厚生労働省のWebサイトから配布されている記入のテンプレートとなるファイルもXLSXであることが多いです。PDFを生成するのに最初からレイアウトが組まれたXLSXファイルがあるのでしたらこれを利用しない手はありません。

他にはHTMLとCSSを組みそこからPDFを生成する方法もあります。扱いが容易なマークアップ言語を用いて素材を作れるのは大きなメリットですが、今回はXLSXファイル自体を要求されるケースも多かったのでこちらの方法は候補から外れました。HTMLからPDFへの変換はかなり綺麗に出来ることは各種ブラウザに搭載されている印刷機能からも確認できます。ただ、HTMLから編集を前提としたXLSXファイルの生成は今の所良い手段が無さそうです。

## 各種ツール検討
XLSXはXMLで構成されていてその仕様がOOXMLとして公開されています。XLSXを扱える様々な言語のライブラリ、ツールが開発されています。ですがExcelで作成した複雑なレイアウトやスタイルを忠実に取り扱えるものは多くありません。
Excelは表計算ソフトですが製図に使われることも多く、今回取り扱う帳票についても一部複雑な構成を取っているものもあります。最終的に人の目に触れるものですから、一部の枠線が意図せず太字になっていたなどあったら違和感を覚えることは必至です。

いくつか検討した結果、最終的に[Excelize](https://xuri.me/excelize/ja/)を採用しました。ExcelizeはExcelのインストールを必要としない、純粋なOOXMLの仕様に基づいたxmlを取り扱えるライブラリです。Excelとの高い互換性はずば抜けておりパフォーマンスも申し分ないです。

## XLSX生成の仕組み

Excelizeはgo言語で取り扱えるライブラリです。goは開発をサポートしてくれる環境が充実していますし洗練された言語ですが、扱えるようになるにはそれなりの学習コストが必要です。弊社ではPHPやRubyでの開発が中心であり、goをすぐに扱えるエンジニアは多くありません（たぶん…）。
帳票は何十種類もあるので、Excelizeがいかに優れたものであってもここにリソースを割くことは少し難しいと感じました。過去の帳票システムはJavaで開発されていました。Javaはgoよりも経験者が多いとはいえ、インタプリタ言語に比べるとハードルがあります。Javaで新たな帳票に対応するとなると、開発適任者を確保することから始めなければならずコストがかかっていたと思います。ここは解決したい課題の一つでした。

そこでgoとExcelizeによる開発を最小限にするようにしました。goで一つ一つの帳票を作成するプログラムを作るのではなく、テンプレートとなるXLSXファイルと、値を埋め込む設定ファイルを読み込み、結果を出力してくれるコマンドを作成することにしました。正式名称はあるのですが呼びづらいためか社内では「帳票作成君」の呼称で通っています。


<img width="400px" alt="帳票作成君" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104643/21617cef-64de-9250-1d1d-11ea50ddd0cb.png">

コマンドはテンプレートとなるxlsxファイルと、値の埋め込め設定が記載されたJSONファイルから新たなxlsxファイルを生成します。この方式であればgoに触れる必要はなく、設定が記載されたJSONファイルを普段慣れ親しんだ言語で開発することが出来ます。

コマンドは後述するAWSLambdaとも相性が良く、サーバーレスアーキテクチャを構成するにあたって扱いやすい要素となりました。


# StepFunctionsによるフロー管理

帳票作成の全体の大まかな流れは以下となります。

1. 状態通知（ステップ１）
1. `XLSXの設定ファイル作成`
1. `PDFの設定ファイルの作成`
1. 状態通知（ステップ２）
1. `XLSXファイル生成`
1. `PDFファイル生成`
1. 状態通知（成功）

このワークフローは非同期で動作する為、起動元に状態を通知する為の処理が挟まっています。これらの手続きをワークフローとして表現していきます。

<img width="250px" alt="ワークフロー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104643/8efd0996-8028-a6ec-fda2-40731b1813a4.png">


ワークフローとは業務に関する一連の手続きの流れを表したものです。プログラムで言えばmain関数であったり、あるいはLambda関数などのタスクをメソッド、ワークフローをクラスとして捉えても良いかもしれません。

何も実行しないPassステートを使って必要な処理を一旦全て書き出してしまうのがお勧めです。StepFunctionsはちょっとした製図ツールとして活用しても便利かと思います。

ここからいくつか重要なポイントとなる箇所を取り上げていきます。


## 1.XLSXの設定ファイル作成
XLSXに値を埋め込む設定ファイル（JSON）の作成を行います。設定ファイルに記載するデータは障害者福祉事業に関する複雑なドメインに基づいた内容を入力しますので、Lambda関数の中で取り扱うには少々荷が重いです。この内容を扱える専門のアプリケーションサービスに処理を委譲します。

StepFuncitonsはAWSで提供される各種サービスと連携しますが、外部のサービスとも連携を取ることが可能です。その場合メッセージ送信システムであるSQSやSNSを使用します。メッセージ送信システムを使いその受信したサービスがタスクを完了することを待機する場合は、`コールバックを待つ`にチェックを入れます。チェックした場合、受信先のサービスは処理が完了したことをステートマシンに通知する必要があります。SQSの場合キューが正常に処理されたからといって自動的に完了扱いにならないことに注意しましょう。

![Step Functions Management Console 2022-12-21 04-32-56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104643/439eefae-51aa-fa81-9f29-a1afb603c9b4.png)

メッセージの受信と完了通知さえ行えれば、あらゆるシステムをワークフローに組み込むことが出来ます。設定ファイルを作る処理はいくつかのアプリケーションに複数投げるので処理を並列で行うようにしました。

<img width="400px" alt="ワークフロー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104643/be527b84-0312-b17d-58a9-c3ce911fb5e1.png">


## 2. PDFの設定ファイル作成

次は複数のXLSXファイルを組み合わせて一つのPDFを作成する設定ファイルを作成します。こちらもどのような順番と組み合わせで結合を行うか、ドメイン知識が必要になるところですのでSQS経由で外部にて行っています。

## 3. XLSXファイルの作成

設定に基づきXLSXファイルを生成します。変換はLambda関数で行います。Lambda関数にgoで作成した`帳票作成君`のコマンドをレイヤー登録しています。先にも触れましたがLambdaとコマンドは相性が良く起動コストもかからない為、軽快でスケールしやすいです。

XLSXファイル生成は複数回行われる為Lambdaを並列に稼働します。どの様に変換処理を行うかを記載した設定ファイルを用意しておきます。設定ファイルにはこの後Map処理する為のデータが配列で表現されています。

```json
{
  "settings": [
    {
      "bucket": "bucketA",
      "template": "テンプレートA.xlsx",
      "input": "設定ファイル1.json",
      "output": "出力ファイル2.xlsx"
    },
    {
      "bucket": "bucketA",
      "template": "テンプレートB.xlsx",
      "input": "設定ファイル3.json",
      "output": "出力ファイル4.xlsx"
    }
  ]
}
```

設定ファイルをS3にアップロードしておけば、`Process JSON file in S3`ステートを使い簡単に並列処理を行うことが出来ます。

このように設定ファイルの読み込みやループに関する記述はLambda関数の中に記述せず、外部からInputする設計にしておくことがモジュールの再利用性を高め、パフォーマンスチューニング等も行いやすくなります。

<img width="300px" alt="ワークフロー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104643/80387f70-f113-43fa-5662-720f09e245bf.png">


## 4. PDFファイル生成

生成された複数のXLSXファイルを統合してPDF変換を行います。
PDF変換は[Gotenberg](https://gotenberg.dev/)を使用しています。


## メインフローによるオーケストレーション

各タスクを内包するワークフローはmain関数やクラスに例えるとイメージしやすいとお話しました。ワークフローの規模にもよりますが、ステップがある程度大きくなった場合は分割を検討してもよいかもしれません。具体的な処理内容とフローは分離されていた方が全体の見通しはよくなります。また、似たようなワークフローを作成する際にモジュールとしてそれぞれの関心事で適切に分離されていると再利用性が高まりそうです。今回は以下の３つに分割しました。

1. メインフロー
1. 帳票作成準備フロー
1. 帳票作成実行フロー


<img width="700px" alt="分割ワークフロー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104643/f883d194-1b1a-7922-49f4-fe5ebf933f25.png">

`メインフロー`はサブとなるフローを呼び出したり、そのサブフローが成功/失敗した場合に状態記録を行う処理を行います。`帳票作成準備フロー`は帳票作成の各種設定ファイルを作り込む段階のフローです。`帳票作成実行フロー`は実際にXLSXファイルやPDFファイルを生成するフローとなります。サブのフローにはそのタスクがすべき仕事に専念させ、状態管理等の外側の関心事は持たないようにすることで余計な責務を負わなくても済むようになります。この辺りの考え方は普段行っているプログラミングの原則がそのまま当てはめられることが多いのではないでしょうか。


# 感想、課題、苦労したこと

最後に開発を経ての感想と今回取り扱わなかったトピック、課題等も含めて雑多に列挙します。別の機会で取り上げられたらと思います。

- フローが一気通貫で正常動作するととても嬉しい
- 帳票作成君（XLSX生成コマンド）のボトルネックとパフォーマンス向上で工夫したこと
- ApiGatewayとStepFunctionsの連携。それぞれのクォータ上限の回避方法
- ワークフローのキャンセル処理
- SQSとPHPLaravelによるJOBワーカー設計
- SREチームとの連携、IAM管理の難しさ
- ワークフローやLambdaコードの管理
- ログ監視とトレーサビリティ
- コレオグラフィパターンとワークフロー

お読み頂きありがとうございました。