オリジナルアプリケーション開発ブログ 【がっこうにっき】 ~第2章~ はじめてのAWSでインフラ構築 ~

2024/06/09に公開

はじめに

 私は2023年12月より、内定直結型エンジニア学習プログラム「アプレンティス」に3期生として参加しています。

https://apprentice.jp/

 現在、アプレンティスの課題として、オリジナルアプリケーションの開発を行っています。

目次

第2章: はじめてのAWSでインフラ構築

 前回の記事では、私が開発中の「がっこうにっき」アプリのコンセプトと初期段階の開発についてお話しました。今回は、AWSでのインフラ構築について学んだことと、その後に直面した様々な技術的な課題、それをどのように解決したかについて詳しくご紹介します。

AWS用語を学校に例えて解説してみよう!

 制作中のアプリが学校系なので、今回私が学習したAWSの用語や仕組みなどを、【学校】という例えで、分かりやすく解説してみようと思います。(ところどころ、例えるのに厳しい箇所もありましたが…)

 まずは、AWSの仕組み全般について、難しそうな用語をできるだけ使わずに、【学校】に例えて解説します!

【リージョン】は、世界中にある【学校】そのもの

 リージョンは、AWSのデータセンターの場所を示します。世界中に複数のリージョンがあり、データをどの地域に置くかを選ぶことができます。

 例えるなら、世界各地にある学校そのものです。

【VPC】は【学校の敷地全体(土地)】のこと

 VPCは「仮想プライベートクラウド(Virtual Private Cloud)」の略で、AWS内で自分だけのネットワークを作ることができます。これは学校の敷地全体のようなもので、外からはあまりよく見えないけれど、中でいろいろな活動が行われる場所です。

 この後の例えで出てくる【校庭】【教室】【図書館】なども、この【学校(VPC)】の中にあります。

【パブリックサブネット】は【校庭】

 パブリックサブネットは、インターネットと直接通信できるネットワークです。外部と直接接触できる場所で、「校庭開放」や「地域のイベント」などで使われる校庭のようなものです。

 つまり、敷地内(VPC)の一部分であり、外部(インターネット)と直接接触する場所でもあります。

【プライベートサブネット】は【教室】

 プライベートサブネットは、インターネットから直接アクセスできないネットワークです。これは学校の教室のようなもので、外部からは直接見えない場所であり、関係者以外が無許可で入ることのできない場所です。

【EC2】は【一般開放されているパソコン教室】(最近無くなってきているけれど)

 EC2(Elastic Compute Cloud)は、AWSが提供する仮想サーバーです。これを使うと、自分のコンピュータを借りてインターネット上でアプリケーションを動かすことができます。

 学校のパソコン室のようなもので、たくさんの人が同時にコンピュータを使うことができる場所です。

【DNS】は【校内ガイドブック】

 DNS (Domain Name System)は、インターネット上のドメイン名をIPアドレスに変換するシステムです。

 校内ガイドブックのようなもので、例えば、「音楽室」を探して、その教室がどこにあるか(校内の特定の位置)を教える役割を果たします。

【Elastic IPアドレス】は【固定電話番号】

 Elastic IPアドレスは固定されたIPアドレスで、EC2インスタンスが停止してもIPアドレスが変わりません。これは学校の固定電話番号のようなもので、いつでも同じ番号で連絡が取れます。

【EBS】は【教室のロッカー】

 EBS(Elastic Block Store)は、EC2インスタンスにアタッチ(継続してしようすること)して使用するブロックストレージです。

 これは学校の教室にあるロッカーのようなもので、生徒が自分の持ち物を保管するための場所です。ロッカーは生徒が学校にいる間も、いない間も使われ続け、常にデータが保存されています。

【S3】は、学校の【図書館】

 S3(Simple Storage Service)はAWSのファイル保存サービスで、データをインターネット上に安全に保存するために使います。ファイルやオブジェクトを大量に保存し、必要に応じて取り出すことができます。

 図書館にはたくさんの本や資料が保管されており、生徒や教師が誰でも簡単にアクセスでき、必要なときに借りて使うことができます。ただし、アクセス制御の設定によっては、貸し出しに制限があり、特定の利用者だけが利用できる場合もあります。

