Dockerを用いたローカルのアプリをECSにデプロイする(その3)

少し前にDocker, Docker Composeを用いたローカルのアプリをECSにデプロイしたので手順を記述します。手順は3記事に分けて説明をする次第で、この記事は3つ目です。手順13から手順18までを記述しています。

全体の手順

  • VPC作成
  • パブリックサブネット・プライベートサブネット作成
  • インターネットゲートウェイ作成・追加設定
  • ルートテーブル作成・追加設定
  • クラスター作成
  • RDS用のサブネットグループ作成
  • RDSインスタンス作成
  • セキュリティグループ作成
  • ECRのレポジトリ作成・イメージのプッシュ
  • Route53の設定
  • ALB作成
  • アプリのドメインをALBのドメインと紐付け
  • entrypoint.shの作成
  • Dockerfileの修正
  • database.ymlの修正
  • タスク定義
  • サービス作成・タスク実行

デプロイ後の構成

f:id:rinda_1994:20210613220304p:plain

前提

  • Dockerfile, docker-compose.ymlを用いてローカルでアプリ用のコンテナを起動している
  • アプリにはRuby, Railsを使用している
  • dotenv-railsをインストールして.envファイルを作成している

13. Dockerfileの修正

手順12でentrypoint.shを作成しましたが、何もしないとentrypoint.shは実行されません。 コンテナ起動時に一緒にentrypoint.shを実行するために、ローカルに置いているDockerfileに以下を追記をします。 Dockerfileとentrypoint.shはどちらもアプリのホームディレクトリに置いている前提とします。

  • COPY entrypoint.sh /bin/

  • RUN chmod +x /bin/entrypoint.sh

  • CMD ["/bin/entrypoint.sh"]

それぞれの行の役割は以下です。

  • COPY entrypoint.sh /bin/

    • 手順12で作成したentrypoint.shを/binにコピーしています。
    • これは2つ下の行のCMD命令でentrypoint.shを実行できるようにするためにentrypoint.shを適切な場所に置いています。/binに置くとentrypoint.shは実行できましたが、ホームディレクトリに置いてある方は実行できませんでした。
    • ホームディレクトリのentrypoint.shを実行した場合のエラーは「starting container process caused: exec: "entrypoint.sh": executable file not found in $PATH」で、executableという言葉からも連想されますがこれはentrypoint.shに実行権限がないために出るエラーのようです。ただ1つ下の行で実行権限を与えた場合でこのエラーが出ていたので、このことについては結局よく分かれていません。
  • RUN chmod +x /bin/entrypoint.sh

    • コンテナ側のentrypoint.shに実行権限を与えています。これをしないとコンテナがentrypoint.shを実行できません。
  • CMD ["/bin/entrypoint.sh"]

    • イメージからコンテナを起動する時にentrypoint.shを実行しています。

手順12と手順13に関連したコンテナ起動あたりの流れは以下になります。

Dockerfileをもとにイメージが作成される→イメージをもとにコンテナが起動する→起動時にCMD命令が実行される→entrypoint.shに記述しているコマンドが実行される→コンテナの中でWebサーバ用のアプリケーションが起動する

以下筆者のDockerfileです。

FROM ruby:2.7.1

RUN apt-get update -qq && \
    apt-get install -y build-essential \
                       libpq-dev \
                       nodejs

RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && apt-get install -y yarn

RUN curl -sL https://deb.nodesource.com/setup_7.x | bash - && \
apt-get install nodejs

RUN apt-get update && apt-get install -y unzip && \
    CHROME_DRIVER_VERSION=`curl -sS chromedriver.storage.googleapis.com/LATEST_RELEASE` && \
    wget -N http://chromedriver.storage.googleapis.com/$CHROME_DRIVER_VERSION/chromedriver_linux64.zip -P ~/ && \
    unzip ~/chromedriver_linux64.zip -d ~/ && \
    rm ~/chromedriver_linux64.zip && \
    chown root:root ~/chromedriver && \
    chmod 755 ~/chromedriver && \
    mv ~/chromedriver /usr/bin/chromedriver && \
    sh -c 'wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -' && \
    sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list' && \
    apt-get update && apt-get install -y google-chrome-stable

