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などに変更したいと思います。

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

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

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

全体の手順

  • 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ファイルを作成している

8. RDSインスタンス作成

RDSはAWSRDBを作成するためのサービスです。この記事ではRDBMySQLを選択しています。

  • データベース作成方法:標準作成

  • エンジンのタイプ:MySQL

  • バージョン:MySQL 5.7.30

  • テンプレート:本番稼働用

f:id:rinda_1994:20210613124735p:plain f:id:rinda_1994:20210613125318p:plain

f:id:rinda_1994:20210613125334p:plain

  • VPC:手順1で作成したVPCを選択

  • サブネットグループ:手順6で作成したサブネットグループを選択

f:id:rinda_1994:20210613125541p:plain

  • VPC セキュリティグループ:既存の選択

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

f:id:rinda_1994:20210613231620p:plain

【追加設定】

  • 最初のデータベース名:自由な名前(必須)

f:id:rinda_1994:20210613233337p:plain

※手順8について、上記の項目以外はデフォルトで問題ありません

9. Route53の設定

Route53はAWSDNSサービスです。ACMなどAWSの他のサービスと連携しやすい感じになっているのでインフラ関連をAWSでまとめたい場合はドメイン管理・DNSサービスにRoute53を選択した方が安牌かもしれません。

ドメイン検索

入力スペースに自由にドメイン名を入力し、そのドメイン名が既に存在していないかを確認するためチェックボタンを押下。 (下のドメイン検索でのスクショでは適当に「rerejojfioaojfoian.com」というドメイン名にしています)

f:id:rinda_1994:20210613181017p:plain

②連絡先の詳細

氏名、電話番号、メールアドレスなど自分の個人情報を入力。

③確認と購入

ドメインを自動的に更新しますか?」の項目は「有効化」を選択し、「注文を完了ボタン」を押下してドメインを作成。

f:id:rinda_1994:20210613181138p:plain

10. ALB作成

ALBはAWSのロードバランサです。ユーザの端末から届いた通信を手順17で作成する2つのタスクに分配します。

f:id:rinda_1994:20210614210714p:plain

f:id:rinda_1994:20210614211522p:plain

  • アベイラビリティーゾーン:
    • VPC:手順1で作成したVPCを選択
    • 1つ目のサブネット

      • アベイラビリティーゾーン:ap-northeast-1a
      • サブネットの選択:手順1で作成したタスク用サブネットを選択
    • 2つ目のサブネット

      • アベイラビリティーゾーン:ap-northeast-1a
      • サブネットの選択:手順1で作成したタスク用サブネットを選択

f:id:rinda_1994:20210614212304p:plain

  • 証明書タイプ:ACMから証明書を選択する (推奨)

証明書タイプを選択した後、「ACMから新しい証明書をリクエスト」というリンクを押下

f:id:rinda_1994:20210614213451p:plain

f:id:rinda_1994:20210613211502p:plain

  • 検証方法の選択:DNSの検証

f:id:rinda_1994:20210613211515p:plain

どれくらいの時間か失念しましたが検証のための待ち時間があり、それが終わると証明書が正常に作成される形です。

証明書が作成されたことを確認した後、ALBの証明書タイプを選択した画面に戻り以下を選択します。

f:id:rinda_1994:20210614213451p:plain

  • セキュリティグループの割り当て:新しいセキュリティグループを作成する

  • セキュリティグループ名:デフォルト

  • ルール:デフォルト(ポート範囲が80と443のルールが勝手に作られていると思うのでそれをそのまま使用します)

f:id:rinda_1994:20210613211704p:plain

ターゲットグループはALBから特定のインスタンスに通信をルーティングするためのコンポーネントです。その際通信のポート番号も上書きできます。

f:id:rinda_1994:20210613211727p:plain

  • インスタンス:選択しない(ECSのタスクを設定する項目。手順16でタスクを作成したあとに追加で設定する)

f:id:rinda_1994:20210613211741p:plain

11. アプリのドメインをALBのドメインと紐付け

Route53のホストゾーン一覧のうち手順9で作成したドメイン名を選択したのち、以下画面で「レコードを作成」を押下

f:id:rinda_1994:20210614220230p:plain

  • レコードタイプ:A

「値」の「エイリアス」のボタンをONにした後プルダウンが表示されるので以下を選択

  • エンドポイントを選択:Application Load Balancer と Classic Load Balancer へのエイリアス

  • リージョンを選択:アジアパシフィック (東京) [ap-northeast-1]

  • ロードバランサーを選択:手順10で作成したALBを選択

f:id:rinda_1994:20210613213739p:plain

12. entrypoint.shの作成

手順12から手順14はAWSの設定ではなくローカルでファイルを追加作成・修正するという話になります。

この記事の上の方にdocker-compose.ymlを用いていることを前提として記述しましたが・・・。その前提を満たしている場合、ローカルではdocker-compose.ymlを用いてコンテナを起動し、そのコンテナの中では何かWebサーバ用のアプリケーションを起動している形と思います(「Webサーバ用のアプリケーション」という表現がどうなのかはちょっと分からないですが・・・この記事ではそう書いておきます)。

それを踏まえて本番環境であるECSでも同様のことをするためにはですが・・・

コンテナの起動に関しては手順18でタスクの実行を行うことでできますが、コンテナの中でWebサーバ用のアプリケーションを起動することに関しては追加で設定を行う必要があります。 その設定手段として一つあるのがentrypoint.shの作成です。

entrypoint.shって何だという話ですが・・・ まずentrypointを大文字にしたENTRYPOINTという言葉はDockerfileに記述する命令の一つです。この命令はイメージをもとにコンテナを起動する際に実行するLinuxコマンドを指定します。例えば

ENTRYPOINT ["ping","-c","3"]

とDockerfileに記述をするとコンテナ起動時にpingが3回実行されます。 つまりENTRYPOINTとは「コンテナを起動する際に実行するLinuxコマンドを指定するためのもの」と考えて問題なさそうです。

続いて.shはそのファイルがシェルスクリプトであることを示す拡張子です。 調べた感じシェルスクリプトとはざっくり「複数のLinuxコマンドをまとめて順々に実行するためのファイル」のようなので、要約するとentrypoint.shは「コンテナを起動する際にまとめて実行するLinuxコマンドを指定しているファイル」と考えてよさそうです。

