はじめに
カスタマーサービス私・・・ エンジニア先輩・・・
「Amazon Connectの利用開始には、祝日判定機能が必要です」
「お、インフラ作ってあげるからアプリケーション作ってみなよ」
「アプ……?え、私でやってみていいんですか?わーい!」
「じゃあアーキ図これね。似た処理してるソースコードあげる!環境構築もしておいたよ!」
「まってこのクジラさんなに??????」※
※dockerです
概要
クラウド型コンタクトセンター「Amazon Connect」の、祝日判定機能を作りました。
ハウツー記事ではありません。
祝日か、それ以外か。それだけの開発に、非エンジニアがどれだけ大変な思いをしたか。
笑って読んでいただけると幸いです。
私の初期スペック
入社後に2ヶ月間取り組んだIT研修が楽しくて、その中で簡単なプログラミングをしてLambdaをいじったり、SlackのWebhookで遊んだりをした経験はありました。
また、Amazon Connectに関しては、比較的詳しい状態でした。
この祝日判定も、自部署のコールセンターシステムをAmazon ConnectにリプレイスするPJTの一環であり、その際に、Amazon Connectのインスタンスは私が全て作っています。
祝日判定機能が必要だと判断した段階で、Amazon Connectから
直接Lambdaを着火させる仕組みがあることは知っていましたし、
「Lambdaで祝日リストを照会して、T/F返してもらう」くらいには想像がついていました。
とはいえ、開発経験らしい開発経験はなく、今回はrubyを使ったのですが、
私の知識はちょっぴり進めたprogateのみでした。
開発する前「5行で終わるでしょ」
Is today holiday?
3語です。「今日は祝日ですか?」これだけに答えたいのです。
最初に想像した私の「開発をやってみる」は、祝日リストを除いて、以下、たった5行でした。
require 'date'
def isholiday()
#祝日リスト
holidays = [
"2023-01-01", # 元日
"2023-01-02", # 会社の休日
#2023年度の祝日が続く......
]
holidays.include?(Date.today.to_s)
end
lambda_response = isholiday()
そんなことはなかった
ええ、そうは問屋が卸しません。そんな気はしていました。
Lambdaに直接書いて終わりなら、環境構築などしなくていいはずなのです。
AWSCodeCommitPowerUserなんて必要ないのです。
クジラさん(Docker)も出てこなくて済んだはずです。
ちゃんと開発をする、とはどういうことなのか。
1ヶ月かけて、身を持って体験することになりました。
ここからが、やっと私の開発奮闘記になります。
祝日判定の設計書
下の奮闘記を読むにあたり、私が作っていたものをイメージしていただくため、
設計書を載せておきます。
とても小さな機能であることがお分かりいただけるかと思います。
終わった今、同じものを作ることを任されたら、3営業日あれば作れるんじゃないかと思います。
「0から作るんですか??」
「まさか。はい。参考にするソースコードから、不要なところを極力削ったやつ」
「おおー、この子を編集していくんですね。ありがとうございます。」
「やれるだけ頑張ってみて〜!」
こんな感じで始まりました。
ここから本編:はじめて開発してみた
ここからは、私の失敗談や学びを書いていきます。
あまりにも色々あったので、各トピックを折りたたんでみました。
準備期間:参考にもらったソースコードが読めない
既に動くものを参考にして作るのが一番分かりやすくて良さそうだ、として、
実際に事業部で使われていて、LambdaとS3間のやりとりがある、一番シンプルなソースコードを渡してもらいました。
しかし。コードどころか、何がどう置いてあるのかがさっぱりわからないのです。
フォルダが大量にあって、domainとinfrastractureに分かれていて、他にもymlファイルに、gemファイルに、dockerファイルに……
「これは早々に助けを呼ばなければ無理だ」と察しました。
当時の私は、レイヤードアーキテクチャなぞ知りませんでした。
というか、rubyも怪しい状態です。
そもそも、事業部で使われているソースコードだからと言って、カスタマーサービスの私はその処理の起点と、結果しか知りません。
コードとしてどんな処理を通って、何が起きているかも正確に理解しているわけではない状態で、読んだらイメージがつくわけがありませんでした。
メインの処理の流れに加えて、ログ出力、バリデーションやエラーハンドリングがあることを、この段階で初めてきちんと学びました。
また、処理を関数で細かく分けること、フォルダを分けて、共通処理しうる部分を切り出すことなど、システム作りのお作法に関するインプットから始まるのでした。
初日:最初のgitでつまづく
先輩エンジニア氏の指導と説明のままにコマンドを叩いて環境構築をした後、
ブランチを切って「ここが開発する場所だよ」と言われてから数日。
自分でgitを理解しようとして、ブランチを切り直そうとしました。
(この時点で、既に自分のブランチに対して何度かpushをしています)
作業していたブランチを消して、もう一度同じ名前でブランチを切ってから、
開発して、最後にpushしたところ、怒られました。
You have divergent branches and need to specify how to reconcile them.
reconcile、「和解せよ」と言われましても、さっぱりです。
よくよく調べると、rebaseなるものか、mergeなるものかをすればいいらしい。
聞き覚えのあるmergeをしました。そこでびっくりします。
ブランチを最初に消したはずなのに、昔編集した箇所が編集されたことになっている。
これはなんだ、と、混乱した私は、エンジニア同期に助けを求めました。
「リモートブランチとローカルブランチって知ってる?」
「リモートワークと出社でブランチ変わっちゃうってこと????」
「んなわけあるかwwwwwwwwww」
※先輩エンジニアの名誉のために補足
gitの仕組みは、最初に一通りきちんと説明してくれていました、が、当時の私は情報量の海に溺れており、理解が追いついていませんでした。
というわけで、初日は、以下2コマンドを叩いて、リモート/ローカルブランチを完全に削除する、というところで終わったのでした。
git push --delete origin mybranch1
git branch -d mybranch1
まさかの進捗、マイナス。しかし、gitの仕組みがよくわかりました。
3日目:とりあえず、全部無視して、サンドボックスで動くものを作る
開発を始めて、情報量に溺れて絶望した私は、ひとまず"お作法"を捨てて、
「開発イメージをつける」を次のNAに決めました。
ログ出力やエラーハンドリング、関数の切り分けなど、知らないものは全て後回しにして、
Amazon Connect、Lambda、S3を用意して、「とりあえず動くもの」を作ることにしました。
幸いにも、事業部の方針で全員に(許可制)AWSのサンドボックスが用意されており、
好き勝手にAWSのサービスを使ってみたり、コードを試しに動かしてみたりすることができました。
祝日を判定するのに、機微情報もなにもありません。
環境としても題材としても、気楽に色々なトライ&エラーができる状態だったことは、とてもありがたいことでした。
LambdaからS3にアクセスして、バケット内のファイルを読み込む方法など、AWSならではのAWS同士の使い方は、Qiitaのハウツー記事を読みながら、サンドボックスでひたすら試して理解しました。
また、Rubyの処理を試すには、paiza.IOを使いました。
Date.today
がどんな形をしているのか、テキストデータをどう切り分けてrubyの配列にするのかなど、とりあえず知りたいことは「print」と「実行」を押しまくって考えるタイプの私には、相性のいいツールでした。
5日目:動いた処理を切り分けて、どの層で実行するかを分ける
「具体の処理はservice層に切り出してね」
「なんて????」
動く一連のコードが作れたら、次は、お作法通りに機能を関数に切り分け、各ファイルに置き直しました。
今回、レポジトリにはまだこの祝日判定1つしかないため、全てを厳格に切り分けることはせず、
機能部分の処理として以下3つに分けることにしました。
- Lambdaのeventを受け取るhandler層
- 祝日判定をするservice層
- S3へのアクセス処理のみ共通化したawsのhelper層
この役割ごとにディレクトリを分けて、ファイルを配置するのが難しかったです。
「祝日判定という機能単位で1ファイル」という状態が直感的には受け入れやすく、切り分けは言われるがままに、参考にしたソースコードを見たままに分けていました。
分けることのメリットはなんとなく分かるのですが、「Amazon Connectに関わるアプリケーションとしてファイルを切り分ける」という感覚をつかむのに時間がかかりました。
バリデーション:gemのバージョン問題は初心者殺し
ActiveModelを使って、Amazon ConnectからLambdaがeventsを受け取る際のバリデーションも作りました。
内容としては、以下だけ判別する簡素なものになります。
- Amazon Connectからのeventsであるか
- 受け取るeventsが、Lambdaを組み込んだAmazon Connectのフローと齟齬のないものか
着火の解像度がここでやっと上がる
バリデーションに着手してから初めて、「Amazon ConnectからLambdaの着火」が、「Amazon Connectから"events"をLambdaへ送っている」ということを知りました。
サンドボックスで作ったときは動いたのに、なぜか開発環境で動かない
バリデーションの掛けかたもいくつかあり、今回はそのうちの1つequal to
を使って、バリデーションをかけました。
しかし、テスト環境では動くのに、なぜか動かない……
presence
というバリデーションは使えるのに、equal to
が使えない、という謎のエラーと戦い続けました。
"errorMessage": "Unknown validator: 'EqualToValidator'"
なにがUnknownじゃい。とキレながら、他の色々なものを試しましたが、やはりpresence
以外使えず。
エンジニア同期に、検証したログとともに泣きついたら、なんと。
「どうやっても出るのであれば、そのバージョンにはequal to
ないんじゃない?」
「!?」
参考にしたソースコードから、gemfileの設定も引き継いでいたので、
バージョンが古いものだったそうです。
アップデートは同期が全て助けてくれました。開発初心者が持つべきは、すぐに頼れるエンジニアの友です。
エンジニア先輩の名誉のためにお伝えしておくと、先輩も「どんなに小さくても、いつでも聞きな」と毎週時間を抑えてくれていました。
ただ先輩とは、設計や進め方等で相談することが多く、具体のコーディングは同期を頼っていたのでした。
ログ出力とエラーハンドリング:ログって自分で出すんだ
ログ出力は、すでにある参考コードを書き換えるだけで、「一旦なんか出てきた」という状態だったため、理解と作成にさほど時間はかかりませんでした。
ログというものは、何もしなくても出るようになっていて、何かあったら調べられるようになっていると思っていたので、「自分でログを出させるポイントを作る」というのはなんだか新鮮でした。
Mission:rescueでエラーを拾い切れ
また、起きうるエラーをできるだけ想定して、エラーケースの処理を書いたのですが、最後の最後はどんなエラーが出ても最後のrescue
が拾ってくれる、というエラーハンドリングの設計を初めて知りました。
他の言語だとtry catch
って言うらしいですね。小人さんが助けてくれる感があっていいですね。
「rescueされなかったらどうなります?」
「異常終了する」
「祝日判定って異常終了しちゃだめなんですか?」
「起きうるエラーはできる限り想定して、意図しない挙動が起きないようにするんだよ」
「それはそうだ」
Amazon Connect君は親切なので、「エラーだったら何する?」という分岐が最初から用意されています。
そのため、未だにこんな理解ではありますが、きちんとエラーハンドリングはしました。
テストコード:......FFFFFF あああ
テストは、本当にAmazon Connect上で動くのか?だけだと思っていました。
「テストってAmazon Connectに電話かけて試すやつじゃないんですか」
「それはSTとかUATというやつだな。ほい、RSpec。」
「正常系と異常系ってなんですか……?」
この状態から始まりました。最初のテストは当然ながら、......FFFFFF
が返されました。
ローカルで自分の書いたコードが実行できるなんて、魔法のように便利ですね!と言いながら、後半はずっとテストコードの作成やエラーの修正と戦っていました。
(ローカルでテストできることを知ってからも、本物が動いているところが見たく、よくサンドボックスへデプロイしては動かしてエラーを見て直して再デプロイ、ということをしていました。時間はかかりました。)
mockを作ったり、stab化したり、なんやら難しいことをしてテストしていたはずなのですが、正直この辺は見よう見まねで、エラー文をひたすら直していたらいつのまにか......TTTTTT
となっていました。
今回の祝日判定におけるテストケースは、たったの6つです。
最初に開発を経験するには、良い題材でした。
カバレッジ:100%の最後の関門は最後のrescue
カバレッジなる概念も、今回で初めて学びました。
思想はとっても良く分かります。意味のないコードなどあってはならない。
必要なものだけ残せ、それ以外は消せ、大事なことです。
また、使っていないファイルやコードが見えるので、カバレッジに着手した瞬間に、参考にした元のコードの形から、不要だった部分が大きく削られて、とても見やすくなりました。
ただ、最後の1エリアがどうしても大変でした。
あの最後にあるrescue
に通せず、98%のカバレッジとなってしまうのです。
「想定してないから最後のrescueがあるんじゃないんですか??」
「そうそう、想定しないエラー起こすんだよ!」
「ほええ(それは想定して手前で拾うんじゃ……)」
最終的には、events
のバリデーションをかける際に、バリデーションエラーではなく、「そもそも前提となる所定の形式でevents
が入ってこない(を、想定外とする)」という形に落ち着き、無事100%になったのでした。
プルリクエストは毎回新規作成するものだと思っていた
9割完成したくらいから、プルリクエストを作って、エンジニア先輩に見せる、ということを始めました。
PR作成して、ついたコメントを見て、直して、また新たにPRを作成する。
それを繰り返して4回目くらいで、先輩に言われました。
「毎回最初から見るの大変なんだけど」
「そうですよね、作業ログ貼っても大変だろうなーって思ってました」
「PRって、修正したら新規作成はしなくていいんだよ」
「そうなんですか???」
こんな感じで、プルリクエストの修正は再リクエストしなくていいと学びました。
この話をしたらエンジニア同期が爆笑して、qiitaに載せることを勧めてくれました。
エンジニアの皆様、初心者にPR教えるときは、最初に伝えてあげてください。
そういえば非機能も大事
STが通り、UATに移る段階で、「非機能テスト」なるものを教わりました。
そもそもの処理スピードや、Lambdaが大量に呼び出されるなどした際に、無事かどうかをきちんと試す必要があるとのこと。
それはそうだ、と納得しつつ、「Amazon Connectに電話かけまくるにしても限度あるよな」と思っていたら、エンジニア先輩がささっとコードを書いて、codebuildからシェルを流して、awscliでlambdaをループで呼び出す処理を書いてくれました。
LambdaのInvocation、Throttle、Durationをテストして、Lambdaはコールドスタートだから最初だけちょびっと時間がかかること、自分の作ったコードは電話が殺到しても耐えうるということを知りました。
祝日判定、思っていた何倍も奥が深かったです。
マージ&デプロイ:パイプラインに感動
UTも通り、問題ないことが分かったので、最後のプルリクエストがマージされました。
(プルリクエストを全て新規作成していたので、"最後の"という表現をしています。)
AWSアカウント周りのインフラは全てエンジニア先輩が構築してくれており、
マージされた瞬間に、CodePipelineが動き出して、緑のチェックがついていく様子を一緒に見ました。
CI/CDというものらしい、ということ、これがないと大変だということ、本番環境へはこのパイプラインを通してデプロイする仕組みにしていることなどを教わりました。
1度でオールグリーンになり、本番環境に無事、デプロイされました。
一番ほっとした顔をしていたのは、私よりも先輩だった気がします。
IaCって大事
流れの中で書く機会がなかったのですが、LambdaやPipelineなど、全てIaCで作成しています。インフラ部分はすべて任せましたが、
Serverlessを使ったLambdaの設定や、環境変数の埋め込みなどはやらせてもらいました。
1文字でもミスっていると動かないコードが、容易に編集できてしまうのは怖すぎます。
IaC管理されて、プルリクエストやPipelineの手順を通って更新される仕組みになっていることの大事さは、開発をやった後じゃないと分からないな、と感じました。
学んだこと
具体のコーディングとしての学びも大量にありましたが、それ以上にシステムというものの設計や、「作らないとやってくれない」という感覚が学べたことがとても良かったと思います。
手段がコードであるだけで、開発における設計能力はどこでも必要
カスタマーサービスの仕事上、ノーコードで様々な設定ができるツールを使うことがあります。
Amazon Connectの本体がまさにそのような設計で、IAMを知らなくても権限管理が、処理の切り分けや共通化を知らなくてもコールセンターの処理フローが作れてしまいます。
ただ、「システムってこういうふうに作ると後で変えるのが楽だよね」といった設計への知見があるかないかで、作り方はかなり違ってきてしまうと感じました。
理論として言いたいことは分かっていても、こうやって自分で開発して、体感として何に困るかのイメージがついたことが、とても良かったように思います。
実際に、自分で全部構築したAmazon Connectも、この開発のインプットを通して、何度か作り変えました。
作らないとやってくれない
既製品のツールでは、色々な機能があらかじめ作られています。
エラーをSlackに通知する、ログを勝手に残しておいてくれる……etc.
「祝日判定をするだけでしょう」と思っていましたが、実際には上で書いたような様々なことを考え、祝日判定を正しく行うために必要な周辺機能を用意する必要があります。
祝日判定だけでこんなに奥が深かったのに、事業部で動いているシステムはどれほどなのか、考えるだけで恐ろしいものがあります。
魔法のコード、魔法の「技術」だと思っていたものに、魔法なんてないときちんと理解できたことが、今回の一番の収穫だったと思います。
さいごに
注釈として、私は何もなしに、急に開発を任されたわけではありません。
エンジニア職ではない、ただのAmazon Connectリプレイス担当が、ここまでできたのは、ありがたいことに、以下のような環境が整っていたからです。
- 下地となった、フリーダムなIT研修(私の代が特殊でした、いつかこの話も)
- カスタマーサービスが開発に挑戦する、ということを、必要なものとして受け止めてくれた事業部&会社
- 初心者にやらせてみるために、下準備&援護を頑張ってくださった先輩&同期
また、今回作った祝日判定機能なんて、今年か来年には、きっとAmazon Connectの公式機能として作られていることでしょう。(みなさん欲しいはず……!)
(2024.12.4 追記) 公式機能としてできるようになりました!!
開発した機能は、めでたくお役御免になりました。1年半ありがとう。
2025年からは以下の機能で祝日判定していきます。
経験は無駄にはなりませんが、作ったものがいつ無用になるか、半分ドキドキしつつ、今後のアップデートを待とうと思います。
この開発を通して、カスタマーサービスとしてはとても嬉しいことに、Amazon Connectの無限のポテンシャルも感じられました。
作りたい機能の構想が既にいくつかできており、これからどんどん機能開発を進めていきたいと思います。