6
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ブログシステムをAWS ECS上で動かす【500円/月】(開発環境構築〜デプロイ)

Last updated at Posted at 2021-10-27

はじめに

初年度運用費500円/月のブログシステムをAWS ECS上に構築しました。環境構築~デプロイまでに実施したことや、トラブルシューティングをまとめました。

####こんな人向け
・AWS上でコンテナを動かす方法をざっくり理解したい人
・1つのEC2インスタンス(t2.micro)内に、webサーバー、APサーバー、DBサーバーを構築する方法を知りたい方
・Dockerでローカル開発環境は作ったけど、AWS上でどうやって動かすかわからない人
※ブログシステムをゼロから作るっていう記事ではないです

主な使用技術

  • Docker
  • Nginx
  • php8.0
  • Laravel8
  • Voyager
  • MySQL8.0
  • AWS ECS
  • AWS ECR
  • AWS Route 53
  • AWS Cloud Map
  • AWS Certificate Manager

目次

1. 完成形(システム構成)
2. 開発環境構築
3. ブログシステム開発
 3-1. ブログデザイン
 3-2. お問い合わせ画面
 3-3. サイトルーティング設計
 3-4. 管理画面ライブラリVoyagerの導入とカスタマイズ
4. デプロイ
 4-1. デプロイ時の.envファイルとvoyagerのconfig設定 
 4-2. DockerイメージをAWS ECRにプッシュする
 4-3. AWS ECSタスク定義の作成
 4-4. AWS ECSクラスターの作成
 4-5. AWS ECSサービスの作成
5. ドメイン取得とSSL化
6. セキュリティ上、気を付けておくこと
7. 運用費用見積もり
8. トラブルシューティング

1. 完成形

image.png

■EC2インスタンス内(図赤枠内)の概要
・ECSサービスを2つ作成(front service、db service)
 →db serviceを独立させることでデータ永続化させる
・LaravelからMySQLへの接続は、ECS Service間の通信のため、AWS Cloud Mapを使用して、DBサービス用のPrivate IPを発行し、そのIPにアクセス
参考:VPC 内の Amazon ECS サービス間のネットワーキング

■通信概要
【サイトへのアクセス(オレンジ線)】
・domain.comにアクセス
・DNS(AWSサービス:Route 53)でApplication Load Balancer(以下、ALB)にルーティング
・上記がhttp(port:80)でのアクセスの場合は、ALBでhttpsにルーティング(下記、補足)

ブラウザのURL入力欄に、「domain.com」としてアクセスすると、「http://domain.com」 にアクセスします(Chromeで検証)。domain.comにサーバー証明書を発行していても、https://domain.comとしてアクセスしないとSSL化されないので、http(80)でアクセスがあったときはhttps(443)にルーティングするようにしています

・ALBからwebサーバーの80ポートにアクセス
・APサーバー(php)とDBサーバーの処理結果をユーザーのブラウザに返す

【Dockerイメージの更新(青線)】
開発PCからAWS ECRにログインしてイメージをpushする。手順は下記サイトに記載の通りです。

【SSH接続(青線)】
EC2インスタスにSSH接続して、コンテナにログイン。基本的に、front serviceはDockerイメージを更新してバージョンアップをしますが、db serviceは初回のイメージプッシュ後はSSH接続してDBを操作します。(イメージを更新するとデータが消えてしまう)

2. 開発環境構築

Docker + Nginx + Laravel(php8) + MySQL8.0の環境をローカル環境に構築しました。
下記サイトが非常に参考になります。

3. ブログシステム開発

3-1. ブログデザイン

デザインはあんまりできないので、シンプルかつスタンダードなデザインにしました。(by Figma)

トップページ ブログ一覧ページ 記事ページ
image.png image.png image.png
記事検索結果ページは、プログ一覧ページの上部に「検索結果:XXX」を追加

こんな感じでエリアに分けておくと実装しやすいです。

トップページ ブログ一覧ページ 記事ページ
image.png image.png image.png

3-2. お問い合わせ画面

お問い合わせ画面はgoogleフォームで作成。
googleフォームを選んだ理由は、

  • 自分でフォームの実装が不要
  • お問い合わせがあったときにメール通知できる
  • お問い合わせ内容をスプレッドシートで管理できる
  • スパム対策も一応可能(googleフォームだけだとスパムが来るという報告もちらほらあったので、下記サイトを参考にreCAPTCHAっぽいの実装)

ゆがんだ文字の作成はphotoshopで。

お問い合わせ画面
image.png

3-3. サイトルーティング設計