この手順では、ECSのコンテナ起動と一緒にコンテナ内でWebサーバ用アプリケーションを起動するための設定をしたいので、そのために実行する必要のあるLinuxコマンドをentrypoint.shに記述する形になります。

以下がentrypoint.shの内容です。以下内容の前提として、筆者は自分のアプリをRailsで書いていて、本番環境のWebサーバ用のアプリケーションにはPumaを使っています。

#!/bin/sh

yarn install
rm -f /myapp/tmp/pids/server.pid
bin/rails s -p 3000 -b '0.0.0.0' -e production
bundle exec rails db:migrate

それぞれの行の役割は以下です。やや詳しめに書いています。

  • #!/bin/sh

    • このentrypoint.shのインタプリタとして、UNIXの標準のシェル(sh)を使うことを指定しています。このシェルによって以下のコマンドが、コンテナの基盤にあるホストOS内のカーネルが実行できる何かに変換されるみたいです。とりあえずこの行を書いていないと以下のコマンドは実行されません。
  • yarn install

    • yarnをインストールしています。yarnはJavaScriptのライブラリを管理するのに必要で、RailsではRails6から必要になったみたいです。yarnがインストールされていない時点でrails sなどのrailsコマンドを叩くと実行できない旨のエラーが出ます。
  • rm -f /myapp/tmp/pids/server.pid

    • server.pidを削除しています。このファイルがあるとPumaを起動できない時があるので削除しています。
    • server.pidって何なんだという点ですが、Pumaのプロセスに関する情報が入っているファイルです。プロセスに関する情報といってもファイルの中身はプロセスIDである1という数字が書かれているのみですが・・・。プロセスはサーバ(今回でいうとPuma)が実行する1つ1つの処理のことをそう呼ぶとのこと。
    • (残るのがこの場合のみなのかは分かりませんが)コンテナが強制終了された時にコンテナは停止しているけどこのserver.pidはなんか残っている、みたいなことがあります。そうした場合に改めてコンテナおよびPumaを起動しようとすると、コンテナは起動するもののPumaが起動しません。Rails側で「server.pidがあるということはPumaがなんか処理しているってことなので、Pumaの起動はしない」という処理がされるためです。
    • そんな感じでコンテナ起動時にserver.pidがあるとPumaを起動できないので、rails sする前はとりあえず削除のコマンドを入れとこうという意図の行です。
    • ただ筆者が開発する中でserver.pidがあっても普通にPuma起動できた時もあったような気がするので、ちょっとその辺りはちゃんと分かれていないかもなところです・・・
  • bin/rails s -p 3000 -b '0.0.0.0' -e production

    • Pumaを起動しています。
    • -e production
      • 起動する環境が本番環境であると指定しています。たしか指定しないとエラーが出ます。指定しない場合、環境についてはデフォルトで設定されている開発環境としてPumaが起動しようとするわけですが、なぜ「実際の環境:本番環境、Puma側での環境:開発環境」だとエラーが出るのかはよく分かりません。
    • -p 3000
      • ポート番号を3000番で開けています。ただ調べるとデフォルトで3000番が開いているっぽいのでこのオプションは書かなくても大丈夫そうです・・・
    • -b '0.0.0.0'
      • Pumaのプロセスをコンテナが持っている全ての仮想インタフェースに接続しています。0.0.0.0というのが「不明」という意味を持つIPらしいので、具体的にIPを指定はせずにどのIPを持ったインターフェースも接続の対象にする、みたいなことだと思います。この設定により、コンテナの外部からコンテナ内にアクセスできるようになります。多分この設定をしないとローカルの端末からこのECSのコンテナにSSH接続などをしようとしてもできなさそうです。
  • bundle exec rails db:migrate
    • DBのマイグレーションをします。これしないと本番環境のDBにローカルと同じ変更内容を加えられず、あとそもそもエラーが出てアプリを表示できません。

「実践 Rails――強力なWebアプリケーションをすばやく構築するテクニック」の第1章2節の途中までを読んでの学びと感想

知り合いのエンジニアの方から「Rubyをするならこの本を読むといいよ」とお薦めを受けたので下記の本を購入。

www.oreilly.co.jp

多分現時点での自分にとっては相当むずかしく、途中で放り出す可能性は大だが、とりあえずほんの少しずつ読み進めていきたい。気が向いたらこうして何か学んだことや感じたことを書きたい。

とりあえず本記事では「1章 基本的な手法」の「1.1 メタプログラミングとは何か」と「1.2 Rubyの基礎」の途中までを読んだ学びと感想を書いていく(どういうことか気になったがよく分からないところはそのまま「よく分からない」と記載している)。

学び

メタプログラミングについて
メタプログラミング
  • メタプログラミングとはあるプログラムを実行すると別のプログラムが生成されるというプログラミング手法のこと
    • つまり「→」がプログラムだとして、入力と出力の関係が以下のようになるプログラムをメタプロと呼ぶっぽい
      1. プログラム → プログラム
      2. データ → プログラム
    • メタプログラミングは多くのプログラミング言語で使用されているが、最もよく使用されるのは、一般にプログラムをデータとして操作する機能が強化されているRubyPythonなどの動的プログラミング言語
      • プログラムをデータとして操作するとはどういうことか?逆にプログラムをデータとしては操作しない言語があるということか?
        • よく分からない・・・
    • RubyRailsにおけるメタプログラミングの実例は以下
      • Rubyにおいてdefine_methodで自分で新たにメソッドを定義すると普通のコントローラなどでそのメソッドを使えるようになる
      • RailsでUserモデルにname属性を付与してmigrationすると、name_changed?メソッドなどnameについてのいろんなメソッドが勝手に使えるようになる
    • 例えばRailsのアプリでメタプログラミングのファイルを作った場合、それはどのフォルダに置くものなのか?
      • よく分からない・・・