【RDS】は、学校の【データ管理システム】(例えの限界)

 RDSはリレーショナルデータベースサービス(Relational Database Service)で、アプリケーションのデータを構造化して保存し、管理、検索するためのものです。

 例えば、

  • 生徒の成績
  • 出席情報
  • 図書館の貸し出し記録
  • イベントのスケジュールなど

 さまざまな情報を管理しています。このシステムは【S3】と異なり、特定の関係者(教師や学校の管理者など)だけがアクセスできます。一般の生徒や外部の人は簡単にはアクセスできません。これは、機密性の高い情報を保護するためです。

【セキュリティグループ】は学校の【セキュリティシステム】(そのまま!?)

 セキュリティグループは、VPC内のリソースに対するネットワークトラフィックを制御するファイアウォールのような役割を果たします。特定のIPアドレスやポートからのアクセスを許可または拒否することができます。

 例えば

  • 学校に許可なく入って来られないようにする【警備員】
  • 学生証や職員証をカードリーダーにかざして認証された人だけが校内に入れる【電子カードリーダー】
  • 特別な教室や実験室など、特定の資格を持った人だけがアクセスできるようにする【セキュリティカード】

のような存在です。

 次に、【管理者側(学校の先生など)】に関係する用語について解説します。

【IAM】は、【職務分担と権限】のこと

 IAM(Identity and Access Management)はユーザーやグループのアクセス権を管理するシステムです。これにより、誰が何をできるかを細かく設定できます。

 例えば、

  • 校長先生

学校全体の運営を管理し、すべての情報にアクセスできます。全てのデータにアクセスできる最高権限を持っています。重要な決定を下す権限もあります。

  • 体育主任の先生

体育の授業やスポーツイベントの運営を担当します。体育館のスケジュールやスポーツ用具などの管理をする権限を持っています。

  • 生活指導チームの先生たちは…

 というように、IAMは、各ユーザー(校長先生、体育主任など)に適切な権限を割り当て、それぞれの役割に応じてリソースへのアクセスを制御します。IAMを使用することで、学校内のデータやリソースが適切に管理され、不正アクセスが防止されます。

【SSH】は、教職員の【IDカード】みたいなもの

 SSHは「Secure Shell」の略で、インターネットを経由してリモートのサーバーに安全に接続するためのプロトコル(約束事)です。データの送受信を暗号化することで、セキュリティを保ちながらサーバーにアクセスできます。

 例えるなら、教職員のIDカード。校内のさまざまな場所にアクセスするために必要な身分証明書であり、安全にアクセスできるようにするための手段です。(セキュリティグループがアクセス許可を確認)

 最後に、【学校に訪問する人】を例に、インターネット通信の流れを説明していきます。

【Route53】は【受付】

 インターネットユーザー(訪問者)が「本日、学校に訪問します」と連絡をすると、この受付(Route53)で、どの部屋に行くべきかを案内されます。

 このように、Route53は、ドメイン名をIPアドレスに変換し、どのサーバーにアクセスすればよいかを決める役割を担っています。

【インターネットゲートウェイ(IGW)】は【学校の正門】

 VPC内のリソースがインターネットと通信するための出入り口です。これは学校の正門のようなもので、外の世界(インターネット)との出入りを管理します。

 もちろん、ここでも【セキュリティグループ】のチェックを受けます。

【ルートテーブル】は【校内経路案内】

 ルートテーブル(Route Table)は、ネットワーク内でデータの送信先を決定するためのルールを定義します。具体的には、データがどのネットワーク経路を通って目的地に到達するかを決めます。

 例えば、特定の場所(トイレ、図書館、体育館など)にどう行けばよいかを示す看板です。訪問者がどの経路を通って目的地に行けばよいかを教えます。「トイレはこちら→」や「図書館はこちら→」といった案内板を見て、訪問者が目的地にたどり着けるようにします。

 その後、訪問者は【校庭(パブリックサブネット)】を通って、EC【パソコン教室(EC2)】や【図書館(S3)】にアクセスすることができます。

作成した時点で料金がかかるもの

 AWSの各サービスで料金が発生するタイミングを簡単にまとめておきます。

料金のかかるもの一覧
  • EC2 (Elastic Compute Cloud)

 EC2インスタンスを作成すると、起動した時点から使用時間に応じて料金が発生します。無料利用枠もありますが、インスタンスの種類やリージョンによっては料金が発生します。

  • EBS (Elastic Block Store)

 EBSボリュームを作成すると、その容量に応じて料金が発生します。EC2インスタンスにアタッチされているかどうかに関わらず、作成した時点で料金が発生します。

  • RDS (Relational Database Service)

