あすたぴのブログ

astap(あすたぴ)のブログ

Railsで暗号化するときのメモ的なアレ

背景

ログインに使用するパスワードは不可逆な暗号化を行って、DBに保存にするのは一般的。 それとは別でDBに保存しておきたいんだけど普通に生データとして持つとセキュリティ事故でデータが漏れた場合にそのまま見えてしまう困るようなデータを可逆な暗号化で保存しておきたい。データが漏れないようにセキュリティを強化すると共に、万一データが漏れた場合に容易に流出しないように2重の対策をすることが目的。

ActiveSupport::MessageEncryptor

ActiveSupport::MessageEncryptorを使う。 http://api.rubyonrails.org/classes/ActiveSupport/MessageEncryptor.html

RubyのOpenSSL::Cipherのラッパーだと思う。 https://docs.ruby-lang.org/ja/latest/class/OpenSSL=3a=3aCipher.html

デフォルトの暗号化方式として aes-256-cbc が使用される。 暗号化方式については詳しくないのでそこの説明はできない。後日ちゃんと学びたい。

サンプルにあるように、以下のように ActiveSupport::KeyGenerator を使用して暗号化に使用する鍵を生成する

salt  = SecureRandom.random_bytes(64)
key   = ActiveSupport::KeyGenerator.new('password').generate_key(salt, 32) # => "\x89\xE0\x156\xAC..."

ユーザーのデータを暗号化する場合は、saltはユーザーごとに生成して保存しておく。saltは漏れても問題がないデータになる。 パスワードは一意の情報を用意するがこれは漏れてはいけない。環境変数などでアプリ実行時に渡す形にする必要がある。

その後は、その鍵から ActiveSupport::MessageEncryptor のオブジェクトを生成して暗号化、復号化を行う。

この処理はそれなりに重い処理になる為、頻繁に行うと辛いことになる。

いまの自分にわかってるのはこの程度なのでメモレベル

ECS環境構築時のポイントをまとめておく

また作るときにハマると辛いからね。

SSHして見に行ってもいいんだけど、入らなくても状況がわかるようにしておくと楽。

大前提

すべてのdockerイメージで実行するプロセスのログはstdout,srderrに出しておく。 普通のアプリケーションだとログファイルに出力してfluentdとかで収集して、S3に保存しておいたりするんだけど、dockerでは標準出力以外は拾いません。 なので、nginxとかunicornとかのログはstdoutに設定しておきます。

これは普段開発する時も同じようなコンテナ構成にして、普段から標準出力で開発するようにしておくといい。

awslogsログドライバーを最初から設定する

こちらです。

これを設定すると、cloudwatch logsにdockerのログを送ってくれます ここに送られる内容はdockerコンテナに対して、docker logs コンテナ名 or コンテナIDのコマンドを叩いたときに出力されるものと同じです。

awslogsを簡単に見れるようにしておく

https://github.com/jorgebastida/awslogs awslogsコマンドを使うと、指定し cloudwatch logsのログをtailする感じで見れる。

ここまで設定しておけば、

ECSクラスターにコンテナを配置しているんだけど、何故かすぐに停止する。 ヘルスチェックが通らないというトラブル時にも対処が簡単になる。

ホストOSを通して通信できるようにする

コンテナをたてるEC2インスタンスにイカを設定しておく。 たぶん、デフォルトで設定されているはずなんだけどいつからかdockerコンテナから外へ出れなくなっていたので、一応設定しておく。

sysctl -w net.ipv4.ip_forward=1

fs.inotify.max_user_watchesを増やしておく。

inotifyの監視対象ファイル数の上限を上げる(Rails起動時に足りなくなる場合がある)

echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p

コンテナインスタンスで、dockerが使用するポートを全て開けておく

32768 ~ 61000番です。 ここにのってる。

CircleCI2.0 の Workflow を使っているよ

いまいちやる気が出ない時は一度立ち止まってブログを書きます。

Workflow

こちらですね。 ジョブを複数定義し、ジョブごとに依存を定義できる。