自分で追加したルーティングは下記の通り。記事やユーザー等の登録、編集、削除は次章で導入する管理画面ライブラリVoyagerで自動で設定されるため自分では設定する必要はありません。

URI HTTPメソッド 画面
/ GET トップページ
/posts GET ブログ一覧ページ
/posts/{id} GET 記事ページ
/contact GET お問い合わせページ
/posts/{search} GET 記事検索結果ページ

3-4. 管理画面ライブラリVoyagerの導入とカスタマイズ

ブログシステムの管理画面を実装するためにVoyagerという管理画面ライブラリを使いました。

laravelが入っていて、php artisan migrateが通る環境であれば、下記2つのコマンドを実行すれば利用可能です(お手軽)

composer require tcg/voyager
php artisan voyager:install --with-dummy

--with-dummyを付けることでいくつかのダミーデータと一緒にVoyagerをインストールしてくれます。

VoyagerはWordpressのようなCMSとは違いますが、ブログシステムのベースとなる機能(ユーザー管理、メディア管理、記事作成、固定ページ作成)がそろっています。
voyager_movie.gif

下記で:x:になっているVoyagerで対応していない機能を追加で実装しました。

■ブログシステムに必要な機能
:o:記事投稿・修正・削除
:o:ユーザー管理
:o:メディア管理
:x:フロントページの実装(トップページや記事デザインに沿って実装)
:x:記事検索ロジック&表示
:x:トップページや記事へのリンク(管理画面からトップページや記事画面に飛べないのでリンクを追加)
:x:SEO対応(descriptionの記入欄はあるが、headタグへの追加が必要 )
:x:ogpタグ(headタグへの追加が必要)
:x:favicon設定(headタグへの追加が必要)
:x:meta robotsタグ(headタグへの追加が必要)

###3-3-1. 躓いたところ
・voyager管理画面の表示がやたらと遅い
ローカル環境でvoyagerをインストールして使っていると、画面の切り替えに平均5秒くらいかかってしまう現象が発生しました。キャッシュを設定するなど高速化をいろいろ試したんですが、結局は、「wsl2から、Windowsのファイルシステム上のファイルへのアクセスが遅い」ことが原因でした。公式のドキュメントでも、WindowsホストのディレクトリをマウントするよりもLinuxのファイルシステムをマウントするほうを勧めています。私はWSL2バックエンドをHyper-Vバックエンドに戻して解決しました。

4. デプロイ

4-1. デプロイ時の.envファイルとvoyagerのconfig設定

.envファイル

[4.5章AWS ECS Serviceの作成]で、db service作成時に「サービスの検出のDNSレコード」を設定します。これにより、front serviceからdb serviceに接続することができます。
接続する際、.envのDB設定は下記のように修正します。

.env(ローカル環境)
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel_local
DB_USERNAME=phper
DB_PASSWORD=secret
.env(デプロイするとき)
DB_CONNECTION=mysql
DB_HOST=XXXX (←ここの設定を下手順の通り変更)
DB_PORT=3306
DB_DATABASE=laravel_local
DB_USERNAME=phper
DB_PASSWORD=secret
  1. Route 53のホストゾーンを開く

  2. ドメイン名をクリック([4.5章AWS ECS Serviceの作成]でdb service作成時に設定した名前空間名)
    image.png

  3. タイプ:Aのレコード名をDB_HOSTに設定
    フォーマットはこんな感じ⇒「[半角英数字].db.[ドメイン名]」(例. abcdefghijklmnopqrstuvwxyz.db.namespace)

voyagerのconfig設定

画像の保存先を、ローカル開発時はstorage/app/publicに保存して、本番環境(AWS)ではS3に保存するようにしたかったので、下記のようにconfig/voyager.phpでvoyagerの画像保存設定を変更しました。

.conf(ローカル開発時)
    /*
    |--------------------------------------------------------------------------
    | Storage Config
    |--------------------------------------------------------------------------
    |
    | Here you can specify attributes related to your application file system
    |
    */

    'storage' => [
        'disk' => 'public'
    ],
.conf(デプロイ時)
    /*
    |--------------------------------------------------------------------------
    | Storage Config
    |--------------------------------------------------------------------------
    |
    | Here you can specify attributes related to your application file system
    |
    */

    'storage' => [
        'disk' => 's3'
    ],

4-2. DockerイメージをAWS ECRにプッシュする

開発環境では、下記のdocker-compose.ymlファイルをdocker compose upして各Dockerイメージを作成し、コンテナを起動しました。