イントロスペクション
  • イントロスペクションとはざっくり、対象のプログラムの役割を調べる機能のこと。一般的に構文的イントロスペクションと意味的イントロスペクションの2つがある
    • このイントロスペクションはメタプロにおいてどのような用途?
      • よく分からない・・・。これはこの節以降で説明されるものかも
    • 構文的イントロスペクションとは何か?
      • プログラムの文字列などを直接調べること
    • 意味的イントロスペクションとは何か?
      • 言語の高水準なデータ構造を通じてプログラムを調べること
        • 言語の高水準なデータ構造とはどういうことか?
          • よく分からない・・・
        • Rubyでは、メソッドの作成と書き換え、エイリアスの作成、メソッド呼び出しのインターセプト、継承チェインの操作など、クラスおよびメソッドレベルで作業を行うことを意味する。つまり普通にコントローラとかでアクションの規定をしているのとかはこの意味的イントロスペクションに該当しそう。
          • エイリアスとは何か?
            • メソッドAが存在した時に、内容は同じでメソッド名が違うメソッドBを生成すること
          • メソッド呼び出しのインターセプトとは何か?
            • メソッドが呼び出されるのを中断させること......?
          • 継承チェーンとは何か?
            • クラスが継承されている状態のこと。Rubyの全てのクラスはObjectクラスを継承しており、ObjectクラスはBasicObjectクラスを継承している
DRY
  • DRYとは「Don't Repeat Yourself」の略であり、システムにおいて特定の情報を表現する必要があるのは一度だけでよいという意味のプログラミングの原理
    • DRYを実現する方法の1つにメタプログラミングがある。どういうことかというと・・・メタプログラミングをすることによって、アプリケーション内で重複している同様の概念を意味したプログラムを抽象化すれば1つDRYに近づくということ
Rubyの基礎の仕組みについて
全てのRubyオブジェクトに含まれるフィールド
  • 全てのRubyオブジェクトはメモリの中に以下のフィールドを持っている。
    • klass
      • このオブジェクトのclassクラスへのポインタ
        • ポインタとは何か?
          • 変数やメソッドなどが置かれているメモリ上のアドレスを指し示す変数
        • ググった限りの情報だがRubyでは全てのクラスはclassクラスのオブジェクトらしく・・・かつ全てのデータはオブジェクトらしいので、全てのRubyオブジェクトのメモリの中にklassポインタが含まれているのは納得のいく話
      • Rubyではclassは予約語なので・・・・classの代わりにklassという文字列が用いられている。もしこのフィールドにclassという文字列を用いた場合はなんか不具合が起きるらしい。このような意図的なミススペルはRubyに散在している模様。
        • 予約語とは何か?
          • 開発者が変数名やメソッド名としては用いることができない文字列
          • ポインタは変数なので、自身の名前に予約語の文字列を用いることはできない。そのためクラスオブジェクトへのポインタはclassでなくklassとしているよう
    • iv_tbl
      • このオブジェクトに属するインスタンス変数のデータが入っているフィールド
    • flags
      • オブジェクトの汚染状態、ガベージコレクションマークビット、オブジェクトがフリーズされているかどうかといった、何らかのステータス情報が含まれたBooleanフラグのビットフィールド
        • ステータス情報の具体例を示した用語がよく分からない・・・
    • m_tbl(オブジェクトがクラスまたはモジュールだった場合のみ)
    • super(オブジェクトがクラスまたはモジュールだった場合のみ)
クラスの継承について
  • クラスの継承についての図は以下。これはこの「実践 Rails」の本に書いてある図を少し修正した図 f:id:rinda_1994:20210706194523p:plain
    • klassポインタの矢印が、全てのクラスはclassクラスのオブジェクトであるという関係を示している
    • superポインタの矢印が、自作したクラスがObjectクラスを継承しているという関係を示している
メソッドとメッセージとレシーバ
  • Rubyではメソッドが呼び出されるときのことを「メッセージを送信している」と言い、メッセージの送信先となるオブジェクトを「レシーバ」と言う。

    • メソッド=メッセージではなく、メソッドというものが呼び出されることをメッセージを送信すると呼称するよう。
  • 以下伊藤淳一さん著のプロを目指す人のためのRuby入門のP211を丸々参考にさせていただいた図だが・・・レシーバとメッセージの関係性はこんな感じだと思う

f:id:rinda_1994:20210706155044p:plain

感想

  • メタプロは重複してる複数のプログラムを抽象化するのにも使われるんだ。というかそれが主な用途なんかな・・・?分からん。
  • データの入力を受け取るとデータの出力を吐き出すプログラムが普通のプログラムで、それ以外のパターンのプログラムがメタプロになるんかな(プログラムを受け取ってプログラムを吐き出すとか)。
  • コードとプログラムという用語を今日まで使い分けせずにきたけど、調べると意味の差は一応あるらしい。プログラムがコンピュータ側が理解できるもの、コードが人間側でRubyとかを用いて書いたもの・・・。とはいえそんなに意識されてなさそうなとこな感じはしたので、この記事では一旦「プログラム」で統一しようと思う。
  • 翻訳系の本はそうなんかもしれんけど、とりあえず自分にとってこの本の文章は分かりづらい・・・
  • 1.1節からいきなりかなりむずいので、一旦は今持ってる上述したRuby入門書とネットの記事を都度頼りながら読んでく他なさそう。

参考文献・参考リンク

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

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

全体の手順

  • 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ファイルを作成している

1. VPC作成

VPCAWS上にあるユーザ専用のネットワークです。この記事でこれから作成する、サブネット・タスク・ALB・RDSインスタンスなどのコンポーネントを設置するための大枠となるネットワークをこの手順で作成していきます。

  • 名前タグ:自由な名前(必須でない)

  • IPv4 CIDR ブロック:自分で設定(必須。自分はサブネットマスクを16にしました)

  • IPv6 CIDR ブロック:なし

f:id:rinda_1994:20210612105130p:plain

2. タスク用サブネット・RDSインスタンス用サブネット作成

サブネットはVPCなどのネットワークを分割して作った小さなネットワークです。この小さなネットワークに、タスクやRDSインスタンスなどのインスタンスと呼ばれるものを設置していきます。

VPC ID:手順1で作成したVPCを選択

f:id:rinda_1994:20210612163926p:plain

1つ目のタスク用サブネット

  • サブネット名:自由な名前(必須でない)

  • アベイラビリティーゾーン:ap-northeast-1a

  • IPv4 CIDR ブロック:自分で設定(必須。自分はサブネットマスクを24にし、他3つのサブネットも同様にしました)