今までのCircleCI

いままでのビルドは1フローのみでした。 A-B-C-D というイメージ。 だけど実際はジョブが1つしか定義出来なかった(出来るけど使用するには自分でAPIを呼ぶ必要があった。)ので、A1-A2-A3-A4 みたいな感じだった。

A4はA1に依存してるかもしれないけど、A3には依存していない。とか、 buildとdeployは明確に分けることが出来るんだけど、deployにもいくつかdeploy対象があって、それらは並列でいいとか、あるけど必ず順次実行だった。

具体的にどう使っているか

CIのdeploy対象としては以下の2種類がある。

  • assets(js,css,image)
  • dockerイメージ

dockerイメージも複数種類があったりして、直列に処理をしていると結構な時間がかかる。 以下のような workflow にしている。

workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - app_deploy:
          requires:
            - build
          filters:
            branches:
              only:
                - develop
                - master
      - assets_deploy:
          requires:
            - build
          filters:
            branches:
              only:
                - develop
                - master
      - build_node_app:
          requires:
            - build
          filters:
            branches:
              only:
                - develop
                - master
      - node_app_deploy:
          requires:
            - build_node_app
          filters:
            branches:
              only:
                - develop
                - master

まず build ジョブが走る。実際内容は rspec の実行なんだけど、たぶん buildっていうジョブ名が固定?

それが成功すると、各deployジョブが走る。

app_deploy はRailsのdockerイメージをbuildしてECRにpushしている。

assets_deployは、webpackのbuildを行ってその成果物をS3にアップしている。

build_node_appはexpress用のjsをwebpackでbuildしている。

node_app_deployはbuild_node_appの成果物を受け取って、dockerイメージをbuildしてECRにpushしている。

ここを少し気をつける必要があるのは、各job自体がdockerで動いているということ。 そのdockerイメージは自分で指定するので、自分が行いたいbuild,deployが出来るdockerイメージを用意する必要がある。

app_deployの部分は以前に書いた、multi stage buildなのでdockerではなくmachineビルドになっている。

assets_deployは、webpackのbuildを行うので、nodejs, yarn, そしてS3コマンドを使うため、aws cliが入っているdockerイメージを用意している。

build_node_appは、assets_deployと同じでいい。nodejs, yarnがあるイメージ

node_app_deployは、dockerが入っているdockerが必要になる。そして、ECRにpushするので、aws cliが必要。 あと、build_node_appからartifact経由で成果物を受け取るときにtarコマンドが必要なのでそれも入れておく。

dockerが入っているdockerってなんやねん。って思うかもしれないけど、公式に用意されている。 イカのようなDockerfileからdockerイメージをbuildしてdocker hubとかに置いておく。 何も見られて困るようなものは入ってないので、普通にpublicで良い。

FROM docker:17.05.0-ce

RUN apk update && \
    apk add \
    ca-certificates \
    git \
    openssl \
    zip \
    unzip \
    wget \
    python \
    py-pip \
    py-setuptools \
    groff \
    less \
    tar && \
    pip install awscli

という感じで、色々と準備は必要なものの、CircleCI2.0より前の場合は、 machineビルドで、用意されたVMにその場で色々インストールして、buildしてdeployしていた。 それに比べると、純粋にbuildとdeploy以外は事前に済ませておけるので実行時間は早くなる。

つまり docker は最高。

なぜDockerを使うのかを整理する

※追記 整理とかいいつつ、雑に書きなぐっただけだったので少し文章と見栄えを直しました。

背景

いま作っているシステムではECSのクラスタ上に、Dockerコンテナを配置してアプリケーションをデプロイしている。 なぜ一般的なEC2にアプリケーションを配置する手法を使わずに、ECSでDockerを使用するのか。 自分の考えを整理する。

前提知識

ECSとは

AWSのサービスの1つでEC2ContainerServiceの略。 Dockerコンテナを運用をいい感じにやってくれるサービスです。

TL;DR;

  • アプリケーションの運用はいろいろ大変
  • 運用を楽にするために色々な技術がある
  • Dockerを使うとより楽になる