docker-compose.yml
version: "3.9"
services:
  app:
    build: ./infra/php
    volumes:
      - ./backend:/work

  web:
    image: nginx:1.20-alpine
    ports:
      - 8080:80
    volumes:
      - ./backend:/work
      - ./infra/nginx/default.conf:/etc/nginx/conf.d/default.conf
    working_dir: /work

  db:
    build: ./infra/mysql
    volumes:
      - db-store:/var/lib/mysql
    ports:
      - 33060:3306

volumes:
  db-store:

このときにイメージは作成しているので、docker commitして変更内容を含んだイメージをECRにプッシュすれば良いかなと考えましたが、トラブルシューティング8-1に記載しているように、volumeを含んでいるためうまくコンテナを起動することができませんでした。
そこで、ECRにプッシュするDockerイメージ作成用のDockerfileを別途作成しました。volumesを使っているwebイメージとappイメージの2つ作りました。
変更点は、volumesでホストとコンテナをマウントするのではなく、ホスト側のフォルダをコンテナにコピーするようにしただけです。

Dockerfile(web)
FROM nginx:1.20-alpine

COPY ./infra/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY ./backend /work
Dockerfile(app)
FROM php:8.0-fpm-buster
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

ENV COMPOSER_ALLOW_SUPERUSER=1 \
  COMPOSER_HOME=/composer

COPY --from=composer:2.0 /usr/bin/composer /usr/bin/composer

RUN apt-get update && \
  apt-get -y install git unzip libzip-dev libicu-dev libonig-dev libpng-dev gnupg libjpeg-dev libfreetype6-dev && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* && \
  docker-php-ext-install intl pdo_mysql zip bcmath && \
  docker-php-ext-configure gd --with-freetype --with-jpeg && \
  docker-php-ext-install -j$(nproc) gd exif


COPY ./infra/php/php.ini /usr/local/etc/php/php.ini

COPY ./backend /work

RUN chmod -R 777 /work/storage

WORKDIR /work

AWS ECRへのプッシュ手順は下記参照(再掲)

4-3. AWS ECSタスク定義の作成

docker-compose.ymlで、APサーバー(app)/webサーバー(web)/DBサーバー(db)を定義しています。最初に示した構成図の通り、front service(app+web)とdb service(db)を作成したいので、それぞれのタスク定義を用意します。

■タスク定義を作成する上で意識しておくこと

インスタンスのメモリとvCPU
今回使用するインスタンスタイプ(=t2:micro)のスペックは、vCPU:1, メモリ:1GiBで、うまく各コンテナに配分しないとメモリエラーとなるので、事前に配分を検討しておきます。今回は下記のようにしました。
 - appコンテナ:0.333vCPU, 400MiB
 - webコンテナ:0.333vCPU, 150MiB
 - dbコンテナ:0.333vCPU, 400MiB
※メモリ3つ分足し算しても1GiBとならないのは、コンテナ以外が使用するメモリを残しておかないといけないからです。50MiB残せば正常に起動できました。

どのイメージをタスク定義に含めるか
今回はfront serviceとdb serviceの2つのECS Serviceを作成しました。
front service用のタスク定義には、appイメージとwebイメージを含め、
db service用のタスク定義には、dbイメージを含めました。
db serviceを独立させておくことでデータを保持できるようにしています。3つのイメージをひとつのタスク定義に含めてしまうと、appイメージやwebイメージを更新したとき、dbイメージも更新されていまいデータが消えてしまいます。

■ front service用タスク定義