RDSインスタンスを作成すると、稼働時間に応じて料金が発生します。インスタンスの種類やストレージの容量に応じて料金が発生します。

  • S3 (Simple Storage Service)

S3バケットを作成するだけでは料金は発生しませんが、データをアップロードするとそのストレージ量に応じて料金が発生します。データ転送やリクエスト数にも料金がかかる場合があります。

  • Elastic IPアドレス

Elastic IPアドレスを作成し、EC2インスタンスにアタッチしていない(=EC2インスタンスが起動していない)場合、時間単位で料金が発生します。インスタンスにアタッチされている(=EC2インスタンスが起動している)場合は基本的には料金がかかりません。

  • Route 53

Route 53でホストゾーンを作成すると、ホストゾーンごとに料金が発生します。また、DNSクエリの数に応じて料金が発生します。

  • VPC (Virtual Private Cloud)

IAMユーザー、グループ、ロールの作成自体には料金は発生しませんが、IAMで管理するリソースの使用に応じて料金が発生します。

  • 無料利用枠について

 AWSには無料利用枠があり、特定のリソースを一定期間または一定量まで無料で使用することができます。ただし、無料利用枠を超えると追加料金が発生するため、利用状況を確認しながら進めることが重要です。

いざ、インフラ構築へ!

 AWSやインフラなどについての仕組み・手順については、Udemy講座のほかにも、以下のYoutube動画を参考にしました

https://www.youtube.com/watch?v=vetGQktHuAs

https://www.youtube.com/watch?v=HvrIPQ77xRY&list=PLS0SWeRoWAzEo2RffpYiRSSdHmQnTQl95

やっと決まったインフラ構成図

 ローカル環境でしっかりと動作確認をし、コア機能を実装できた状態で、はじめて「本番環境」に向けてのインフラ構築に挑みました。ついに、「localhost:3000」から飛び立つ時が来たのだ…と思ってからが長かったです。

 そもそも「インフラとは?」というところから始まったので、Udemyの講座やYoutubeの動画を観て、その仕組みを理解するところから始めました。

インフラ構成図(初期)

 運用コストも考えて、初期の時点ではこのような構成にしました。また、将来的な拡張にも対応できるように、

インフラ構成図(長期運用)

このような構成も準備しました。

  • サイトやアプリのトラフィックが一定の水準(例えば1日あたりのリクエスト数や同時接続ユーザー数)に達した場合
  • レスポンスタイムが遅くなり、ユーザー体験に影響を与え始めた場合
  • CPU、メモリ、ディスクI/Oなどのリソース使用量が80%以上に達することが頻繁になった場合
  • アプリの重要性が増し、ダウンタイムが許容されなくなった場合
  • 手動での運用管理が複雑化し、自動化や分散処理が必要となった場合

などのタイミングで、移行を検討する予定でいます。

インフラ構築中に直面した問題と解決方法をまとめよう!

 ここからは、私が直面した様々なエラーや問題についてと、その解決方法を書いていきます。

【1】SSH接続できない問題

 上記のように、SSH接続でタイムアウトエラーが発生する場合、いくつかの原因が考えられるようです。

  • EC2インスタンスのセキュリティグループがSSHアクセスを許可していることを確認
  • EC2インスタンスが「running」状態であることを確認
  • Elastic IPアドレスがEC2インスタンスに関連付けられていることを確認
  • プライベートサブネットのルートテーブルの設定が正しいことを確認

 ちなみに、私の場合はこれらを全て正しく設定しても状況が変わらないままでした…。

他に考えられる原因としては、私の使っているOSがWindowsだったので

  • Windows Defender ファイアウォールの設定を確認し、ポート22を許可

  • **セキュリティソフトウェア(ウイルスバスター)**のファイアウォール設定でポート22のトラフィックを許可

 これらも試しましたが、依然としてタイムアウトエラー。途方に暮れながら、もう一度上記の設定を確認していた、その時です。

「あれ?セキュリティグループの【マイIP】が、設定時と現在とで違っているんだけど…」

そうです。私がインバウンドルールに設定したマイIPは、ダイナミックIPアドレスだったようで、一定期間ごとに変更されることがあるのです。(知らなかった…)

こうして私は、定期的にIPアドレスを確認し、必要に応じてセキュリティグループの設定を更新することで、安定したアクセスを確保することに成功しました。