f:id:rinda_1994:20210612163943p:plain

2つ目のタスク用サブネット

  • サブネット名:自由な名前(必須でない)

  • アベイラビリティーゾーン:ap-northeast-1c

  • IPv4 CIDR ブロック:自分で設定(必須)

f:id:rinda_1994:20210612163957p:plain

1つ目のRDSインスタンス用サブネット

  • サブネット名:自由な名前(必須でない)

  • アベイラビリティーゾーン:ap-northeast-1a

  • IPv4 CIDR ブロック:自分で設定(必須)

f:id:rinda_1994:20210612164014p:plain

2つ目のRDSインスタンス用サブネット

  • サブネット名:自由な名前(必須でない)

  • アベイラビリティーゾーン:ap-northeast-1c

  • IPv4 CIDR ブロック:自分で設定(必須)

f:id:rinda_1994:20210612164028p:plain

上記4つのサブネットのIPv4 CIDR ブロックは、VPCで設定したIPv4 CIDR ブロックに包含されている必要があります。 例えばVPCIPv4 CIDR ブロックを50.0.0.0/16としていた場合、サブネットのIPv4 CIDR ブロックは50.0.1.0/24などとするのが例えば適当です。

図にするほどのことでもないかもですが、こんな感じです。

f:id:rinda_1994:20210612164807p:plain

3. インターネットゲートウェイ作成・追加設定

インターネットゲートウェイはNAT機能を持つコンポーネントです。インターネットから来た通信はインターネットゲートウェイを通じて宛先IPがALBのグローバルIPからALBのプライベートIPに変換されます。

名前:自由(必須でない)

f:id:rinda_1994:20210612214233p:plain

【作成後の追加設定】

インターネットゲートウェイ一覧から作成したインターネットゲートウェイ選択→「アクション」から「VPCにアタッチ」を選択→手順1で作成したVPCを選択→「インターネットゲートウェイのアタッチ」を押下

4. ルートテーブル作成・追加設定

ルートテーブルはルーティングの機能を持つコンポーネントです。インターネットゲートウェイは通信の宛先IPをグローバルIPからプライベートIPに変換してくれましたが、そこからALBに通信を送る機能はおそらく持っていません。そこでルートテーブルの設定をすることで、VPCに届いた、宛先IPがALBのプライベートIPである通信はALBへと送られます。反対にVPCからインターネットに通信が出ていく際のルーティングの設定も行います。

  • 名前:自由(必須でない)

  • VPC:手順1で作成したVPCを選択

f:id:rinda_1994:20210612213431p:plain

【作成後の追加設定】

  • ルートテーブルとサブネットの関連付け

ルートテーブル一覧から作成したルートテーブルID押下→「サブネットの関連付け」を押下→「サブネットの関連付けを編集」押下→手順2で作成した4つのサブネットを選択→「save associations」を押下

f:id:rinda_1994:20210612213506p:plain

  • インターネットから通信を受け取るためのルーティング追加

ルートテーブル一覧から作成したルートテーブルID押下 →「ルート」を押下 →「ルートを編集」を押下 →「Add route」を押下 →「Destination」の検索窓で「0.0.0.0/0」を選択 →「Target」の検索窓で「Internet Gateway」を選択すると手順3で作成したインターネットゲートウェイが候補に表示されるので、さらに選択 →「変更を保存」を押下

f:id:rinda_1994:20210612215256p:plain

5. クラスター作成

クラスターは(厳密ではないと思いますが)EC2インスタンスの集まりです。クラスターの上でタスクが展開される形になります。

  • クラスターテンプレートの選択:ネットワーキングのみ

f:id:rinda_1994:20210613092607p:plain

f:id:rinda_1994:20210613092833p:plain

6. RDS用のサブネットグループ作成

サブネットグループはサブネットをグループとして集めたものです。ここでは手順1で作成したRDSインスタンス用サブネットを2つ集めてサブネットグループにしています。RDSインスタンスを作成する際に必要です。

  • 名前:自由な名前(必須)

  • VPC:手順1で作成したVPCを選択

f:id:rinda_1994:20210613093847p:plain

f:id:rinda_1994:20210613093900p:plain

7. セキュリティグループ作成

セキュリティグループはそのインスタンスに入ってくる通信・出ていく通信の制御を行うためのコンポーネントです。例えばプロトコルがAの通信は入ってくるの許可するけど、プロトコルがBの通信は許可しません、みたいなそういう感じです。

【ECSクラスター用セキュリティグループの作成】

  • セキュリティグループ名:自由な名前(必須)

  • 説明:適当に記載(必須)

  • VPC:手順1で作成したVPCを選択

f:id:rinda_1994:20210613222355p:plain

  • インバウンドルール

    • 1つ目のルール

      • タイプ:HTTP
      • ソース:0.0.0.0/0
    • 2つ目のルール

      • タイプ:カスタムTCP
      • ポート範囲:3000
      • ソース:0.0.0.0/0
  • アウトバウンドルール

f:id:rinda_1994:20210613214738p:plain

f:id:rinda_1994:20210613135924p:plain

【RDSインスタンス用セキュリティグループの作成】

  • セキュリティグループ名:自由な名前(必須)

  • 説明:適当に記載(必須)

  • VPC:手順1で作成したVPCを選択

f:id:rinda_1994:20210613222355p:plain

  • インバウンドルール:

    • 1つ目のルール
      • タイプ:MySQL/Aurora
      • ポート範囲:3306
      • ソース:0.0.0.0/0
  • アウトバウンドルール

f:id:rinda_1994:20210613222830p:plain

RailsでStripeAPIを利用して送金システムを実装する

掲題のことをしたので実装までの過程を記録に残そうと思います。 Stripeを用いてRailsで送金、という趣旨の記事はあまり見つからなかったので、後々どなたかの役に立てれば幸いです。

尚、本記事は「テストデータでの送金」までしか扱っておらず、本番環境での送金はもう少々別途で実装する必要があります。ご了承ください。

ユーザ間での送金完了までにする必要のあること