RUN mkdir /myapp
ENV APP_ROOT /myapp
WORKDIR $APP_ROOT

ADD ./Gemfile $APP_ROOT/Gemfile
ADD ./Gemfile.lock $APP_ROOT/Gemfile.lock

RUN bundle install
ADD . $APP_ROOT

EXPOSE 3000

COPY entrypoint.sh /bin/
RUN chmod +x /bin/entrypoint.sh

CMD ["/bin/entrypoint.sh"]

14. database.ymlの修正

.envに以下を追記します。

RDS_DB_NAME = DB名
RDS_USERNAME = マスターユーザー名
RDS_PASSWORD = 手順8でRDSインスタンスを作成した際に設定したパスワード
RDS_HOSTNAME = エンドポイント
RDS_PORT = 3306
RDS_URL = mysql2://マスターユーザー名:パスワード@エンドポイント

上記のDB名・マスターユーザー名・エンドポイントの情報は、RDSのデータベース一覧から作成したRDSインスタンスを選択した後、設定の項目と接続とセキュリティの項目を押下すると表示されます(DB名とマスターユーザー名は「設定」の項目、エンドポイントは「接続とセキュリティ」の項目)。

その後database.ymlのproduction環境用のところに.envで設定した環境変数を追記します。 以下筆者のdatabase.ymlの抜粋です。

production:
  <<: *default
  database: <%= ENV['RDS_DB_NAME'] %>
  username: <%= ENV['RDS_USERNAME'] %>
  password: <%= ENV['RDS_PASSWORD'] %>
  host: <%= ENV['RDS_HOSTNAME'] %>
  port: <%= ENV['RDS_PORT'] %>
  url: <%= ENV['RDS_URL'] %>

これでコンテナ内のRailsアプリからRDSインスタンスにアクセスできるようになりました。

15. ECRのレポジトリ作成・イメージのプッシュ

ECRはAWSでDockerイメージを取得・保存するためのサービスです。ローカルにあるアプリのイメージをここに送ります。

まずECRのレポジトリを作成します。

リポジトリ名:自由な名前(必須)

f:id:rinda_1994:20210620153142p:plain

その後ローカルのイメージをECRにプッシュします。プッシュするには以下のコマンドをCLIで叩いていきます。

まずECRのレジストリにログインします。aws ecr get-login-password --region ap-northeast-1でログインのためのパスワードを取得できるので、それをログインのためのコマンドとくっつけている形です。

$ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin AWSのアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com

次にローカルでDockerが管理しているイメージ一覧を表示し、表示結果のうちREPOSITORYの項目からプッシュしたいイメージの名前を確認します。

$ docker images

次にプッシュするイメージにタグを付けます。

$ docker tag イメージの名前:latest AWSのアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/イメージの名前:latest

最後にイメージをECRにプッシュします。

$ docker push AWSのアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/イメージの名前:latest

16. タスク定義

ようやくECSが出てきました。ECSはコンテナの起動・管理・停止を行うサービスです。 ECSにはいくつか形式があり、そのうちEC2インスタンスというAWSの仮想サーバを管理せずにコンテナを作成できる形式はFargateと呼ばれます。この記事ではFargateの形式でデプロイを行う次第です。

またこの手順でその内容を定義しようとしているタスクというのは、ざっくりいうと複数のコンテナを集めたコンポーネントのようです。タスクは「フロント」「バックエンド」など何かしらの役割を名付けられて集められます。複数のタスクにそれぞれどのような設定を入れるのかについてをこの手順の題名であるタスク定義で決めます。タスク定義はイメージ的にdocker-compose.ymlと同じようなものと思っても差し支えないのかもしれません。

  • 起動タイプの互換性の選択:Fargate f:id:rinda_1994:20210620160954p:plain

  • タスク定義名:自由な名前(必須)

  • タスクロール:なし f:id:rinda_1994:20210620163037p:plain

  • タスク実行ロール:ecsTaskExecutionRole

  • タスクメモリ:1GB

  • タスクCPU:0.25vCPU

最初タスクメモリを500MBにしていましたがなんかその上限値ではコンテナ内での処理を正常に行えずエラーになったので1GBにしています。ちなみに下記のコンテナのメモリ制限のところも同じ感じの理由で128MBから500MBにしました。