【2】本番環境でGoogleアカウントを選択した後に、なぜかlocalhost:3000にリダイレクトされちゃう問題

 私は、Google認証を次のように開発していました。

  • ユーザーの性質上、Google認証でログインを管理する
  • セキュリティの観点から、DBに新たにパスワード等を保存するようなことはしない
  • RailsのGem 'devise'と、'omniauth-google-oauth2'を使用する

 ローカル環境で上手く動作することを確認したうえで、本番環境にそのまま持っていくと、次のようなことが起こりました。

Google認証エラー

 ローカル環境では上手く動いたのに、本番環境でエラーが起きるということは、

  • GoogleCloudの設定確認
  • redirect_uriの設定確認
  • nginxの設定確認

このあたりに原因が絞られてくるはずです。

 意外と忘れがちなので、まずはGoogleCloudの設定を一緒に確認しておきましょう!

GoogleCloudの設定

GoogleCloudの設定

 私は、①公開状況が「テスト」のままだった ②リダイレクトURIでタイポしていた、という2つのやらかしが発覚しました。しかし、これらを修正しても状況は改善せず…。(他2つの項目は、どちらも問題なし)

 ただ、1つだけ分かったことは、先ほどのこの画面

Google認証エラー

ここで、URLに直接"https://ドメイン名/users/auth/google_oauth2/callback"と入力すると、用意したページにちゃんと遷移できることも分かりました。

 つまり、Googleアカウントを選択後、「続行」ボタンを押した後だけなぜかlocalhost:3000にリダイレクトされる、ということ以外は正常に機能していることが言えます。なんと奇妙な状態でしょう…。

 このエラーに関しては、直ぐには解決できず、しばらくは直接URLに入力して別の機能を実装していました。そして、別件でのエラー対応の過程で、次のような言葉を発見したのです。

「環境変数RAILS_ENVをとくに設定していない場合、Rails は開発環境(development)として実行されます。」

 これを見たときに、突然localhost:3000へリダイレクトされるGoogle認証の問題を思い出しました。

修正前のdocker-compose.ymlは

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:8.0
    volumes:
      - 'db_data:/var/lib/mysql'
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: school_diary_app_development #<= そもそもdevelopmentのままでした…

  web:
    build: .
    command: bash:ssh -c "rm -f tmp/pids/server.pid && bundle exec rails s -b '0.0.0.0'"
    volumes:
      - '.:/myapp'
    ports:
      - '3000:3000'
    depends_on:
      - db

volumes:
  db_data:

このようになっていました。ここに、RAILS_ENV=productionを追記しました。

docker-compose.yml
version: '3'
services:
  db:
    image: mysql:8.0
    volumes:
      - 'db_data:/var/lib/mysql'
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: school_diary_app_production
      MYSQL_USER: school_diary_app
      MYSQL_PASSWORD: ${SCHOOL_DIARY_APP_DATABASE_PASSWORD}

  web:
    build: .
    command: bash:ssh -c "rm -f tmp/pids/server.pid && bundle exec rake assets:precompile && bundle exec rails s -b '0.0.0.0'"
    volumes:
      - '.:/myapp'
    ports:
      - '3000:3000'
    depends_on:
      - db
    env_file:
      - .env
    environment:
      - RAILS_ENV=production #<= これを追記

volumes:
  db_data:

 このように追記したことで、Googleアカウント選択後も、ちゃんと本番環境へとリダイレクトされるようになりました!

【3】docker-compose exec web bin/rails db:migrateできない問題

 DBコンテナへのMySQL接続はできるのですが、なぜかmigrateだけができないという状況に陥りました。

  • database.yml の設定確認
  • docker-compose.ymlの設定確認

=>Docker-compose.ymlのserver名とRailsのdatabase.ymlのhost名が一致していないと正常に繋がらないみたいなのでそこを確認しましたが、どちらも「db」になっていました…。

https://qiita.com/SyoInoue/items/2ed5b3017c517920ec09

  • .envファイルの環境変数の設定確認

=>こちらはGitactionで反映されない設定にしてあるので、nanoエディタを使って直接確認をしました。特にタイポなどもなかった気がするのですが、念のためもう一度入力し直して保存しました。

 すると、今度は

というエラーに変わりました!これは書いてある通り「DBへアクセスするユーザーに権限がない」ことが原因のようです。

① MySQLのコンテナに入り、データベースユーザーに正しい権限が与えられているか確認する

② MySQLコンソールで、権限を付与するSQLコマンドを実行する