送金完了までで、大まかに以下の点を行う必要があります。

  • カード情報ページの表示
  • 送信元ユーザのカード情報の登録
  • 送信先ユーザのカード情報を含めた個人情報の登録
  • 送金処理

それぞれの工程を、必要なコードと一緒に以下から詳細に説明していきます。

下準備

ここはそんなに本題ではないのでさらっと書きます、、、

多いですが、下準備としてしていただく必要のあることは以下の事項です。

  • ここからStripeのユーザ登録

  • カード情報登録用のコントローラとモデルを作成

    • 以下の例ではコントローラ名はcards_controller、モデル名はCard
    • コントローラのアクションで必要なのはnewとcreate
    • モデルの要素にはuser_idとcustomer_idを入れておく
  • 送金用のコントローラを作成
    • 以下の例ではコントローラ名はgifts_controller
    • コントローラのアクションで必要なのはnewとcreate
  • ビューの作成
    • cardsとgiftsともにnew.html.erbが必要
  • "stripe"と"gon"というgemのインストール
    • それぞれのgemの詳細は下の方で説明

カード情報入力ページを表示するまで

カード情報入力ページを表示するまでにどのようなコードを準備する必要があるのか説明します。

①application.html.erbで外部のStripe.jsを読み込む
<!-- application.html.erb -->

<head>
 <script src="https://js.stripe.com/v3/"></script>
</head>

application.html.erbにこの1文を入れることにより、外部にあるStripeサーバと通信できるようになります。 このサーバはStripe.jsを提供してくれるStripeサーバとなります(別種のStripeサーバについては後述します)。

ちなみにカード情報や送金に関するページだけでなく、Application.html.erbを使って全てのページで読み込んでいるのは、セキュリティ保護の面で公式からそう推奨されているからです。全ページでStripe.jsを読み込むことで、Stripe側が、ユーザがWebサイトを閲覧する際に不正行為の可能性がある不審な動作を検出してくれるとのこと。なるほど。

ただこれに関しては一旦「決済関連のページ以外からも決済に関する攻撃が可能」ということだと解釈をしましたが、それが例えばどのような形で行われるのかなどはまだイメージがついていないです、、、

②ローカルのStripe.jsを作成
// Stripe.js

// 画面の読み込み終了時に以下のコードを動かす
$(window).on("load", function(){

    // gonにより取得したテスト用公開鍵を変数に入れる
    const key = gon.stripe_public_key;

    // 公開鍵を用いてStripeオブジェクトを生成
    // 外部側のStripe.jsから取得できる全ての要素はこのオブジェクトの中に入っている
    const stripe = Stripe(key);

    // Stripeオブジェクトからelementsオブジェクトを生成
    // elementsオブジェクトは、カード情報を入力するフォームを生成するためのUIコンポーネント
    const elements = stripe.elements();

    // カード情報入力フォームのUIを整えるための変数
    const style = {
      base: {
        fontSize: '17px'
      }
    };

    // 上からカード番号、有効期限、cvcの入力スペースを作成している
    // mountでDOM上にそれぞれの入力スペース用要素をアタッチしている
    const cardNumber = elements.create('cardNumber', {style: style});
    cardNumber.mount('#card-number');
    const cardExpiry = elements.create('cardExpiry',{style: style});
    cardExpiry.mount('#card-expiry');
    const cardCvc = elements.create('cardCvc', {style: style, placeholder: ''});
    cardCvc.mount('#card-cvc');

});

ひとつひとつのコードの意味は大体コメントアウトをしているのでそこを参照ください。 ざっくり、このコードの意義はカード情報入力フォーム用のUIコンポーネントを用意することです。 ビュー側でidがcard-numberなどの要素を書いてあげることで、ページにStripeサーバから与えられた入力フォームが表示されます。 テスト用の公開鍵はgonというgemを利用して取得しています。gonの詳細は次で説明します。

③card_controller.rbのnewアクションの中身を書く
#card_controller.rb

class CardsController < ApplicationController

  def new
    # あらかじめ環境変数に入れておいたテスト用公開鍵を、gonの変数にセット
    gon.stripe_public_key = ENV['STRIPE_PUBLISHABLE_KEY']
    card = Card.where(user_id: current_user.id)
    # カードのデータが既にDBに存在していた場合は指定したページにリダイレクト
    redirect_to new_post_gift_url(id: params[:id]) if card.exists?
  end

  def create

  end

end

newアクションの中身で特筆すべき箇所は gon.stripe_public_key = ENV['STRIPE_PUBLISHABLE_KEY'] です。

まず左辺から説明すると・・・

このgonってなんなん?という方に向けて、gonとはRailsJavaScriptを連携させるためのgemです。 これを使うとコントローラ内でセットした変数をJavaScriptで使用することができます。

続いて右辺について・・・

ENV['STRIPE_PUBLISHABLE_KEY'] では環境変数を取り出しています。あらかじめdotenv-railsなどで STRIPE_PUBLISHABLE_KEY という環境変数にテスト用公開鍵をセットしておく必要があります。変数名はもちろんこの名前でなくても構いません。 StripeAPI用の公開鍵と秘密鍵は下準備で登録してもらったStripeサイトのダッシュボードにある「APIキーの取得」の項目で確認できます。

つまりこの行は「環境変数にセットしたテスト用公開鍵をgonの変数にセットしている」コードになります。 これによりStripe.jsでgonの変数を使用することができ、結果として公開鍵を直書きするのを避けることができます。

④cards/new.html.erbの中身を書く
<!-- cards/new.html.erb -->

<% provide(:button_text, '登録する') %>

<!-- ローカルのstripe.jsの読み込み -->
<%= javascript_include_tag 'stripe.js' %>

<div class="container">
  <div class="row">
    <div class="col-md-4 offset-md-4">
    <h2 class="card_header">カード情報の登録</h2>
      <div class="card">
        <!-- cards_controllerのcreateアクションにformを送信 -->
        <%= form_with url: post_cards_path(id: params[:id]), method: :post, id: "payment-form", local: true do |f| %>
          <div class="card-element card-number">カード番号</div>
          <!-- カード番号入力スペース用の要素が中にアタッチされるdiv -->
          <div class="stripe-input" id="card-number"></div>
          <div class="card-element">有効期限</div>
          <!-- 有効期限入力スペース用の要素が中にアタッチされるdiv -->
          <div class="stripe-input" id="card-expiry"></div>
          <div class="card-element">CVC</div>
          <!-- cvc入力スペース用の要素が中にアタッチされるdiv -->
          <div class="stripe-input" id="card-cvc"></div>
          <div class="form-row">
            <div id="card-errors" role="alert"></div>
          </div>
          <%= f.submit yield(:button_text), class: 'btn btn-primary', id: 'info_submit' %>
        <% end %>
      </div>
    </div>
  </div>
