RM-BLOG

IT系技術職のおっさんがIT技術とかライブとか日常とか雑多に語るブログです。* 本ブログに書かれている内容は個人の意見・感想であり、特定の組織に属するものではありません。/All opinions are my own.*

【AWS】漫画Webサイトをサーバーレスで作ってみた

f:id:rmrmrmarmrmrm:20200824110831p:plain趣味の一環で描いてる漫画を公開するためのWebサイトを、AWSで作成した。
ちなみにここです。見てね。
まだ一部作成途中だったり、改善の余地がある部分はあるが、とりあえず一旦公開という形に踏み切る程度には準備できたので、ここで作成の経緯や過程、苦労話等をまとめておきたい。

はじめに

趣味の一環で漫画を描いていたが、この漫画を公開するにあたって、どこかの投稿サイトに画像アップロードして見てもらうだけじゃ面白みがないし、かといって今さら自サイトを拵えるのにレンタルサーバなんてのも…と思ったので、勉強がてらAWSで作ってみた。
ただAWSといっても、EC2たててELBで負荷分散して…というんじゃ、やはりレガシー感があって面白くない。
いわゆる「サーバレスアーキテクチャ」というのを体感する丁度良い機会だろう、と思って、これに挑戦してみることにしたのだった。

ハイレベルアーキテクチャ

いきなりでなんだが開発したWebサイトの構成としては以下の通りとなっている。(アイコンはAWSが配布してるやつを使用)
f:id:rmrmrmarmrmrm:20200824110831p:plain

  • Webアプリケーションとしての動作部分を構成しているのはS3、Lambdaになる。
    CSSや画像、Javascript等の静的コンテンツをS3、他の動的コンテンツはLambdaで動かす。
    LambdaはNode.jsランタイムで、aws-serverless-expressというライブラリを使用している。
  • ELBはLambdaの前段に配置し、CloudFrontにELBをオリジンとして登録して動作させる。
    また、CloudFrontでカスタムヘッダーを設定してELBに流し、ELB側のルール設定でカスタムヘッダの有無をチェックして、カスタムヘッダがないか指定の値でない場合、強制的に403を返すように設定しており、これによりELBのオリジンドメインへの直接アクセスは基本的に出来なくなっている。
  • S3とELB(->Lambda)の前段にCloudFrontを配置し、コンテンツをキャッシュ配信させて画面レスポンス向上を図る。
    CloudFonrtからオリジンへのアクセスはHTTPSを強制させる。
    また、オリジン側のバックエンドからのエラーを丸めてエラーページとしてレスポンスする設定を共通でここに入れている(いろいろな場所にエラー設定箇所が分散するのが嫌だっただけだが)
    あとエンドユーザーのhttpでのアクセスはhttpsにリダイレクトさせるよう設定。
  • S3のバケットはパブリックアクセスを禁止しており、アクセスポリシーでCloudFront経由からの特定のフォルダ配下に対するGetObjectのみ許可している。
  • Certificate Managerで証明書を取得しており、通信はすべてSSLになっている。
    ただ、AWS側の仕様で何故かCloudfrontにいれる証明書はus-east-1リージョンのものしか選択できず、一方ELBは自分と同じリージョン(今回で言えばap-northeast-1;東京)にある証明書しか選択できないため、ワイルドカード証明書を両方のリージョンでそれぞれ取得している。
  • Webサイトそのものとは関係がない裏側の部分だが、CodeCommit+CodeBuildのパイプライン(CodePipeline)を構築しており、git pushすると自動的にデプロイまで動く。
    自分で体感してみて思ったが、これ凄い便利。
    Herokuとかの動きを沸騰とさせる。(まあHerokuよりデプロイ反映に時間かかるけど)
    実際自分で開発作業をしてみて、非常に感動した。
    これは画期的な仕組みだ!w
  • ちなみにローカルでの開発作業は、↑のaws-serverless-expressのサンプルにある、「app.local.js」というのに少し手を加えて、ローカルでexpressサーバたてて動かしてる。 ローカルにはS3がないので(まあアクセスしてもいいんだけど)同一ドメイン配下にあるリソースにアクセスするという状況を仮想的に実現するために、Expressのstaticルーティングを使っている。
    こんな感じ↓
...  
// app.listenする前に下記のコードをいれてローカルで動かすときのみstaticルーティングを追加  
app.use('/assets', express.static('assets'))  
...  
app.listen(port)  
console.log(`listening on http://localhost:${port}`)  