③ Docker Composeを再起動(docker-compose down => docker-compose up)する

 これで、マイグレーションに成功しました!(データベースが存在しない場合は、まずはdb:createから)

https://qiita.com/Kiyo_Karl2/items/2ff248621d35c598028f

【4】本番環境でCSSが全部失われた問題

 ようやく本番環境でもアプリが動作するようになったのですが、起動すると、ローカル環境で当てたはずのCSSが全て失われた状態になっていました。

どうやらRailsでは、本番環境でアセット(CSS、JavaScriptなど)はプリコンパイルされる必要があるようです。そこで、次のコマンドを実行しました。

ssh
RAILS_ENV=production bin/rails assets:precompile

すると、

Railsアプリケーションが指定されたディレクトリやファイルにアクセスする権限がない、ということのようです。そこで、権限を付与してみました。

ssh
sudo chown -R ec2-user:ec2-user /home/ec2-user/myapp/...()
  • 上記のコマンドで、/home/ec2-user/myapp/...(略) ディレクトリとその中のすべてのサブディレクトリおよびファイルの所有者を ec2-user ユーザーおよびグループに変更
ssh
sudo chmod -R 755 /home/ec2-user/myapp/...()
  • 上記のコマンドは、/home/ec2-user/myapp/school_diary_docker_app ディレクトリとその中のすべてのサブディレクトリおよびファイルに対してパーミッションを設定。
  • 具体的には、所有者に読み取り・書き込み・実行権限(7)、グループとその他のユーザーに読み取りと実行権限(5)を設定。

 ここで再び、

ssh
RAILS_ENV=production bin/rails assets:precompile

を実行すると、今度はターミナルが固まったまま動かなくなりました…。アセットのプリコンパイルが止まってしまう場合、「メモリ不足」が原因であることが分かりました。

https://tech-savvy.hatenablog.com/entry/2016/04/07/000102

https://qiita.com/snowsunny/items/aaeacf7aecb0121948d4

試しに、EC2インスタンスのメモリの使用状況を確認してみます。

ssh
free -m
total (MB) used (MB) free (MB) shared (MB) buff/cache (MB) available (MB)
Mem: 949 678 63 3 207 121
Swap: 0 0 0
  • 総メモリ(total)が949MB
  • 使用済みメモリ(used)が678MB
  • 空きメモリ(free)が63MB
  • 利用可能メモリ(available)が121MB
  • スワップ領域(Swap)は、設定していないため0

 この状況では、アセットのプリコンパイルどころか、アプリで複数のプロセスが動作する際にはメモリ不足になる可能性が高いということが分かりました。そこで

EBSボリュームのサイズ拡張(8GiB => 16GiB)

スワップファイルの作成

ssh
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048

③ スワップファイルの権限を設定

ssh
sudo chmod 600 /swapfile

④ スワップファイルをスワップ領域として設定

ssh
sudo mkswap /swapfile

⑤ スワップファイルを有効にする

ssh
sudo swapon /swapfile

⑥ スワップの有効化を確認

ssh
free -m

 ここまでやって、assets:precompile --traceを実行すると、

public/assetsディレクトリ内のファイルに対してアクセス権限が不足していることが原因とのことでした。

⑦ public/assets ディレクトリの権限を設定

ssh
sudo chown -R ec2-user:ec2-user public/assets
sudo chmod -R 755 public/assets

⑧ アセットのプリコンパイルを再実行

ssh
RAILS_ENV=production bin/rails assets:precompile

⑨ Dockerコンテナを再起動

ssh
docker-compose down
docker-compose up -d

⑩ Nginxを再起動

ssh
sudo systemctl restart nginx

これで、ようやくCSSが当たる状態になりました。長かった…。

日記を書く画面

おわりに

 今回のブログでは、はじめてのAWSインフラ構築に挑戦し、いくつかの問題に直面しながらも解決方法を見つけて進めていった経験を共有しました。AWSの学習と実践を通じて、多くのことを学びましたが、最も重要なのは「諦めずに試行錯誤を繰り返すこと」の大切さです。

 新しい技術に挑戦することは、時には大変なこともありますが、その分だけ成長を感じられる瞬間も多くあります。このブログを通じて、皆さんが少しでもAWSのインフラ構築に対する理解を深め、挑戦する勇気を持つきっかけになれば幸いです。

次回は、引き続き「がっこうにっき」アプリの機能追加やユーザーインターフェースの改善について詳しくお話しする予定です。どうぞお楽しみに!

Discussion