</div>

②で作成したローカルのStripe.jsをまず読み込みます。 それにより、既にローカルのStripe.jsにおいて「card-numberというIDのdivの中にカード番号入力スペース用の要素をアタッチします」といったコードを、カード番号、有効期限、cvcについてそれぞれ書いているので、その3つの入力フォームを表示することができます。

ここまでのコードが準備できたらカード情報入力ページを表示することができます。データの流れは以下です。

f:id:rinda_1994:20210308071301p:plain

カード情報入力ページのリンクを踏むと、外部のStripe.jsにより与えられた入力フォームが表示されるという流れです。

ちなみにカード情報入力画面自体は上記の諸々のコードを書くことで表示できるのですが、素の状態だとかなり味気ないのでフォームを以下のCSSで整えます。

.card {
  border: none;
  margin-top: 20px;

  .stripe-input {
    padding:8px;
    margin-bottom:5px;
    border: 1px solid #BBBBBB;
    border-radius:5px;
  }

  #card-number {
    width: 220px;
  }

  #card-expiry {
    width: 90px;
  }

  #card-cvc {
    width: 70px;
    margin-bottom: 20px;
  }
}

手元に表示されるであろうカード情報入力画面は以下です。

f:id:rinda_1994:20210308211509p:plain

送信元ユーザのStripeアカウント情報をDBに登録するまで

送信元ユーザのStripeアカウント情報をDBに登録するまでにどのようなコードを準備する必要があるのか説明します。

①ローカルのstripe.jsに追記
// stripe.js

$(window).on("load", function(){

    const key = gon.stripe_public_key;

    const stripe = Stripe(key);

    const elements = stripe.elements();

    const style = {
      base: {
        fontSize: '17px'
      }
    };

    const cardNumber = elements.create('cardNumber', {style: style});
    cardNumber.mount('#card-number');
    const cardExpiry = elements.create('cardExpiry',{style: style});
    cardExpiry.mount('#card-expiry');
    const cardCvc = elements.create('cardCvc', {style: style, placeholder: ''});
    cardCvc.mount('#card-cvc');

    // 項目入力時のエラー処理
    cardNumber.addEventListener('change', function(event) {
      const displayError = document.getElementById('card-errors');
      if (event.error) {
        displayError.textContent = event.error.message;
      } else {
        displayError.textContent = '';
      }
    });

    const form = document.getElementById('payment-form');
    form.addEventListener('submit', function(event) {
      event.preventDefault();

      // トークンを作成
      stripe.createToken(cardNumber).then(function(result) {
        if (result.error) {
          // カード情報を登録しようとした結果、内容が不適切であればエラーメッセージを表示
          var errorElement = document.getElementById('card-errors');
          errorElement.textContent = result.error.message;
        } else {
          // カード情報が適切な内容であれば、トークンをフォームに入れてWebサーバに送信
          alert('カード情報を登録しました');
          stripeTokenHandler(result.token);
        }
      });
    });

    function stripeTokenHandler(token) {
      // トークンをフォームに内包する
      const form = document.getElementById('payment-form');
      const hiddenInput = document.createElement('input');
      hiddenInput.setAttribute('type', 'hidden');
      hiddenInput.setAttribute('name', 'stripeToken');
      hiddenInput.setAttribute('value', token.id);
      form.appendChild(hiddenInput);

      //フォームをWebサーバに送信
      form.submit();
    }
});

追記箇所により、入力されたカード情報からトークンを生成し、自身のWebサーバに送信できるようになります。 このようにカード情報をトークンという形にすることで、自身(もしくは自社)のサーバにユーザの具体的なカード情報が渡らないようにします。

これはStripeをはじめ、Payjpなどどの決済サービスを使う際もマストとされている要件なようです。 クレジットカードの不正利用があまりに多いことから、サービスのサーバからカード情報が漏洩することを防ぐ意図で取られている措置みたいです。

②card_controller.rbのcreateアクションの中身を書く
#card_controller.rb

class CardsController < ApplicationController
  # stripeAPI用のメソッドなどを使用できるようにする
  require "stripe"

  def new
    gon.stripe_public_key = ENV['STRIPE_PUBLISHABLE_KEY']
    card = Card.where(user_id: current_user.id)
    redirect_to new_post_gift_url(id: params[:id]) if card.exists?
  end

  def create
    # あらかじめ環境変数に入れておいたテスト用秘密鍵をセット
    Stripe.api_key = ENV['STRIPE_SECRET_KEY']
    # トークンが生成されていなかった場合は何もせずリダイレクト
    if params['stripeToken'].blank?
      redirect_to posts_url
    else
      # 送金元ユーザのStripeアカウントを生成
      sender = Stripe::Customer.create({
        # nameとemailは必須ではないが分かりやすくするために載せている
        name: current_user.name,
        email: current_user.email,
        source: params['stripeToken'],
      })
      # Cardテーブルに送金元ユーザのこのアプリでのIDと、StripeアカウントでのIDを保存
      @card = Card.new(
        user_id: current_user.id,
        customer_id: sender.id,
      )
      if @card.save
        redirect_to new_post_gift_url(id: params[:id])
      else
        redirect_to posts_url
      end
    end
  end

end

追記箇所により、送金元ユーザのStripeアカウント作成および、Cardテーブルに送金元ユーザのアプリ内でのIDとStripeアカウントでのIDを保存しています。

①にてformにトークンを乗せているので、トークンはparamsで取得可能です。 そのトークンをもとにStripe::Customer.createで送金元ユーザのStripeアカウントを作成します。 そしてそのStripeアカウントのIDとアプリ内でのユーザIDを一緒にCardテーブルに保存しています。

