はじめに
初年度運用費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. 完成形
■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)
トップページ | ブログ一覧ページ | 記事ページ |
---|---|---|
記事検索結果ページは、プログ一覧ページの上部に「検索結果:XXX」を追加 |
こんな感じでエリアに分けておくと実装しやすいです。
トップページ | ブログ一覧ページ | 記事ページ |
---|---|---|
3-2. お問い合わせ画面
お問い合わせ画面はgoogleフォームで作成。
googleフォームを選んだ理由は、
- 自分でフォームの実装が不要
- お問い合わせがあったときにメール通知できる
- お問い合わせ内容をスプレッドシートで管理できる
- スパム対策も一応可能(googleフォームだけだとスパムが来るという報告もちらほらあったので、下記サイトを参考にreCAPTCHAっぽいの実装)
ゆがんだ文字の作成はphotoshopで。
お問い合わせ画面 |
---|
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で対応していない機能を追加で実装しました。
■ブログシステムに必要な機能
・記事投稿・修正・削除
・ユーザー管理
・メディア管理
・フロントページの実装(トップページや記事デザインに沿って実装)
・記事検索ロジック&表示
・トップページや記事へのリンク(管理画面からトップページや記事画面に飛べないのでリンクを追加)
・SEO対応(descriptionの記入欄はあるが、headタグへの追加が必要 )
・ogpタグ(headタグへの追加が必要)
・favicon設定(headタグへの追加が必要)
・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設定は下記のように修正します。
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=laravel_local
DB_USERNAME=phper
DB_PASSWORD=secret
DB_CONNECTION=mysql
DB_HOST=XXXX (←ここの設定を下手順の通り変更)
DB_PORT=3306
DB_DATABASE=laravel_local
DB_USERNAME=phper
DB_PASSWORD=secret
-
Route 53のホストゾーンを開く
-
ドメイン名をクリック([4.5章AWS ECS Serviceの作成]でdb service作成時に設定した名前空間名)
-
タイプ:Aのレコード名をDB_HOSTに設定
フォーマットはこんな感じ⇒「[半角英数字].db.[ドメイン名]」(例. abcdefghijklmnopqrstuvwxyz.db.namespace)
voyagerのconfig設定
画像の保存先を、ローカル開発時はstorage/app/publicに保存して、本番環境(AWS)ではS3に保存するようにしたかったので、下記のようにconfig/voyager.phpでvoyagerの画像保存設定を変更しました。
/*
|--------------------------------------------------------------------------
| Storage Config
|--------------------------------------------------------------------------
|
| Here you can specify attributes related to your application file system
|
*/
'storage' => [
'disk' => 'public'
],
/*
|--------------------------------------------------------------------------
| 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イメージを作成し、コンテナを起動しました。
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でホストとコンテナをマウントするのではなく、ホスト側のフォルダをコンテナにコピーするようにしただけです。
FROM nginx:1.20-alpine
COPY ./infra/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY ./backend /work
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クラスターの作成
項目名 | 設定値 |
---|---|
クラスター名 | 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サービスの作成
- 作成したクラスターをクリック
- "サービス"タブを選択し、作成をクリック
- 下記設定として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になっているタスクを見つける
2.タスクをクリックして、コンテナのステータスを確認(失敗の原因になったコンテナのステータスの欄にエラーメッセージが表示される)
3.詳細を開き、[状況の理由]に表示されたメッセージを確認する
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のタスクを起動後、サイトにアクセスすると下記エラーとなりました。
Doctrine\DBAL\Driver\PDO\Exception
という記載からしてデータベース周りのエラーです。
.envのDB_HOSTの設定が間違っている場合にこのようなエラーとなります。
ローカル環境では下記のように指定しますが、本番環境では、db serviceを独立させているため、この指定ではDB_HOSTを見つけることができません。
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=blog
DB_USERNAME=develop
DB_PASSWORD=secret
[4-1章]に記載している通り、DB_HOSTは下記手順で設定する必要があります。
-
Route 53のホストゾーンを開く
-
ドメイン名をクリック([4.5章AWS ECS Serviceの作成]でdb service作成時に設定した名前空間名)
-
レコード名が「[半角英数字].db.[ドメイン名]」(例. abcdefghijklmnopqrstuvwxyz.db.namespace)をDB_HOSTに設定
さいごに
AWS ECS上でブログシステムを構築してみました。簡単な構成のわりにまとめてみるといろいろ躓いたなぁと。誰かの参考になればうれしいです。