EC2にアプリケーションを配置する手法

通常EC2を使い際に、AWSが用意しているAMIをそのまま使用することは多くない。 理由としては幾つかあるが、雑に思いついたのはイカ

  • デフォルトでインストールされている言語以外、または別バージョンを使用したい
  • アプリケーションで使用するライブラリをインストールする必要がある
  • インスタンスSSHする際に使う鍵を起動時の設定とは別に用意したい

1つずつ詳しく解説していく。

デフォルトでインストールされている言語以外、または別バージョンを使用したい

いまは知らないが、一昔前のAmazonLinuxであればRubyのversionは2.0だった。 たぶん当時の最新は2.1とか2.2だったと思う。

バージョンがいくつ違うとかはまぁいいとして、重要なのは常に最新に追随するわけではないということ。 Rubyはversionupのたびに高速化している。 いままで最新バージョンを使用していて、既存のアプリケーションが動かなくなるというようなバグに遭遇したことはなく、基本的にはversionは上げ得である。 であれば、最新のメソッドも追加されているしRubyのversionは新しいほうがいい。

アプリケーションで使用するライブラリをインストールする必要がある

すぐに思いつくのはImageMagicとかだろうか。 画像処理を行うアプリケーションであればほぼ入れることになるであろうライブラリである。

インスタンスSSHする際に使う鍵を起動時の設定とは別に用意したい

EC2ではインスタンスSSHするためにkey pairと呼ばれる鍵を作成し、公開鍵をインスタンスに配置してくれる。 ユーザーは公開鍵を自身のマシンにセットし ec2-user とかAMIごとに指定のユーザーとしてSSHをする。

チームで開発する際に、SSHする可能性のあるメンバーには鍵を渡す必要がある。また、このデフォルトのユーザーはroot権限(sudo)を持っているため全員に配布するのはリスクがある。

そこで、インスタンスにデフォルトとは違うユーザーを作成し、そこにチームメンバーの公開鍵を配置する。 そうすることで各メンバーは自分のユーザーを持ち、自分の秘密鍵SSHが可能になる。

デフォルトのCloudwatchのメトリクスとは別にメトリクスを収集したい

Prometheusとか、そういうエージェントを入れることもあるよね。

どのようにEC2インスタンスを作るのか。(設定するのか)

簡単な方法だと、ユーザーデータと呼ばれるインスタンス起動時に実行されるスクリプトがある。ここに各種インストールのコマンドを記述すると起動時に色々インストールしてくれる。 しかし、これだと起動時に言語のビルドなども含まれると異常に起動が遅くなる。(正確に言うとすでに起動はしているので、起動後に自分の使いたいものが揃うのが遅くなる)

その為、一般的にはインスタンスを起動後、SSHして自分好みにセットアップし、AMI(インスタンスのイメージ)に保存する。 保存したAMIから起動することでセットアップされた状態でインスタンスを起動する。この手法を使うことで同じ環境のインスタンスを複数起動することが可能になる。

更に言うとそれらをいちいち手でセットアップするかというとそんな面倒なことはしたくない。その為、Chef、Ansibleなどの構成管理ツールと呼ばれるものを使用し、セットアップを行う。

それも面倒なのでPackerでAMIが自動生成されるようにしたりする。(実はPakcerはあんまりちゃんと使ったことがない)

EC2インスタンスで配置したアプリケーションの運用

アプリケーションを運用をしていく上で、いろいろな機能追加が入ると思う。 またRubyは毎年12月25日に最新バージョンがリリースされる。(バグフィックス等のバージョンアップは随時行われている)

Rubyのバージョンだけなら、アップデートしない。という選択もあるだろう。しかし、先述したようなImageMagickのようなライブラリが追加で必要になった場合はインスタンスにインストールをする必要がある。 運用しているサーバーが1台、2台ならSSHして手でセットアップ、または構成管理ツールでセットアップするという選択肢もあるだろう。(動いているサーバーにそんなことおれはやりたくないけど)