送金元ユーザのStripeアカウントが正常に登録しているかどうかは、Stripeサイトのダッシュボードの「顧客」の項目を見ると確認できます。

StripeアカウントIDとアプリ内でのユーザIDをDB(Cardテーブル)に保存するまでのデータの流れはこんな感じです。

f:id:rinda_1994:20210308234618p:plain

送金が完了するまで

送金が完了するまでにどのようなコードを準備する必要があるのか説明します。

①gift_controller.rbの中身を書く
#gift_controller.rb

class GiftsController < ApplicationController
  require 'stripe'

  def new
  end

  def create
    Stripe.api_key = ENV['STRIPE_SECRET_KEY']
    amount = 100
    card_sender = Card.find_by(user_id: current_user.id)

    begin

      # 送金先ユーザのStripeアカウント作成
      reciever = Stripe::Account.create({
        type: 'custom',
        country: 'JP',
        business_type: 'individual',
        capabilities: {
          card_payments: {
            requested: true,
          },
          transfers: {
            requested: true,
          },
        },
      })

      # 証明写真の面裏をStripeサーバにアップロード
      front = Stripe::File.create({
        purpose: 'identity_document',
        file: File.new("app/assets/images/inu.png"),
      })

      back = Stripe::File.create({
        purpose: 'identity_document',
        file: File.new("app/assets/images/neko.png"),
      })

      # 送金先ユーザがStripe側から承認を受けるのに必要な情報を追加登録
      stripe_account = Stripe::Account.update(
        reciever.id,
        individual: {
          address_kanji: {
            :postal_code => "150-0013",
            :state => "東京",
            :city => "渋谷区",
            :town => "恵比寿",
            :line1 => "1-1-1",
            :line2 => "テストビルディング101号",
          },
          address_kana: {
            :postal_code => "150-0013",
            :state => "とうきょうと",
            :city => "しぶやく",
            :town => "えびす",
            :line1 => "1-1-1",
            :line2 => "てすとびるでぃんぐ101ごう",
          },
          :first_name_kanji => "太郎",
          :last_name_kanji => "田中",
          :first_name_kana => "たろう",
          :last_name_kana => "たなか",
          dob: {
            :day => "1",
            :month => "1",
            :year => "2000",
          },
          verification: {
            document: {
              :front => front.id,
              :back => back.id,
            }
          },
          :gender => "male",
          :phone => "+819012345678",
        },
        tos_acceptance: {
          :date => Time.now.to_i,
          :ip => "192.168.0.1",
        },
        external_account: {
          :object => 'bank_account',
          :country => 'jp',
          :currency => 'jpy',
          :routing_number => '1100000', #銀行コード+支店コード
          :account_number => '0001234',
          :account_holder_name => 'トクテスト(カ',
        }
      )

      # 送金処理
      @gift = Stripe::Charge.create({
        amount: amount,
        currency: "jpy",
        customer: @card_sender.customer_id,
        transfer_data: {
          destination: reciever.id,
        }
      })

    # stripe関連でエラーが起こった場合
    rescue Stripe::CardError => e
      flash[:error] = "#決済(stripe)でエラーが発生しました。{e.message}"
      render :new

    rescue Stripe::InvalidRequestError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(InvalidRequestError)#{e.message}"
      render :new

    rescue Stripe::AuthenticationError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(AuthenticationError)#{e.message}"
      render :new

    rescue Stripe::APIConnectionError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(APIConnectionError)#{e.message}"
      render :new

    rescue Stripe::StripeError => e
      flash.now[:error] = "決済(stripe)でエラーが発生しました(StripeError)#{e.message}"
      render :new

    # stripe関連以外でエラーが起こった場合
    rescue => e
      flash.now[:error] = "エラーが発生しました#{e.message}"
      render :new
    end

    # 送金が完了したら指定のパスにリダイレクト
    redirect_to posts_path

  end

end

送金用のコントローラはかなりボリューミーです。

していることとしては、まず Stripe::Account.create で送金先ユーザのStripeアカウントを登録します。 次に送金先ユーザのStripeアカウントがStripe運営から承認を受けるにあたり、Stripe::Account.updateで必要な情報を漏れなく追加登録します。 そしてStripe::Charge.createで、送金元ユーザのStripeアカウントIDと送金先ユーザのStripeアカウントIDをもとに送金処理を行います。

送金が完了するまでのデータの流れはこんな感じです。

f:id:rinda_1994:20210309010527p:plain

送金が正常に完了しているか、および送金先ユーザのStripeアカウントが正常に作成できたかは、それぞれStripeサイトのダッシュボードの「支払い」と「Connectアカウント」の項目を見ると確認できます。

承認にあたって必要な項目は上記の通り、指名、住所、電話番号、口座番号などかなり多いです。これはテストデータの送金なので適当にコード側で全部記入しても項目さえ埋めていれば承認が通りますが、本番環境だと人による審査が当然入ると思うので、どこかでユーザに上記の個人情報すべてを入力してもらう動線を用意しておかなければなりません。

また送金先ユーザのStripeアカウントにはタイプがあり、今回利用しているのはCustomというタイプになります。タイプは他にStandardとExpressがあり、それらはOAuthという、複数のWebアプリを連携させる仕組みを用いてアカウントの認証を進める必要があります。興味がある方はこのページとかから調べてみてください。

それと筆者のアプリは100円固定で送金としていますが、ユーザ側で金額を決めてもらうには別途フォームを用意する必要がありますね。

まとめてないまとめ

長くなりました。不正確な情報があればご指摘いただけると幸いです。

参考

HTTP/2というプロトコルを用いるとネットの通信が速くなる場合があるようで

HTTP/1.0

HTTPはブラウザ(Webクライアント)とWebサーバ間でデータをどのようにやり取りするかを取り決めたプロトコル

HTTPにもバージョンがあり、1つのリクエストに対して1つのレスポンスを返す、という形式の通信を取るのがHTTP/1.0(までのバージョン)。筆者がHTTPと聞いてイメージする通信はこれだった。

図に表すとこんな感じ。

 

f:id:rinda_1994:20210226104321p:plain

上図は、まずはHTMLを、次に画像を、最後にCSSを取得するためのリクエスト/レスポンスが順次生成されている流れとなる。

 