このローカルの「assets」の下にCSSとか画像が入っている、という感じ。
なお、この「assets」というフォルダは本番環境ではS3に配置されるので、Lambdaのデプロイパッケージには含まれない。(CICDの流れにおいてデプロイ先として分離している)
ただCodeCommitのリポジトリとしては同一に管理している。

難しかったところ

  • 構成の検討。
    最初は実はAPI Gateway+Lambdaの構成で、LambdaでHTML文字列つくって、API Gatewayをtext/htmlでレスポンスするという構成で、割と無理やりWebページを作っていた。
    ググった感じの昔の記事だとLambdaをWebアプリとして利用する場合のやり方としてそういうのを見つけるのが圧倒的に多かったんだよね。。
    ただAPI gatewayという名前に反して(?)無理やりHTML作ってるような気がしていて、違和感はずっとあった。
    そのあとLoad BalancerとLambdaを連携させるAWS公式の記事が見かって、あーこの方が「っぽい」かも、と思ってそっちに切り替えた。
    LambdaはやはりAPIのバックエンドとしての利用用途が多いみたいで、Webアプリケーションとして利用している事例はググってる感じではあまり見つからない。
    今回の構成も、そういう意味だと、Webアプリケーション的には正解なのか(正解というか無理のない構成事例なのか)わからない。
    まあ動いてるからいいかという感じ(現場猫
  • CloudFrontへのアクセスのHTTPS化
    「同じドメインの証明書を別リージョンで取得できる」というのを知らなくて、CloudFrontにいれる証明書がus-east-1限定なもんだから、最初は「ELBとかも含めて全部us-east-1リージョンで作らないといけないのか…」と思ってた。
    そんなことなかったね。。
    ただそのあとも、CloudFrontのオリジナルドメイン(~cloudfront.net)なら動くけど、代替ドメインじゃ動かないとか、オリジンとのSSL通信が失敗してて証明書のSSL通信ポリシーが間違ってんのかとか、なんか色々試行錯誤した思い出はある。(もう細部は忘れたが)
  • CloudFrontのBehaviorの設定。
    これに加えて、誤ったBehaviorの設定によるレスポンスがキャッシュされており、設定見直し後に何回テストしても同じエラーが帰ってきたりして、原因追及が難航した。
    こういうのが多かったのでかなりの回数Invalidation作った気がする。。。
    こんなに頻繁にInvalidation作ってるんじゃCDNの意味がねえじゃねえか…と思った…
  • CloudFrontのDefaultルートの存在。
    これに"/"をいれていると、最終的にオリジンに向かうリクエストの末尾が"//"になってしまい、オリジン側が「そんなリクエスト定義ねえよ」と怒ってくる。(まあ定義作っとけば動いたんだろうけどさ)
    f:id:rmrmrmarmrmrm:20200824003819p:plain
    ちなみにここ↑のことである。(DistributionのGenerallタブ)
    ここに"/"をいれてると、最終的に末尾が"//"になってしまう。

    Defaultルートって未入力でも設定更新できたっけ…と思ったらできた。
    なんか未入力だと更新できないって怒られた記憶があったのが…間違いだったかな…

  • CloudFrontのカスタムヘッダをELBまで流す方法。
    ググっても具体的なやり方が出てこなくて、試行錯誤を繰り返した感じである。
    CloudFrontのカスタムヘッダんところにいれておけば勝手に流れてくれるんじゃねえのかと思ってたら違った。
    Behaviorのほうに「リクエストヘッダをオリジンに流す」って設定入れないといくら設定してもそもそも流れていかないのねこれ…

    f:id:rmrmrmarmrmrm:20200824003835p:plain
    ちなみにここ↑のことである。 Behaviorを作成しようとしたときの初期状態はリクエストポリシーが未選択なので、外から送られてきたリクエストはヘッダやクエリ文字列含め、一切オリジンに送られない。
    画像は初期でAWSに用意されている「とりあえずヘッダもボディも全部送る」ポリシーである。

やはりCloudFront周りが多いかな。。。
この手のリソースを使うのは経験上初めてだったので勝手が掴みづらかったのも大きいかもしれない。

未だによくわかってない(いつかなんとかしたい)ところ

  • CodePipelineでは最初デプロイ部分にCloudFormationを使おうとおもっていたが(ググったらこのケースがいっぱい出てくるので)、何回やってもうまくいかず諦めた。
    うまくいかないというのは、「(Lambda Function) already exists」というエラーが毎回出るのである。
    こんなん↓
    f:id:rmrmrmarmrmrm:20200824003910p:plain
    既存のLambda Functionのコードを「更新」したいんだからスタック作成時点でLambda関数が存在しているのは当たり前だと思うんだが何故かうまくいかない。
    見てる感じだとどうも「関数の新規作成」を試行しようとしてエラーになっているようであるが、いやだから新規作成じゃなくて更新がしたいんだよ!!…という想いがつうじてくれない。
    CloudFormationだけを単独で(AWS CLIで)実行しても同じエラーになるので、まあ多分ymlの書き方が悪いかパイプライン上での設定がなんか悪いんだろう。
    ちなみに今はCodeBuildのbuildspec.yml内でaws lambda update-function-codeを書いて実行させ、無理やり更新している(もはやBuildステージの枠を超えている…)
    まあ、「git pushをトリガーにコード固めてLambda更新」がとりあえず実現できているので一旦今は良しとしているが、いつかこの原因を追求したいなあと思っている。
  • CloudFrontより後ろが死んでた場合は上記のエラーがなんとかしてくれるんだろうが、現時点では、CloudFront自体が死んでた場合の考慮が全くされていない。
    これはRoute 53に手を加えれば何かしら実現できるのだろうが、詳しく探せていない。
    そして、「というかそもそもそこまで考慮が必要なほどのシステムか?これ?」と自問すると、正直重い腰になってしまうのは否めない。

    • そういう意味ではもっと言うとDR(Disaster Recovery)の考え方も皆無である。
      基本的に東京リージョンでうごかしているので、大災害で東京が壊滅した場合、このシステムは死ぬ。
      でもそんなときは俺自身、自分の漫画サイトがどうのこうの言ってる場合じゃないだろうし、「そもそもそこまで考慮が必要なほどのシステムか?これ?」と自問すr(ry
  • この構成にはDBが、RDB、NoSQL含め登場しない。
    というのもこのレベルのアプリケーションならDBいらないな、と思ったためである。
    画像をS3に配置している分、残りの動的コンテンツはLambdaのデプロイパッケージに十分含められる程度の容量しかなく(現時点ではコンソール上でコードをインラインで編集できるレベル)わざわざDBを使う必要性を感じなかった。
    …というのがまあ、取り繕って言えば理由なのだが、実際のところ、サーバレスアプリケーションにおいて、DBのうまい使い方が分からなかった。
    いわゆるサーバありアプリケーション(っていうの?言い方がわからん)では、WebサーバにDBへのコネクションプールが常駐していて、処理のたびに毎回接続しなおすなんてことはしないと思うのだが、サーバレスは、リクエストのたびにインスタンスが作られるので、DBがあったとしても、コネクションをリクエストのたびに毎回貼りにいくことになるのではないか?という懸念があった(実際の動作は知らないのでなんとなくで言っている)
    そうなるとDBを用意して毎回そこに繋ぎに行くより、自分の腹の中にコンテンツをため込んでおいて、それをそのまま使ったほうが早いのは間違いないはずで、容量的にもそれで動くならそれでいいじゃん、というのが自分の中での結論になってしまった。
    ただこれは上述した通り具体的な動作や仕様に対する理解がない状態に基づく一時的な結論なので、今後、サーバレスアーキテクチャにおけるDBの使い方に関しては、理解を深めていきたいと思っている次第である。

これからなんとかしようと思っているところ

  • 「ページトップに戻る」アンカーの実装。
    表示上、スクロールバーが一番上だと出てこないが、ちょっとでもスクロールすると画面右下に出てくるアレである。
    ググるjQueryのライブラリはちょこちょこ出てくるのだが、このサイト、実はjQueryを使ってないので(一部のライブラリが内部的に使ってるところはあるようだが、サイト自体がjQueryを利用している箇所はない。要するにjquery.min.jsを用意していない)、このためだけにわざわざjQueryをいれるのに抵抗がある。
    結果的に実装できていない。
    jQuery使わなくて住む純正JavaScriptCSSのライブラリないかなー、と探してる状況である。
    いずれにせよそのうち入れようと思っている。
  • レスポンシブデザイン。
    実はこのサイト、viewportをいれてないのである。
    手元のiPhoneiPadで見る分にはそれほど崩れないで観れたし、逆にviewportいれたときのほうがレイアウト崩れが激しかったので、viewportを外している。
    ただviewportに関しては自信の理解が不足している部分が多分にあると思っていて、恐らくなんか間違ってるんだろうと予想している。
    この辺の追及がいろいろ不足している。
    • というかもっと根本的なことを言うなら、見てもらえば分かるのだが、そもそもWebデザインが直視できないレベルのひどいもんだというのがそもそもの問題としてある。
      今回、実際に自分でサイトを作ってみて痛感したが、Webデザインというのは本当に難しい。
      これをセンス良くかっこよく仕上げるには、確かな専門技術が必要で、俺にはそれが皆無というのが今回本当によくわかった。
      まあ、今回、こうしていい実験台wを手にできたというのを+思考に変えて、いろいろ試行錯誤して練習してみるしかないのだろう…頑張ります。。
  • 細かい話だが、漫画ページ入り口の各話へのリンクが自分が当初思い描いていたように動いておらず、何かいい方法はないものかと考えている。
    これ、画像の上に文字(第xx話)がのっかってて、画像全体をaタグで囲ってリンクにしてるんだけど、
    画像部分は初期状態でopacity:0.5でhoverでopacity:1.0にするCSS書いてるので、マウス乗せるorスマホでタップした瞬間、選んだ話のタイルが明るくなるようにしている。
    ただ、画像の上の文字は、当然「画像より上」にあるので、文字の部分をマウスオーバーすると、画像のhover属性がなかったことになって(文字のほうの属性が優先されて)opacity:0.5に戻ってしまう。
    本当なら画像を含むあの辺全体にマウスオーバーしてるときは、画像の上だろうと文字の上だろうとopacity:1.0になっててほしいのだが、これがなかなか難しい…
    この辺が「なんちゃってWebデザイン」の知識による実装の限界なんだろうなあ。
    jqueryとか使って、文字にhoverしたときにイベント発火させて、下の画像のopacity操作…とか無理やりやればできなくなさそうだが、ちょっとなあ…
    画像の上に文字載せる構造の時点でこの問題を解決するのは難しそうなので、ちょっとリンクのデザインを見直さないとだめかな~と考えていたりしている次第…
  • CloudFrontのログ(要するにアクセスログ)。
    色々な意味でなんとかしたい(というかしないといけない)
    今のところ無制限にS3に吐きまくってるが、延々貯め続けているわけにもいかないし(→S3のポリシー変更の検討が必要)、
    吐いてるならばそれはそれで活用したいものだ(→ログの抽出・加工・集計処理が必要)
    後者に関してはローカルからaws sdkとかCLI使ってもいいと思ってて実際ちょこちょこやってたんだけど、API使いすぎたせいでS3のFree Tierもう尽きるとかメールきてビビっている。
    そういえばS3へのPUTならイベントで拾ってLambdaに投げられるかもなと思って今それをもとにした構想中。。
    最終的にはアクセスカウンタみたいな形で実装できるのがベストではあるが…これはそのうちかな…
  • SEO対策のためのアレコレ。
    例えばOGPに関しては途中まで書いたんだが面倒くさくなってやめてしまった(SNS投稿時の画像とか用意するのがだるくて…)
    あとrobots.txtもまだ用意していない…とか。。
    でもせっかくだからなんかやらないとな~でも他にいろいろやりたいこともあるしな~というジレンマとの格闘中である。
  • CICDパイプラインのラストでTwitterアカウントにも投稿をpublishできたりすると良いなと薄っすら考えている。
    Twitterへの画像付きのPublishはInstagram連携で作ったときに既に経験があるし、やろうと思えばこれは多分すぐできる。(実装に着手できる)
    ただこれはまあ、そのうちかなあ。。
  • これも直近ではなく将来的な構想段階の話なんだが、いつかSPA(Single Page Application)にしたい。
    というか最初は実はAPI Gateway+Lambdaで、やり取りは全部JSONにして、コンテンツ部分だけを書き換えるような構想もしてはいたのだ。 ただちょっと調べた感じ、GoogleBotJavascriptに対応していなくて、Botのページアクセス時に人が見ているようなHTMLが生成されず、結果的にSEO的に不利、というような情報を得たので(これはその後さらに調べたら「もう改善していて気にする必要はない」という話だったが)まあ、やめた。
    付け加えると、そもそも昨今のシステム作りのトレンドを知らない状況でいきなりこんなのに手を出しても痛い目見るだけだろうから、最初は自分の慣れてるやり方でやってみよう、と日和った部分もあったのだが。
    SPAは内部的な作りを大幅に変えなければならないので、まあ、長い目で見て、そのうちかなあー、という心持でいる。

懸念しているところ

  • 現時点では、AWSの12か月無料期間の中でサービスを利用しているので、これらの構成でも費用はほぼかかっていない。
    Route 53のドメイン維持料が毎月1$くらい(100円ちょい)かかってる程度である。
    が、12か月を超えた先の費用状況が気にはなっている。
    12か月無料期間の対象で、かつ割と使ってるのはS3、ELB、CloudFrontか。
    特にELBとCloudFrontはCalculatorに入力するパラメータがよくわからなくて費用算出ができていない。(まああんまり調べられてもいないのだが)
    一応現時点では、Budget画面の予想見る感じでは全然気にすることないのだが、これが「12か月」考慮済の数値だとしたらそもそも信用できないし…
    APIとかで取れたりするのかな…調べてみるか…
    まあでも「さすがにこんなに使わねえだろう」という予想に基づいてかなり多めの数値を入れてみてもS3もELBも両方月2~3$程度だったから問題ないかもしれんが…
    なんとなく先行き不安なのである。。。
    • 費用というと、Lambdaの「月100万リクエストまで無料」というのは凄いなと思う。
      こんな漫画サイト、ELBのヘルスチェック分を加味しても絶対そんなにリクエストこないし、(ワンパンマンのfc2サイトでようやく日に4万アクセスくらい=月換算で120万アクセス)そもそも前段にCloudFrontがいるから全リクエストがLambdaまで行くこともない。
      こんなアプリケーションで人も来ない時間帯にすら寂しくEC2動かしておくか、馬鹿馬鹿しくてやってられん。
      そういう意味でも、サーバレスアーキテクチャを選んだのは正解だったようだ。
      費用の安さやAWSのRecomendであること等から考えても、アプリケーションはサーバレス時代になっている(わざわざサーバを用意する時代は終わった)のかなー、と思ったりした。
  • セキュリティ全般に対する漠然とした不安がある。
    システム規模としてはしょぼいし、内容も大したことないシステムではあるが、インターネットに公開するシステムである以上は、その意識を持っておかねばならないだろう。
    一応ググって入手できた情報をもとに基本的なセキュリティ対策はしているはずだが、どこかに穴がないのか、不安は尽きない。
    専門家の方がいるなら金払ってでも(額によるw)見てもらいたいレベルのものではある。(自分の勉強にもなるしね)
    • そういえばサイト公開して数日後に、/.git/に対するドイツだかスウェーデンだかからのアクセスがあった。(当然結果はエラーだったが)
      おもしれえことするなと思った一方で、こういう危機に日常的に晒されているという状況を認識しなければなるまい。

終わりに

今は「とりあえず動くものを作り切った」ばかりの状態で、上に書いてるようにいろいろ手を加えなければならないところはまだまだある。
ただ逆に言うと自分自身ですらまだまだ改善の余地を見つけられる程度にこの作業にハマっているのも事実なのだ。
そういう意味で、今回の開発作業はとても面白かった。
完全個人の趣味の開発、という背景が「好き勝手自由に作っていい」という状況を生んでおり、これが普段の仕事での開発に比べて遥かにストレスフリーだった、というのは要因の一つとしてあるんだろうなと自己分析している。(付け加えるなら、これが無料利用の12か月期間内で、何やってもほとんど金がかからない無敵状態だったというのもあるとは思う)
また、サーバレスというモダンな響きのするアーキテクチャや、CloudFront等のCDNは自分にとって新しい概念で、習得は大変だったが(まだ全然わからん部分のほうが多いが)使っているのは楽しかったし、
またフロント部分ではHTML5のお作法や(headerタグ、mainタグ、footerタグ、navタグとか今回初めて知って使った)SEO対策のところなど、遥か昔で止まっているHTMLの知識をアップデートできたのも楽しかった。
総じて、システム開発という新たな趣味の形を得られたと思っている。
この趣味は面白い。w
今後も継続していきたい。

余談

漫画の内容もWebサイトの構成もプロの技に比べると全然大したことないので、第三者から見ると「こんなしょぼいコンテンツとサイトに何をそんなマジになっちゃってんの(´・ω・`)」と思う人もいるかもしれない。
実際自分でも冷静に客観的に自分を見つめなおすとそういう気持ちがないわけではない。
CloudFrontでCDNやら、CICDパイプラインやら、こんな程度の漫画サイトに必要か?と自問すると確かにな。。と思うところがある。
ただ重要なのはこれが趣味だという点で、漫画もサイトも完全なる「自分本位」である、というのが全ての土台にいるというのを忘れてはならない。
まず「自分の好きなことをやる」という非常に独善的な気持ちが大前提として一番最初にきているので、周りがどう思うのかはあまり関係なく、二の次なのだ。
極端な話、毎日のアクセスが俺一人であるような閑古鳥の泣くサイトだとしても構わないのだ。
いやちょっと言い過ぎた、やはり少し誰かに来てもらいたい気持ちはあるのが、とにかく、重要なのは自分が何をしたいか、だと思っている。
この気持ちは大事にしていきたい。(やりすぎるとただの痛い人になってしまうから空気の読み方も重要なのだがw)