f:id:rinda_1994:20210620163122p:plain

  • コンテナ名:自由な名前(必須)

  • イメージ:AWSのアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/イメージ名:latest

  • メモリ制限:ハード制限、500Mib

  • ポートマッピング:3000

f:id:rinda_1994:20210620162822p:plain

  • 環境変数
    • RAILS_ENV:production
      • Railsの環境が本番環境であることをコンテナに明示しています。
    • RAILS_LOG_TO_STDOUT:true
      • Railsアプリのログが標準出力に出されるようにしています。Railsアプリではproduction.rbにデフォルトで以下のコードが記述されており、このコードは「RAILS_LOG_TO_STDOUTという環境変数が存在していた場合はログを標準出力に出す」というものなので、RAILS_LOG_TO_STDOUTという環境変数にtrueという値を入れてあげることでログが標準出力に出ます。
  if ENV['RAILS_LOG_TO_STDOUT'].present?
    logger           = ActiveSupport::Logger.new(STDOUT)
    logger.formatter = config.log_formatter
    config.logger    = ActiveSupport::TaggedLogging.new(logger)
  end

f:id:rinda_1994:20210620162833p:plain

  • ログ設定
    • Auto-configure CloudWatch Logs:チェック
    • ログドライバー:awslogs

このログ設定はどちらもデフォルトで上記の通りになっています。1つ上のスクショの設定ではログが標準出力に出るように設定しましたが、このログドライバーの項目がawslogsと設定されていることで、標準出力に出てきたログがCloudWatchに送信されます。CloudWatchはコンテナやEC2インスタンスのログの取得・確認ができるAWSのサービスなので、これによりユーザーである自分がRailsアプリのログを閲覧できるようになります。

f:id:rinda_1994:20210620171909p:plain

17. サービス作成・タスク実行

サービスはタスクを集めたコンポーネントです。この手順を終えるとなんとかデプロイOKです。

クラスター一覧画面から手順5で作成したクラスターを選択した後、下のスクショの赤丸に囲まれているボタンを押下します。 f:id:rinda_1994:20210620175248p:plain

  • 起動タイプ:Fargate

  • ファミリー:手順16で作成したタスク定義

  • リビジョン:latest f:id:rinda_1994:20210620175211p:plain

  • クラスター:手順5で作成したクラスター(デフォルト)

  • サービス名:自由な名前(必須でない)

  • タスクの数:2

f:id:rinda_1994:20210620175908p:plain

  • クラスタVPC:手順1で作成したVPCを選択

  • サブネット: 手順2で作成したタスク用サブネット2つを選択

  • セキュリティグループ:手順7で作成したセキュリティグループを選択

  • パブリックIPの自動割り当て:ENABLED

f:id:rinda_1994:20210620180230p:plain

  • ヘルスチェックの猶予期間:300秒

f:id:rinda_1994:20210620180844p:plain

f:id:rinda_1994:20210620180948p:plain

f:id:rinda_1994:20210620181140p:plain

  • ターゲットグループ:手順10で作成したターゲットグループを作成

筆者の下のスクショはプロダクションリスナーポートが443:HTTPSになっていますが無視してください。もし手順10のターゲットグループを選択できなかったり選択したけどサービス作成後アプリを表示できなかった場合は、ターゲットグループについては別途作成してこの項目で改めて選択してください。すみません。

f:id:rinda_1994:20210620181459p:plain

18. アプリの表示確認

手順9で作成したアプリのドメインをブラウザで叩いてみて、ローカルと同様にアプリの画面が表示されればデプロイOKです。

まとめていないまとめ

筆者はECSでもローカルと同様にWebサーバ用のアプリケーションにPumaを用いています。ただどうやらPumaだと多くのユーザが一度にアプリを閲覧した場合に、その処理にかかる負荷をいい感じに分散させるのがむずかしかったりするようなので・・・。今後自分が作ったアプリを使う人が増えることがあれば、一般的に本番環境に使われているNginxなどに変更したいと思います。

デプロイから少し経っているので手順の内容にやや抜け漏れがあるかもしれない点ご了承ください。