HTTP/1.1

HTTP/1.0ができた3年後、1999年にHTTP/1.1が生まれる。

もう22年も経つわけだが、現在も尚多くのサーバ-ブラウザ間で使用されているらしい。

 

HTTP/1.1ではHTTPパイプラインという機能が実現されており、ブラウザから同時に複数のリクエストを出すことができる。

図に表すとこんな感じ。

 

f:id:rinda_1994:20210226105338p:plain

 

しかしHTTPパイプラインには、「Webサーバはレスポンスをリクエストの順番通りに返さないといけない」というネックがある。上図で言うと、例えばindex.htmlが非常に重たいリソースだと、A.jpgとB.cssのレスポンスもその分遅れてしまう。

この欠点のため、HTTP/1.1を使用しているほとんどのブラウザでは、結局HTTP/1.0と同様にWebサーバに対してリクエストを1つずつ送る通信形態を取っている。

 

ちなみに上図ではTCPコネクションは1つのていで作成しているが、記事によってはリクエスト/レスポンスごとにコネクションを確立すると説明しているものもあった。この辺はちょっとよく分からないが・・・。

リソースをそれぞれ取得するごとにスリーハンドシェイクするのは悠長だし、おそらくは1ページ表示する上で必要なTCPコネクションは1度で良いのだとは思う。

 

HTTP/2とは

HTTP/2は複数のリクエスト/レスポンスを並行して処理することができるプロトコル

1つのコネクションの中でストリームという仮想的なコネクションを複数用意することができ、このストリームの中でリクエスト/レスポンスのやりとりを行う。HTMLのリクエスト/レスポンスはHTMLのストリームで処理され、CSSやJSも然り、という形。

 

そして各ストリームはそれぞれ独立した存在なので、結果複数のリクエスト/レスポンスを並列して処理することができる。

図にするとこんな感じ。各ストリームを同時に処理している。

 

f:id:rinda_1994:20210226112359p:plain

 

またストリームの優先制御が可能なので、HTTPパイプラインと違いどのレスポンスを優先して返すかを規定することができる。この規定はブラウザ側で行う。

 

例えばリソースの中でも画像などはWebサーバからのダウンロードが完了しきっていなくてもページで表示(レンダリング)され始めるようだが、優先制御を使えば、画像用ストリームの優先度を下げてHTMLなどの必須なリソースをなるべく速く取得するということも可能らしい。

 

HTML/2.0には他にも、リクエストされていないCSSなどのリソースをサーバーがクライアントに勝手に送信してくれる「サーバープッシュ」機能もある。これによりリクエストの数を減らせるので、ページが表示されるまでの合計通信時間を短縮できる場合があるようだ。

 

なんか便利なんだろうなとは思いつつ、このサーバープッシュで分からなかったことが1つあった。「Webサーバはどういう基準でリクエストを勝手にブラウザに返すのか」ということ。

おそらくはサーバ側で各リソースの依存関係を決めておいて、依存関係にあるリソースA・BのうちAにだけリクエストが来て、規定した時間Bにリクエストが来なかった場合はBのレスポンスも返す、という仕組みではないかなとは想像したけど、まあちょっとよく分からない。

 

そもそも並列処理できる上に、優先制御やサーバープッシュの機能もあるため、HTTP/2.0ではそれ以前のHTTPに比べて通信の速度が上がる場合も多いらしい。

 

だがそれが見込めないケースもあるよう。

例えば表示すべきリソースが少ない場合などでは、そもそもサーバへのリクエストの数が少ないので並行処理できてあまり意味がなくなる。 

 

まとめていないまとめ

インターネットの通信を速くするのっていろんなやり方があるんだなと感じた。

HTTP/2については、言い方変かもだけど、大通信(コネクション)の中に小通信(ストリーム)が複数ある、みたいな認識を持った。

 

参考

普及が進む「HTTP/2」の仕組みとメリットとは | さくらのナレッジ

HTTP/2の特徴 HTTP/1.1との違いについて | REDBOX Labo

【図解】HTTP/2って?HTTP/1.1との違いと導入メリット・課題まとめ | カゴヤのサーバー研究室

Docker環境でbyebugを使用するための方法

Railsデバッグをしようと思い少しできるまでに時間がかかったので、備忘録として記録。

 

該当のコントローラにbyebugを記入し、"byebug ファイル名"でデバッグ用画面を開くところまではいけた。

しかしnやsで実行をしようとすると uninitialized constant ApplicationController というエラーが出た。

 

「ApplicationControllerという初期化されていない定数があります」ってどういうこと・・・そもそもApplicationControllerって定数だったっけ・・・などと思いつつそのエラーメッセージで調べるも、有力な情報は出てこず。

 

このエラーメッセージから離れてbyebugについて調べていると、どうやらDocker環境でbyebugを使用するには追加設定が必要であることがわかった。具体的には、docker-compose.ymlに以下の設定を追記する。

 

# docker-compose.yml
services:
  web:
    stdin_open: true  #追加箇所
    tty: true #追加箇所

 

この後"docker-compose up --build -d"でDockerを再ビルドしたところ、Docker環境でも正常にbyebugを使えるようになった。

stdin_openが標準入力に関するステータス、ttyが標準出力を自端末にアタッチするかのステータスで、これらをtrueにすることでコンテナ内の標準出力とDockerを起動しているターミナルの標準出力が接続されるらしい。

要はDockerの出力結果がターミナルにも表示されるようになるということだが・・・標準入力・出力については正直よくわかっていない。なかなか手強そうな概念だった。以下の記事が良さそうだったので、今度時間のある時に読み込んでみよう(せんやつ)。

 

qiita.com

 

 

で、本題からは逸れるが、なぜかデバッグ用画面でこちらが入力している文字が表示されない。

正直打ち込む内容は"n"とか"s"とか"p 変数名"くらいの文字なので別に画面に表示されなくてもなんとかなるのだが、やはり不便だと感じはじめ、pry-byebugでは同じ事象が発生しないのであればpry-byebugに乗り換えようと思う。

 

参考

Docker環境でByebugを使う方法 - logicoffee プログラミング勉強日記

Docker環境でByebugを使う方法 | ゆみしん夫婦のブログ