- 1. 解説動画
- 2. 概要
- AMI (Amazon Machine Image)
- 構成図
- リソース一覧
- 命名規則
- 公式URL
- 3. 解説手順
- 4. NginxとRubyコンテナを起動して動作確認
- 手順
- ディレクトリ構成
- 注意点
- コマンド
- 5. 予習
- 6. FAQ(随時追加)
AMI (Amazon Machine Image)について学んでいきましょう!
1. 解説動画
※動画の画質が悪い場合は、歯車から720p以上の画質を選択ください
2. 概要
AMI (Amazon Machine Image)
【AMIとは】
AMIとはEBSスナップショット(EC2にアタッチするEBSボリュームのバックアップ)を含むマシンイメージのことです。EC2インスタンスは必ずAMIを指定して起動します。AMIは、AWSが提供していたり、第三者が提供して販売していたり、EC2をバックアップするイメージで自分で作ることもできます。
【今回の狙い】
前回作ったEC2 [viaelb]の上で、一例としてNginxとRubyのコンテナが動くように手でコマンド操作した後、AMIを作ります。EC2をバックアップするイメージです。それを元に次回Launch TemplateとAutoscaling Groupを使って、自動的に複数台起動させますのでお楽しみに。
構成図
今回の構成図はこちらです。EC2 [viaelb]への前後の通信経路を青色の矢印で表示しました。 ※小さくて見づらいので、右クリックして新しいタブで開いてください。
リソース一覧
繰り返しで恐縮ですが、いつもの表形式でも書いておきます。
リソース | 目的 | 備考 |
---|---|---|
AMI (Amazon Machine Images) | EC2インスタンスを起動するのに必要な「マシンイメージ」※CFn未対応 | ①AWSが作成したもの ②Red Hatなどの第三者が作ったりライセンス付きで販売しているもの ③自分でEC2インスタンスから作ったもの など様々ある。必ず何かのAMIを元にEC2インスタンスを起動する |
命名規則
Classmethodさんの命名規則 https://dev.classmethod.jp/articles/aws-name-rule/ に準拠します。
公式URL
ELB
- Docs: Amazon マシンイメージ (AMI) - Amazon Elastic Compute Cloud
- CFn: 未対応
- CLI: ec2 — AWS CLI v2 Command Reference > *-image
3. 解説手順
- EC2 [viaelb]とRDSをもし停止していたら起動する(起動に数分かかるので待たず、次に進む)
- EC2 [viaelb]の上でNginxとRubyコンテナを起動して動作確認 👉 下記4.
- AMIを作る
- EC2インスタンスを選択 > Instance state > Stop instance
- EC2インスタンスを選択 > Actions > Image and templates > Create image
- Image nameとImage description: [SystemName]-prod-viaelb-YYYYMMDD(日付) を入力 > Create imageボタンをクリック
4. NginxとRubyコンテナを起動して動作確認
NginxとRubyのDockerイメージを作成するための準備 → DBのセットアップ → Dockerイメージ作成 → Dockerコンテナの起動 → 動作確認 の流れで行います。プログラミングスクールで最も採用されているRubyをサンプルにしましたが、結構面倒な手順になってしまいました...😭
手順
- EC2 [viaelb] に接続する(コンソール画面かAWSCLIで)
- dockerコマンドをssm-userで使えるようにする
- Dockerビルド前の準備
- appのDockerビルド
- appコンテナでDBのセットアップ
- ファイル編集が終わったところで最後にappとwebのDockerビルド
- 既存のNginxコンテナを確認して停止
- docker-composeでNginxとRubyのコンテナを起動して確認
- コマンドでテスト(HTTPヘッダーを確認)
- ブラウザでテスト(ユーザーの作成・変更・削除が可能です)
ディレクトリ構成
/home/ssm-user/awsmaster/ 配下
/app/* -> app=Rubyイメージに展開するファイル
/containers/ -> 各イメージの設定ファイル
/app/ -> app=Rubyイメージの設定ファイル
/Dockerfile -> イメージ作成の手順
/entrypoint.sh -> イメージ起動時のスクリプト
/web/ -> web=Nginxイメージの設定ファイル
/Dockerfile -> イメージ作成の手順
/nginx.conf -> Nginxの共通設定
/default.conf.template -> Nginxで起動するWebサーバの設定
/docker-compose.yml -> docker-composeの設定
注意点
①下記コマンド手順の中でSecrets ManagerのSecretを2種類使い分けています。
- 手順3.で
SecretForRDSAwsmaster-************
(RDSの中に作るDBのユーザー) - 手順5.で
SecretForRDS-************
(RDSの管理者ユーザー)
2種類異なるので、混同しないよう注意してパスワード等を参照してください!
②もし途中でうまくいかなくなった場合は、次のコマンドで一度リセットしてから再度やり直してみてください。
## (前提) EC2にログインした状態で
## ディレクトリごと削除
sudo rm -fr ~/awsmaster
## Dockerコンテナを全停止し、イメージ・コンテナ・ネットワーク・ボリュームを全削除
docker container stop $(docker container ls -q)
docker image rm -f $(docker image ls -a -q)
docker system prune --volumes -f
## コマンドの途中で rails db:create が出てきたら実行せず、
## 代わりに rails db:reset を実行してください
- dockerコマンドのリファレンス: https://docs.docker.com/engine/reference/commandline/docker/
- docker-composeコマンドのリファレンス: https://docs.docker.com/compose/reference/
③EC2 [viaelb] の中身は⑪ECSの教材でも使いますので、EC2インスタンスは消さずに、そのまま残しておいてください。
※例えばEC2 [viaelb] のCFnスタックを更新すると、AMI IDが最新のものに変わった場合、EC2インスタンスが作り直されますのでご注意ください。
コマンド
上から順番にコメント文も確認しながら進めてください。
## 1.【途中から再開の場合も毎回実行】コンソール画面からConnectするか、
## AWSCLIでStart Sessionする ※操作前にEC2 [viaelb]を起動させておくこと!
aws ssm start-session --target [i-*** Instance ID] ### [Change Instance ID]
## 【途中から再開の場合も毎回実行】EC2で作業ディレクトリに移動
bash
mkdir -p ~/awsmaster && cd ~/awsmaster
## 2. dockerコマンドをすぐ使えるようにする
sudo usermod -a -G docker $(whoami)
## グループの変更がすぐに反映されないので回避策
exec sudo -i -u $(whoami)
cd ~/awsmaster
## 3. Dockerビルド前の準備
## ディレクトリの作成
mkdir -p app
mkdir -p containers/app
mkdir -p containers/web
## 以下 cat <<EOF > のヒアドキュメント・標準出力の機能を使って、ファイルを作成します
## それぞれ EOF まで複数行まとめてコピペして、エンター(改行)を入力ください!
## \ と $ はその前に \ をつけてエスケープしています
## \ はファイルに出力されない文字なのでご注意ください
##
### [Change] と書かれた箇所は *全て* ご自身のものに置き換えてください
## ↓Secrets Manager で SecretForRDSAwsmaster-************ を開いてください
## ※YouTube動画では誤って SecretForRDS の方を開いていますが間違いです。申し訳ありません
## Secrets の値は Retrieve secret value ボタンをクリック
## Ref. https://ap-northeast-1.console.aws.amazon.com/secretsmanager/
##
## ※注意 docker-compose.ymlにパスワードなどを書くのはセキュリティ的に望ましくない
## ですが、実際の本番環境ではECSなどを使いますので、今回は勉強のためとご容赦ください
################################
### [Change: SecretForRDSAwsmaster > host]
MYSQL_HOST=awsmaster-prod-rds.cluster-************.ap-northeast-1.rds.amazonaws.com
### [Change: SecretForRDSAwsmaster > database]
MYSQL_DATABASE=awsmaster
### [Change: SecretForRDSAwsmaster > username]
MYSQL_USER=awsmaster
### [Change: SecretForRDSAwsmaster > password]
MYSQL_PASSWORD=********************************
### [Change: Route 53 > HostedZone]
RAILS_CONFIG_HOSTS=awsmaster.jp
cat <<EOF > docker-compose.yml
services:
app:
build:
context: .
dockerfile: ./containers/app/Dockerfile
environment:
MYSQL_HOST: ${MYSQL_HOST}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
RAILS_ENV: development
RAILS_CONFIG_HOSTS: .${RAILS_CONFIG_HOSTS}
volumes:
- ./app/:/awsmaster/
ports:
- "3000:3000"
container_name: app
restart: always
web:
build:
context: .
dockerfile: ./containers/web/Dockerfile
command: /bin/bash -c "envsubst '\$\$NGINX_BACKEND' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'"
environment:
NGINX_BACKEND: app
volumes:
- ./containers/web/nginx.conf:/etc/nginx/nginx.conf
- ./containers/web/default.conf.template:/etc/nginx/conf.d/default.conf.template
ports:
- "80:80"
depends_on:
- app
container_name: web
restart: always
EOF
################################
cat <<EOF > app/Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 7.0'
EOF
################################
touch app/Gemfile.lock
################################
cat <<EOF > containers/app/Dockerfile
FROM public.ecr.aws/docker/library/ruby:3.2
RUN apt-get update -qq && \\
apt-get install -y --no-install-recommends --no-install-suggests default-mysql-client && \\
apt-get clean && \\
rm -rf /var/lib/apt/lists/*
WORKDIR /awsmaster
COPY app/Gemfile app/Gemfile.lock /awsmaster/
RUN gem update bundler && \\
bundle install
COPY app/ /awsmaster/
COPY containers/app/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
EOF
################################
cat <<EOF > containers/app/entrypoint.sh
#!/bin/bash
set -e
# Remove a potentially pre-existing server.pid for Rails.
rm -f /awsmaster/tmp/pids/server.pid
# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "\$@"
EOF
################################
cat <<EOF > containers/web/Dockerfile
FROM public.ecr.aws/nginx/nginx:1.25
COPY containers/web/nginx.conf /etc/nginx/nginx.conf
COPY containers/web/default.conf.template /etc/nginx/conf.d/default.conf.template
EOF
################################
cat <<EOF > containers/web/nginx.conf
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
server_tokens off;
map \$http_cloudfront_forwarded_proto \$real_ip_temp {
~http? \$http_x_forwarded_for;
default '\$http_x_forwarded_for, dummy';
}
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '\$real_ip - \$remote_user [\$time_local] "\$request" '
'\$status \$body_bytes_sent "\$http_referer" '
'"\$http_user_agent" "\$forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
proxy_buffer_size 32k;
proxy_buffers 50 32k;
proxy_busy_buffers_size 32k;
include /etc/nginx/conf.d/*.conf;
}
EOF
################################
cat <<EOF > containers/web/default.conf.template
upstream backend {
server \${NGINX_BACKEND}:3000;
}
server {
listen 80;
server_name localhost;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# add_header X-Content-Type-Options nosniff always;
# add_header X-Frame-Options SAMEORIGIN always;
# add_header X-XSS-Protection "1; mode=block" always;
## Module ngx_http_realip_module
## http://nginx.org/en/docs/http/ngx_http_realip_module.html
set_real_ip_from 10.0.0.0/8;
real_ip_header X-Forwarded-For;
set \$real_ip \$realip_remote_addr;
set \$forwarded_for -;
if (\$real_ip_temp ~ "([^, ]+) *, *[^, ]+ *\$") {
set \$real_ip \$1;
set \$forwarded_for '\$http_x_forwarded_for, \$realip_remote_addr';
}
client_max_body_size 30M;
location ~* ^/ {
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# add_header X-Content-Type-Options nosniff always;
# add_header X-Frame-Options SAMEORIGIN always;
# add_header X-XSS-Protection "1; mode=block" always;
## nginx - no resolver defined to resolve s3-eu-west-1.amazonaws.com - Stack Overflow
## https://stackoverflow.com/questions/49677656/
# resolver 169.254.169.253 valid=30s;
proxy_cookie_path ~^/(.*)\$ "/\$1; Secure";
proxy_set_header Host \$host;
proxy_pass http://backend;
}
}
EOF
################################
## rails new(初期設定)※数分かかります
docker-compose run --no-deps app rails new . --force --database=mysql
## Rubyコンテナに展開するファイルが作られたことを確認
ls -l app/
## パーミッション修正
sudo chown -R $(id -u):$(id -g) app/
## ファイルのバックアップ
mv app/config/database.yml app/config/database.yml.original
################################
cat <<EOF > app/config/database.yml
default: &default
adapter: mysql2
encoding: utf8
pool: 5
ssl_mode: required
host: <%= ENV['MYSQL_HOST'] %>
database: <%= ENV['MYSQL_DATABASE'] %>
username: <%= ENV['MYSQL_USER'] %>
password: <%= ENV['MYSQL_PASSWORD'] %>
development:
<<: *default
test:
<<: *default
production:
<<: *default
EOF
################################
## ファイルの修正。doneまでまとめてコピペしてください!
## Rails6から導入されたBlocked hostエラーを回避するための設定を追加します
## .awsmaster.jpのようにドットをドメイン名の前に書くことで「サブドメインを含む」という意味になります
for rb in $( ls app/config/environments/*.rb ); do
perl -pi -e 's/^end$/\n # Allow requests from domain ENV.fetch("RAILS_CONFIG_HOSTS").\n config.hosts << ENV.fetch("RAILS_CONFIG_HOSTS")\n\n # Allow requests from docker container.\n config.web_console.whitelisted_ips = "172.16.0.0\/12"\nend/' ${rb}
done
################################
## 4. appのDockerビルド ※数分かかります
docker-compose build app
## 5. appコンテナでDBのセットアップ(ここからappコンテナの中で実行)
docker-compose run --no-deps app /bin/bash
## RDSでユーザー作成 ※操作前にRDSを起動させておくこと!
mysql --ssl -h ${MYSQL_HOST} -u root -p -e \
"DROP USER IF EXISTS '${MYSQL_USER}'@'%'; \
CREATE USER '${MYSQL_USER}'@'%' IDENTIFIED BY '${MYSQL_PASSWORD}'; \
GRANT ALL ON \`%\`.* TO '${MYSQL_USER}'@'%'; \
SHOW GRANTS FOR '${MYSQL_USER}'@'%';"
## Enter password: が表示されたらMaster user(root)のパスワードを入力
## ↓Secrets Manager で SecretForRDS-************ の password
## ※先ほどは SecretForRDSAwsmaster でした。今回は別の方なのでご注意ください
## Ref. https://ap-northeast-1.console.aws.amazon.com/secretsmanager/
## Secrets の値は Retrieve secret value ボタンをクリック
## 次のように表示されたらOK。awsmasterはご自身の${MYSQL_USER}に置き換わります
## これで全DB(`%`.*)への全権限(ALL PRIVILEGES)が付与されました
## 実際の現場では全DBではなく、実際に使うDBのみに権限を絞った方が良いですね
## +--------------------------------------------------+
## | Grants for awsmaster@% |
## +--------------------------------------------------+
## | GRANT USAGE ON *.* TO `awsmaster`@`%` |
## | GRANT ALL PRIVILEGES ON `%`.* TO `awsmaster`@`%` |
## +--------------------------------------------------+
## RDSでDB作成
## ▼ ①初めて実行する場合
rails db:create
## ▼ ②rails db:create後に全部削除して再度やり直す場合
# rails db:reset
## 次のように表示されたらOK。awsmasterはご自身の${MYSQL_DATABASE}に置き換わります
## Created database 'awsmaster'
## Database 'awsmaster' already exists
## Rubyの動作確認用にscaffoldを実行。Rubyのファイルを編集します
rails generate scaffold user name:string email:string
## RDSでDB更新(DBマイグレーションと呼びます)
rails db:migrate
## 次の内容が表示されたらOK
## CreateUsers: migrated
## appコンテナ終了(ここまでappコンテナの中で実行)
exit
## 6. ファイル編集が終わったところで最後にappとwebのDockerビルド
## パーミッション修正
sudo chown -R $(id -u):$(id -g) app/
## Docker再ビルド ※数分かかります
docker-compose build
## 7. 既存のNginxコンテナを確認して停止
docker container ls
## 上記でNginxコンテナが表示された場合のみ、次の2行を実行する
docker container stop nginx
docker container update --restart=no nginx
## 8. docker-composeでNginxとRubyのコンテナを起動して確認
docker-compose up -d --remove-orphans
docker container update --restart=always app web
docker container ls
## 9. コマンドでテスト(HTTPヘッダーを確認)※初回は応答に時間がかかるので注意
curl -IXGET http://localhost/
curl -IXGET http://localhost/users
curl -IXGET http://elb.[ご自身のドメイン名]/
curl -IXGET https://elb.[ご自身のドメイン名]/
curl -IXGET https://elb.[ご自身のドメイン名]/users
## 10. ブラウザでテスト(ユーザーの作成・変更・削除が可能です)
## https://elb.[ご自身のドメイン名]/
【変更履歴】
- 2022-06-03:
- Dockerのコマンドを次の通り変更
docker ps
→docker container ls
docker stop
→docker container stop
docker update
→docker container update
- Dockerベースイメージのバージョンを最新にアップデート
ruby:3.0
→ruby:3.1
nginx:1.19
→nginx:1.22
- 2022-06-05:
- DockerベースイメージをDocker HubからECR Publicに変更
- Railsのバージョンを
6.1
から最新の7.0.3
に変更 - それに伴い、
nodejs
とyarn
のインストールが不要になった - apt-getインストール時のキャッシュファイルなど不要ファイルの削除
- 2023-07-04:
- Railsのバージョンを
7.0.3
から7.0
に変更(パッチバージョンは自動で最新が選ばれるようにするため) - Dockerベースイメージのバージョンを最新にアップデート
ruby:3.1
→ruby:3.2
nginx:1.22.0
→nginx:1.25
- DB接続時にSSL暗号化を必須に
app/config/database.ymlで
ssl_mode: required
を追加 mysqlコマンドで--ssl
引数を追加 - 2023-09-25:
- docker-compose.ymlでイメージ名を定義するため、
image: awsmaster-app
とimage: awsmaster-web
を追加 - 2023-11-30:
- docker-compose.ymlで
MYSQL_HOST
など値の書き換え漏れを防ぐために、ファイルの外に環境変数として事前に定義する方式に変更 - 2024-01-20:
- RDSでユーザー作成後、
GRANT
にIDENTIFIED BY '${MYSQL_PASSWORD}'
を付けていたが、MySQL8.0(Aurora 3)では非対応になったのでその部分のみ削除 - 2024-04-06:
- docker-compose.ymlで
version: "3"
が非推奨で不要なため、行削除 - containers/app/Dockerfileで
COPY app/Gemfile
で始まる2行が冗長なため、1行に統合 - 2024-05-28:
- DB接続時に
DROP USER IF EXISTS
を追加 - 2024-10-26:
- 警告表示が出てしまうため、docker-compose.ymlの
image: awsmaster-app
とimage: awsmaster-web
を削除 - 作業をやり直した場合に
docker-compse up -d
でエラーになる場合があるため、--remove-orphans
を追加 - 2025-01-15:dockerコマンドを使えるようにするコマンドで
ssm-user
→$(whoami)
に変更
5. 予習
- (CFnでのリソース作成作業はありません)
- 解説動画の通り、手で設定・作成してください。作成中に分からなかった設定項目については調べたり、質問のためにメモしておいてください ※途中でエラーになった場合は、エラーメッセージをSlackでください (AWSの画面が変わっている場合は同じ設定項目を探してください)
- 【宿題】下記の項目について調べて、次回のZoomで説明してください (公式の情報が優先で、分からない場合はブログ等を参照ください)
- 解説手順で作成したAMIとEBSスナップショットをAWSの管理画面上で確認
- 有料で販売されているAMIがあることをEC2インスタンス作成画面で確認
- (AMIのベースとなっている)EBSスナップショットの料金体系
- EC2にて
docker image ls
で保存されているDockerイメージの確認 - EC2にて
docker container ls
で今動いているDockerコンテナの情報を確認し、各列が何を意味しているか? - Nginxの
default.conf.template
でStrict-Transport-Security "max-age=31536000; includeSubDomains"
というHTTPヘッダーを追加しました。これの意味は? - 【応用課題】余裕のある方は実際に挑戦して頂くと勉強になると思います! ※時間の都合でZoomで解説はしませんので、答え合わせをされたい方はSlackでDMください
Strict-Transport-Security
の動作はChromeのデベロッパーツール(他ブラウザも同様)で確認できます。http://elb.[ご自身のドメイン名]
とアドレスバーに入力してhttpでアクセスして、HTTPステータスコードなどを確認ください- Dockerコンテナのセキュリティ向上策として、
root
ではなく一般ユーザーで起動することが推奨されています。上記のサンプルでは対応を割愛しましたが、一般ユーザーでのコンテナ起動を試してみてください
6. FAQ(随時追加)
セキュリティ対策のポイント(関連するツールやリソース)を順に挙げると、
- Dockerfileのベストプラクティスチェック(hadolint)
- Dockerイメージの脆弱性チェック(Trivy、Dockle)
- Dockerコンテナで使うパラメータを安全に保管(Secrets Manager、Parameter Store)
- IAMユーザーやロールのアクセス制御(IAM)
- ネットワークの制御(Security Group、Shield Standard、CloudFront、ALB、NAT Gateway、VPC Endpoint)
- L7レイヤー=HTTPリクエストのチェック(WAF)
などが必要になりますね。詳細は以下の2つを参照ください。
- AWSコンテナ設計・構築[本格]入門 株式会社野村総合研究所 https://www.amazon.co.jp/dp/4815607656/ p.122~ セキュリティ設計 p.379~ WAF設計 p.423~ Trivy/Dockeによるセキュリティチェック
- そこの Dockerfile 書いてるあなたちょっと待って、そのコンテナって安全ですか? AWS Summit 2022 (公式)YouTube 動画 https://youtu.be/5qLUuh9mfC0 (非公式)Qiita 文字起こし https://qiita.com/moritalous/items/e4364e97e85abfc40fe1
- 上記解説手順にありますように、RDSは起動していますか? 起動後に再度試してみてください
- 上記コマンドの上にある注意点の3点を実行してみてください
- それでも解決しない場合は、EC2インスタンスにログイン後、次のコマンド実行結果をSlackでルビコンにお送りください
curl -v http://localhost/
curl -v http://localhost/users
curl -v http://localhost:3000/users
docker container ls
docker container logs app | head -50
docker container logs web | head -30
cat docker-compose.yml
cat containers/app/Dockerfile
cat containers/app/entrypoint.sh
cat containers/web/Dockerfile
cat containers/web/nginx.conf
cat containers/web/default.conf.template
cat app/config/database.yml
※結果をSlackで送る際に ``` (バッククォーテーション 3つ)を入力して改行すると、上記のようにコード入力画面になります。こちらにコピペください
以上、お疲れさまでした!