あんまりないけど再起動が必要なアップデートもある(近年ありがちなSSL,SSH脆弱性アップデートは再起動が必要だった気がする)そうなると、1台運用ならサーバーが停止することになり、複数台運用ならローリングアップデートが必要になる。

自分が昔関わっていたサイトでは毎月必ずインフラの定期メンテがあったりしたこともある。そこに併せて色々やる。

Dockerの良さ

ここまでの流れで運用の大変さとそれを回避するためにいろいろな技術があることがわかったと思う。

DockerはDockerイメージを作成しそのイメージからコンテナを生成しサーバーに配置する。 Dockerイメージは、イカのようなDockerfileから生成する。 イカから生成されるイメージはRuby2.4.1がインストールされたalpine linuxのイメージになる。 このイメージをベースに、自分のアプリケーションに必要な設定を追加していくことになる。

FROM ruby:2.4.1-apline

ADD . /app

すでにわかったかもしれないが、Dockerfileを更新し、イメージを再作成、再作成したイメージのコンテナをサーバーに配置する。これがDockerを利用した場合のアプリケーション(サーバー)の更新になる。

ここまでに記述したようなインフラの更新に必要なのは、 Dockerfileの更新、イメージのPush、新しいイメージからコンテナを配置になる。

またDockerは1つのコンテナに複数アプリを起動するようなことはせず、1コンテナ1プロセスが基本です。 EC2インスタンスで行うような1サーバーのリソースを有効に使うために複数アプリを起動したりするために1つのサーバーにいくつもの言語を入れたり、いくつものアプリケーションを起動するようなことはありません。1つ1つのDockerfileは1つのことをするための設定だけになり設定の管理としてもシンプルになりやすいと思っています。 それに加え、ECSクラスタ内のインスタンスにコンテナを配置する際にはMultiAZやリソースを考慮して配置するため、インスタンスのリソースを自動的に有効に使えるようになります。

Dockerを使いやすい環境

ただ、インフラアップデートが楽なだけでDockerを選択するわけではない。 ECSやGKEなどDockerのクラスタ運用を任せられるサービスがあるのが理由としては大きい。 さすがにクラスタの管理まですべて自前で、となると簡単に手が出せるものではなくなってくる。

正直な話し

Dockerになるとすごく楽になる。みたいな雰囲気で書きましたが、ECSではDockerコンテナを配置するインスタンスはEC2インスタンスになります。 面倒な部分が残っている可能性もあります。

今まで書いてきたようなEC2インスタンスと同じ使い方ではありませんが、もしSSHシたい場合は同じように設定が必要です。セキュリティアップデートも必要です。 それくらいなら、最新のAMIに変える。アップデートコマンドを叩く。等なので新しいAMIから起動してユーザーデータで設定で事足りるのでなんとかという考えです。 また、ECSではコンテナ配置時に配置させたくないインスタンスを設定できるため、比較的簡単にローリングアップデートを行うことができます。

A,Bインスタンスがあった場合に、Cインスタンスを起動。Aインスタンスにコンテナを配置しないようにする。 Aインスタンスを破棄。Dインスタンスを起動。 というようにすることで、安全にインスタンスを切り替えることができます。 Dockerコンテナも同じようなやり方で新しいコンテナに入れ替える。

Dockerの関するありがちな批判

デプロイごとにDockerイメージをbuildするの遅くない?

EC2を設定するのと同じように毎回、Ubuntuイメージとかをベースに使って、自分でRubyを入れて・・ みたいな事をしてたらクソ遅いです。 それをしないために、自分で作ったDockerイメージを保存するリポジトリがあります。 公式だとDockerHub、AWSにECRがあります。 アプリケーションに必要な、言語、ライブラリまでインストールしたイメージをリポジトリにPushしておき、 Deploy時はそのイメージをFROMに記述し、実行するアプリケーションのコードを追加、Rubyならbundle installする程度でしょう。bundle installも、Gemfileに更新がなければイメージに最初から含めておけばいいです。 一番早いのは、アプリケーションのコードを乗せるだけ。という使い方です。