項目名 設定値
タスク定義名 front-definition
タスクロール ecsTaskExecutionRole ※タスク実行ロール(ECRをpullしたり、CloudWatch Logsへの書き込み権限を与えます。参考
起動タイプの互換性 EC2
タスクメモリ 550
タスクCPU 0.666vCPU
コンテナの定義1 コンテナ名:app
イメージ:appイメージのURI
メモリ制限(MiB):(ハード制限)400
ログ設定:チェックON
コンテナの定義2 コンテナ名:web
イメージ:webイメージのURI
メモリ制限(MiB):(ハード制限)150
ポートマッピング:80(ホストポート) 80(コンテナポート) tcp(プロトコル)
スタートアップ依存順序:app(コンテナ名) START(状態)
ネットワーク設定:リンク app
ログ設定:チェックON

※webコンテナはappコンテナと接続する必要があるため、ネットワーク設定のリンクの"app"を指定しています。あと、appが起動してからwebコンテナを立ち上げないとappが見つからないエラーとなるため、スタートアップ依存順序を設定しています。

■ db service用タスク定義

項目名 設定値
タスク定義名 db-definition
タスクロール ecsTaskExecutionRole ※タスク実行ロール(ECRをpullしたり、CloudWatch Logsへの書き込み権限を与えます。参考
起動タイプの互換性 EC2
タスクメモリ 400
タスクCPU 0.333vCPU
コンテナの定義 コンテナ名:db
イメージ:dbイメージのURI
メモリ制限(MiB):(ハード制限)400
ポートマッピング:3306(ホストポート) 3306(コンテナポート) tcp(プロトコル)
環境変数:MYSQL_DATABASE(blog), MYSQL_PASSWORD(secret), MYSQL_ROOT_PASSWORD(secret), MYSQL_USER(develop)
マウントポイント:ソースボリューム(db-store) コンテナパス(/var/lib/mysql)
ログ設定:チェックON
ボリューム 名前(db-store) ボリュームタイプ(Bind Mount)

4-4. AWS ECSクラスターの作成

  1. Elastic Container Service(ECS)の画面を開く。

  2. クラスターの作成をクリック
    image.png

  3. 「EC2 Linux + ネットワーキング」を選択
    image.png

  4. 下記設定として"作成"

項目名 設定値
クラスター名 cluster-test
プロビジョニングモデル オンデマンドインスタンス
EC2インスタンスタイプ t2.micro
インスタンス数 1
EC2 AMI ID Amazon linux 2 AMI
ルートEBSボリュームサイズ(GiB) 30
キーペア 事前に作成しておいたキーペアを選択
※ここで選択しておかないとインスタンスにSSH接続できない。EC2 コンソールのネットワーク & セキュリティのキーペアで事前に作成しておく
ネットワーキング VPC, サブネット, セキュリティーグループを選択する
コンテナインスタンスIAMロール ecsInstanceRoleを選択(ない場合は自動的に作成してくれる)

4-5. AWS ECSサービスの作成

  1. 作成したクラスターをクリック
  2. "サービス"タブを選択し、作成をクリック
  3. 下記設定としてfront serviceとdb serviceを作成する
    ※front serviceからdb serviceに接続できるようdb serviceから先に作成する

■front service

項目名 設定値
起動タイプ EC2
タスク定義 front service用のタスク定義を選択 リビジョンも最新のもの
クラスター 4-3で作成したクラスターを選択
サービス名 front
サービスタイプ REPLICA
タスクの数 1
最小ヘルス率 0
最大率 100
デプロイサーキットブレーカー 無効
デプロイメントタイプ ローリングアップデート
配置テンプレート AZバランススプレッド
後はデフォルト

■db service

項目名 設定値
起動タイプ EC2
タスク定義 db service用のタスク定義を選択 リビジョンも最新のもの
クラスター 4-3で作成したクラスターを選択
サービス名 db
サービスタイプ REPLICA
タスクの数 1
最小ヘルス率 0
最大率 100
デプロイサーキットブレーカー 無効
デプロイメントタイプ ローリングアップデート
配置テンプレート AZバランススプレッド
サービスの検出の統合の有効化 ON
名前空間 新しいプライベート名前空間の作成
名前空間名 名前空間名を記入します。作成しているシステムやサービスの名前などでよいかなと思います。
クラスターVPC VPCを選択します
サービスの検出サービスの設定 新しいサービスの検出サービスの作成
サービスの検出名 db
ECSタスク状態の伝達の有効化 チェックON
サービスの検出のDNSレコード DNSレコード型:SRV(自動的に選択)
ネットワークアドレス:コンテナ名とポート(自動的に選択)
コンテナ: db:3306:tcp
TTL:60
後はデフォルト

※「サービスの検出の統合の有効化」をONにして、各種設定することで、Route 53のホストゾーンに「名前空間名」にした名前でドメインが追加されます。タイプAのレコード名がDB_HOSTになり、appから接続する場合は、この文字列を.envのDB_HOSTに設定すれば接続できます。

5. ドメイン取得とSSL化

この手順でドメインを取得してSSL化しました。

これに加え、Application Load Balancerに、httpリクエストをhttpsにリダイレクトする設定をしました。

6. セキュリティ上、気を付けておくこと

すべてのコンソールログイン可能なIAMユーザーに対して MFA を有効化する (参考)[AWS でのユーザーの MFA デバイスの有効化](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_mfa_enable.html)
インスタンスへの接続に使用する認証情報の管理 これはごくごく当たり前のことですが、IAMユーザーに紐づくアクセスキーIDやシークレットアクセスキーなどの認証情報はきちんとセキュアな環境で保管する
必要なポート、拠点のみ接続を許可する 今回の場合、http(80),https(443)のアクセスはすべてのIPで許可。ただし、インスタンスにssh接続可能なIPは自分のPCのみに設定。
データの保護 **S3データの保護**:機密性の高いデータを保管するわけではなく、また最悪データが消失したとしてもデータはPCに保管されていて復元可能なので、特に暗号化やスナップショット保存は実施しない **dbデータの保護**:機密性は高くはないが、dbのデータが失われるとまずいので、dbデータは定期的にsqlとしてエクスポートして復元できるようにしておく。(本当は自動化しておくのがベターだと思う)
アプリケーションを最新にする Nginx,Laravel,mysql,voyagerは最新にしておく。
セキュリティパッチ適用 OS(Amazon Linux 2)の脆弱性情報を[Amazon Linux Security Center](https://alas.aws.amazon.com/)でウォッチ。クリティカルなものがリリースされたらパッチ適用。 (参考1)[AWSセキュリティベストプラクティスを実践するに当たって適度に抜粋しながら解説・補足した内容を共有します](https://dev.classmethod.jp/articles/explanation-aws-security-best-practices/) (参考2)[Amazon EC2 セキュリティレシピ](https://dev.classmethod.jp/articles/ec2-sec/)

7. 運用費用見積もり

AWS Pricing Calculatorで[create estimate]をクリックして見積もり。
初年度は安い、ただ2年目以降は初年度の10倍以上・・

リージョン:Asia Pacific(Tokyo)
見積もり時期:2021年10月
見積もり額:
初年度:4.4USD/月(52.8USD/年)
以降:44.44USD+α(<10USD)/月(533.28USD/年)

(無料枠の確認⇒AWS 無料利用枠)

利用サービス 利用詳細 見積もり額 無料利用枠対象 無料枠
EC2 OS: Linux
Instance Type: t2.micro(100% Utilization/Month)
12か月間:0USD
以降:14.70USD
12か月無料
EBS gp2 50GB 12か月間:2.4USD
以降:6.00USD
30GB/月
DomainRegistration ドメイン取得 12.00USD(年間) - -
ECR イメージサイズ:487MB 0USD(無料枠内) 500MB/月
Elastic Load Balancing Application Load Balancer 12か月間:無料
以降:17.74USD+α(LCU使用分課金。そこそこアクセスがあっても1,000円未満(たぶん))
12か月無料
Route 53 2.0 HostedZone 1.00USD - -
S3 ストレージ 12か月間:0USD(無料枠に収まる想定)
以降:5USD未満
12か月無料
5GB標準ストレージ
20,000件のGetリクエスト
2,000件のPutリクエスト

8. トラブルシューティング

何度かECS タスクの起動に失敗したので、現象と対処をメモ。

・エラーメッセージ確認手順
1.クラスターを開き、"タスク"タブで[必要なタスクのステータス]がStoppedになっているタスクを見つける
image.png
2.タスクをクリックして、コンテナのステータスを確認(失敗の原因になったコンテナのステータスの欄にエラーメッセージが表示される)
image.png
3.詳細を開き、[状況の理由]に表示されたメッセージを確認する
image.png

8-1. cannot mount volume over existing file, file existsとなりECSタスクが起動できない

既存のファイルにマウントすることができない、というメッセージです。マウントしようとした先にすでにdefault.confが存在していたので怒られたみたいです。ローカル環境だとホストとコンテナをマウントしないとコードの変更がコンテナ側に反映されないので必要でしたが、本番環境では不要なので、本番環境にpushするイメージにはmountを含まないようにして対応しました。

8-2. getaddrinfo failed: Name or service not known

front serviceとdb serviceのタスクを起動後、サイトにアクセスすると下記エラーとなりました。
image.png
Doctrine\DBAL\Driver\PDO\Exceptionという記載からしてデータベース周りのエラーです。
.envのDB_HOSTの設定が間違っている場合にこのようなエラーとなります。
ローカル環境では下記のように指定しますが、本番環境では、db serviceを独立させているため、この指定ではDB_HOSTを見つけることができません。

.env
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=develop
DB_PASSWORD=secret

[4-1章]に記載している通り、DB_HOSTは下記手順で設定する必要があります。

  1. Route 53のホストゾーンを開く

  2. ドメイン名をクリック([4.5章AWS ECS Serviceの作成]でdb service作成時に設定した名前空間名)
    image.png

  3. レコード名が「[半角英数字].db.[ドメイン名]」(例. abcdefghijklmnopqrstuvwxyz.db.namespace)をDB_HOSTに設定

さいごに

AWS ECS上でブログシステムを構築してみました。簡単な構成のわりにまとめてみるといろいろ躓いたなぁと。誰かの参考になればうれしいです。

6
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?