Terraform と Terraform Cloud について理解し直す

久しぶりに使うことになったのでアンラーニングを兼ねてメモ

Terraformって何?

クラウドインフラに対するinfrastructure as code。クラウドインフラの設定をコード化して、保存/実行できる

www.terraform.io

infrastructure as codeできると何が良いの?

コードにすることで、インフラの更新をVCSに残せる

VCSに残せると、変更履歴が残せる。インフラの変更をレビューしやすくなる。CIに乗せることができる。とメリットが多い

Terraformがinfrastructure as codeできるインフラって何があるの?

AWSとかGCPとかAzureとかいろいろ。Datadogの設定とかもTerraform化できる

AWSだとCloudFormationもあるけど?

肌感覚だとTerraform使っている人が多い印象。ぶっちゃけどっちでも良いと思うのでチームで決めれば良いと思う。弊社はTerraform

Terraformの流れ

  • インフラの設定を .tf ファイルに独自言語で記述
  • terraform plan で変更内容を確認
  • terraform apply で実際に変更

変更されたあと、生成されたリソースのIDなどの具体的な情報が tfstate というテキストファイルに保持される。ここがポイント

tfstateをVCSで管理することは非推奨

VCS を用いることは非推奨とされています。これは例えば複数人が git clone して同時に terraform apply を実行してしまった場合などに、競合が発生する可能性があるからです。 tfstate は常に唯一無二のファイルがどこかに存在し、誰もがそのファイルを参照する必要があります。

chroju.github.io

んじゃどうやって管理するの?

Terraformでは、backendという設定でtfstateの管理場所を設定できる。AWSだと大体S3にupして管理するのが主流っぽい

ただしS3にしても、複数箇所で同時にTerraform applyが実行されると、変更がコンフリクトする可能性がある

これを回避するため、S3+何かしらのデータベースでロックして、1回に1人しかTerraform applyできないように設定することもできる(らしい)

ここまでやれば安心だけど、Terraformのためにインフラを作る必要があったりして大変

そこで Terraform Coud

Terraform Cloudは公式のterraformのSaaS

www.terraform.io

できることは大まかに

  • tfstateの管理
  • Terraformコマンドの実行
  • TerraformのCI化(VCSの更新をトリガに terraform apply を実行できる)
  • APIキーなどの秘匿情報のセキュアな管理

エンジニアはtfファイルを書いて、github に push すれば、あとのことはTerraform Cloudがやってくれるので大変便利

今現在、フリープランだとチーム5名まで無料なので、小規模なチームだったらとりあえず入れちゃって良いんじゃないかなという理解

www.hashicorp.com

無料版と有料版の違いはこちらも参照

qiita.com

TerraformのLinerやセキュリティスキャナーについて

このあたりをgithub Actionsで動作させると幸せになれるはず

tflint

Terraformのlinter。存在しないインスタンスタイプなど、Terraformではエラーにならないが、NGなものをチェックして指摘してくれる

github.com

tfsec

Terraformのセキュリティスキャナー。機密情報が紛れていないかとか、AWSやGCPのベストプラクティスに違反していないかチェックしてくれる

github.com

まとめ

  • Terraformはクラウドインフラのinfrastructure as code
  • Terraform Cloudをつかうことで、面倒くさいところをSaaSに移譲できる
  • Linerやセキュリティスキャナーがあるので使おう

dockle の警告 `CIS-DI-0008` をどのように処理すべきか考えた

dockle って?

コンテナイメージのセキュリティチェックツールです。ビルドしたImageを通すと、セキュリティ的に問題がある部分を指摘してくれます

github.com

警告 CIS-DI-0008 って?

これですね

CIS-DI-0008: Remove setuid and setgid permissions in the images

github.com

この警告が出てきた! という記事は多いのですが、それをどういう風に処理した。という記事がなかったので書いてみました

setuidsetgid って?

linuxにおける特殊なアクセス権限で、

  • setuid: ユーザが 所有者の権限 でそのファイルを実行できる設定
  • setgid: ユーザが 所有者グループの権限 でそのファイルを実行できる設定

なので、rootユーザが所有者や所有者グループのままの実行ファイルがあると、それをroot権限で実行できてしまうようです

dockleで警告を受けた、実行ファイルってどれ?

base Imageが debian-slim の時に、これらの実行ファイルについて警告が出ました

/bin/umount
/usr/bin/expiry
/bin/su
/usr/bin/wall
/usr/bin/gpasswd
/bin/mount
/usr/bin/passwd
/sbin/unix_chkpwd
/usr/bin/chage
/usr/bin/chsh
/usr/bin/chfn
/usr/bin/newgrp

それぞれどんな実行ファイルなの?

調べたらこんな感じでした

  • /bin/umount: ファイルシステムのマウントを解除する
  • /usr/bin/expiry: パスワードの有効期限ポリシーを定義する
  • /bin/su: ユーザ切り替えコマンド
  • /usr/bin/wall: ログインしている端末に一斉に通知を送る(昔共有マシンを落とす時とかによく使いましたね...
  • /usr/bin/gpasswd: ユーザが所属するグループを管理する
  • /bin/mount: ファイルシステムをマウントする
  • /usr/bin/passwd: パスワードを管理する
  • /sbin/unix_chkpwd: パスワードをチェックする
  • /usr/bin/chage: パスワードの有効期限を設定する
  • /usr/bin/chsh: ログインシェルを変更する
  • /usr/bin/chfn: ユーザ情報を管理する
  • /usr/bin/newgrp: 新しいグループにログインする

それで、どうしたの?

上記実行ファイルすべての setuid、setgid を外すことにしました

# 特殊なアクセス権、suid, sgid を排除
# 想定外のアクセス権でコマンドを実行されることを防ぐ
#
# see: https://github.com/goodwithtech/dockle/blob/master/CHECKPOINT.md#CIS-DI-0008
RUN \
  chmod u-s /bin/umount && \
  chmod g-s /usr/bin/expiry && \
  chmod u-s /bin/su && \
  chmod g-s /usr/bin/wall && \
  chmod u-s /usr/bin/gpasswd && \
  chmod u-s /bin/mount && \
  chmod u-s /usr/bin/passwd && \
  chmod g-s /sbin/unix_chkpwd && \
  chmod g-s /usr/bin/chage && \
  chmod u-s /usr/bin/chsh && \
  chmod u-s /usr/bin/chfn && \
  chmod u-s /usr/bin/newgrp

そんなことしちゃって大丈夫なの?

それぞれ、Dockerコンテナを運用する限り、普通は使わないコマンドだと思いました

それに、放置して脆弱性が発生した場合に面倒だと思い、最初から上位権限で実行できないようにしたほうが早いなと思ったので

(まだ本番運用はしていないけど多分大丈夫なはず...)

個人的なdockleの運用方針

  • warnも全部潰す(下手に例外を作るとそのほうが最終的に面倒くさくなるので)
  • 警告については、それぞれ公式のgithubに対応方法が書かれているのでそれに従う(めっちゃ親切ですね)
  • (まだやれていないけど)CIに仕込んで、毎回実行して失敗した場合には修正するようにしたい

参考

👇 setuidsetgid の解説ページ

linuxg.net

そんなかんじです

パイプを使うと最後の処理が成功すると戻り値は0になるので、`pipefail` を使ったほうが事故が少なくなるよというお話

コード例

#!/bin/bash -e

exit 1 | echo "a"
echo "b"

実行結果

a
b

-e オプションで、行単位で失敗したら終了するはずなのに、 echo "b" が実行されてしまっている...!

解説

pipeを使う場合、一番右側の処理の戻り値で行の成功/失敗が判定される

exit 1 | echo "a"
echo $? # <= 0

対策

set -o pipefail を使う

  • pipefail は、パイプの左側の処理で失敗した場合、戻り値を失敗扱いにする処理

コード例

#!/bin/bash -e

set -o pipefail

exit 1 | echo "a"
echo "b"

実行結果

a

パイプの右側も実行されるが、行全体としては失敗扱いとなり、 -e オプションにより処理が終了する

参考

DockerfileのLintツール hadolint でも、以下のようにして RUN 実行時に pipefail が設定されるように推奨している

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

see: https://github.com/hadolint/hadolint/wiki/DL4006

UnicornとNginxの接続方法は、UNIXドメインソケットとリバースプロキシの2つの方法がある

UNIXドメインソケット

単一マシン上の高効率なプロセス間通信に用いられる機能・インターフェースの一種である

UNIXドメインソケット - Wikipedia

  • ファイルシステムを介してプロセス同士の通信を行う仕組み
  • 普通にリバースプロキシするより高速だが、ファイルシステムを介するので単一マシン上でしか動作できない
  • UnicornとNginxが同一マシン上にある場合は、これ一択

Railsの場合の設定方法

config/unicorn-config.rb

# Nginxとはソケット通信
listen "/tmp/unicorn.sock"
  • listen + ソケットのPATH でリスナーを設定する

nginx側の設定

http {
  upstream unicorn {
    server unix:/tmp/unicorn-cms.sock
  }
  location / {
     proxy_pass http://unicorn;
  }
}
  • serverunix:${path} で、unicornで設定したUNIXドメインソケットのpathを設定する
  • proxy_pass でリクエストをUNIXドメインソケットに渡す

リバースプロキシ

普通にTCP通信で、unicornにリクエストを投げる方法

  • UNIXドメインソケットより低速
  • UnicornとNginxが別マシンの場合はこちら

Railsの場合の設定方法

config/unicorn-config.rb

listen 3000
  • listen + ポート番号 でリスナーを設定する

nginx側の設定

http {
  upstream unicorn {
    server exsample.com:3000
  }
  location / {
     proxy_pass http://unicorn;
  }
}
  • serverドメイン:ポート番号 で、Unicornがある転送先を設定
  • proxy_pass でリクエストを転送先に渡す

DockerでUnicornとNginxが別コンテナの場合

別コンテナでもボリューム経由でファイルを共有できるので、UNIXドメインソケットを使える

やっぱりコンテナ間で通信より、早いらしいので、UNIXドメインソケットを使ったほうが良い

qiita.com

参考

qiita.com

www.honeybadger.io

docker-slim で 任意のバージョンの ImageMagickを入れる方法

レガシーなシステムをDocker化するときにたまによくやるのでメモ

多分、Aplineでもおんなじだと思う

方法

コードからmakeする

ARG IMAGE_MAGICK_VERSION=${好きなパージョン}
RUN wget --quiet https://imagemagick.org/download/releases/ImageMagick-$IMAGE_MAGICK_VERSION.tar.xz && \
    tar Jxfv ImageMagick-$IMAGE_MAGICK_VERSION.tar.xz && \
    cd ImageMagick-$IMAGE_MAGICK_VERSION/ && \
    export PKG_CONFIG_PATH="/usr/local/libpng/lib/pkgconfig/:/usr/local/zlib/lib/pkgconfig/" && \
    ./configure --prefix=/usr/local/magick --with-png && \
    make && \
    make install && \
    cd .. && \
    rm -f ImageMagick-$IMAGE_MAGICK_VERSION.tar.xz && \
    rm -rf ImageMagick-$IMAGE_MAGICK_VERSION
ENV PATH /usr/local/magick/bin:$PATH

ポイント

  • configure --prefix でmakeされたバイナリの格納先を指定する
  • make install の後に、アーカイブとディレクトリを削除してimageサイズをへらす
  • PATHは ENV で設定する。 RUN export で設定しても、その RUN の中でしか有効ではない

Rails on Docker での PostgreSQL & unicorn が動く開発環境のメモ

自己学習のためにざっくり作ってみた

個人的には、本番はUnicornでも開発環境ではPumaでいいんじゃないかな...って思っている

SQLは合わせたほうが良いと思うけど

前準備

experimentalな機能を使うので環境変数を予め設定しておく

.envrc

export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

Dockerfile

# syntax = docker/dockerfile:1.0-experimental
################################################################
# node Image
################################################################
# 先にマルチステージビルドでNode.jsとYarnを用意
# そうすることで、rails側のDockerfileの記述がシンプルになる
FROM node:12.18-alpine as node

# zipダウンロードの流派もあるけど、記述がシンプルなので apk add を使用
RUN apk --update add --no-cache yarn

################################################################
# rails Image
################################################################
FROM ruby:2.7.1-alpine

# node image から、Node.jsとYarnをコピー
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /opt/yarn-* /opt/yarn
RUN ln -fs /opt/yarn/bin/yarn /usr/local/bin/yarn

ENV RAILS_ENV development
ENV NODE_ENV development

ENV ROOT_PATH /app/
ENV LANG C.UTF-8
ENV PORT 80

WORKDIR $ROOT_PATH

# 必要なパッケージのインストール
RUN apk add --update --no-cache \
      postgresql-client \
      xz-dev \
      tzdata

# ビルドでしか使用しないパッケージは
# 後で削除するため virtual tag をつけている
RUN apk add --update --no-cache --virtual=build-dependencies \
      build-base \
      curl-dev \
      linux-headers \
      libxml2-dev \
      libxslt-dev \
      postgresql-dev \
      ruby-dev \
      yaml-dev \
      zlib-dev

# Gemfile.lock とおなじバージョンの bundler をインストール
RUN gem install bundler -v '2.1.4'

# 全部コピーするとRailsアプリの変更の度にここからビルドし直しになるので
# 必要なGemfileやyarnのファイルを先にコピーする
COPY Gemfile Gemfile.lock package.json yarn.lock $ROOT_PATH

# mount cacheを利用して、gemをキャッシュする
RUN bundle config set path .cache/bundle
RUN --mount=type=cache,target=/app/.cache/bundle \
    bundle install && \
    mkdir -p vendor && \
    cp -ar .cache/bundle vendor/bundle

# ビルドでのみ使用するパッケージの削除
RUN apk del --purge build-dependencies

COPY . /myapp

# 起動
CMD rm -f tmp/pids/unicorn.pid && \
      bundle exec unicorn_rails -E $RAILS_ENV -c config/unicorn.rb -p ${PORT}

Dockerfileの方針

ビルド時間を短くして、無駄な時間を減らす

そのために、

  • 上の方は変更がなるべく無いようにして、イメージレイヤーのビルド数をへらす
  • alpineを利用したり、不要なパッケージを削除して、なるべくimageサイズを小さくする
  • mount cacheを利用して、gemをキャッシュする(2分半ほど処理が短くなった!)

docker-compose.yml

version: '3'
services:
  db:
    image: postgres:12-alpine
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: 'postgres'
    # 開発用。直接DBにアクセスしたい時のために
    ports:
      - "5432:5432"
  web:
    build: .
    ports:
      - "3000:80"
    volumes:
      - .:/app
    environment:
      DATABASE_USER: postgres
      DATABASE_PASSWORD: postgres
      DATABASE_HOST: db
    depends_on:
      - db

# databaseのデータはvolumeとして保存
volumes:
  db_data:

docker-compose.yml の方針

あんまり変わったことはしていない

Dockerfile側は、なるべく変数で制御して、環境毎の変数を docker-compose 側に持たせる流派もあるけど、まぁそこまではやらなくても良いのかな...? と思っている

  • 個人的には、Dockerfileはそこまで汎化せずに、環境毎にDockerfile作ってもいいんじゃないかなと思っている
  • だいたい事情で汎用化は破綻するので

以下に、Docker関係以外で初期状態から設定を追加/変更した部分を記す

config/unicorn.rb

# frozen_string_literal: true

APP_DIR = `pwd`.delete("\n")

working_directory APP_DIR

pid "#{APP_DIR}/tmp/pids/unicorn.pid"

worker_processes 2
# backlog は、workerが作業中でも受け取るTCPコネクションの数
# デフォルトの1024だと大量アクセスの時に待ちが大量発生するので減らしている
listen '/tmp/unicorn.sock', backlog: 64
timeout 60

config/unicorn.rb の方針

あんまり特筆すべきことはない。 backlog の数をデフォルトから減らしているところくらいかな...

config/database.yml

default: &default
  adapter: postgresql
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
  username: <%= ENV['DATABASE_USER'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>
  port: <%= ENV['DATABASE_PORT'] %>

development:
  <<: *default
  database: development

test:
  <<: *default
  database: test

production:
  <<: *default
  database: production

config/database.yml の方針

こちらも、各設定値を環境変数から取ってくるように変えたくらいかな...

config/environments/development.rb

ログ出力の標準出力化

  # Use an evented file watcher to asynchronously detect changes in source code,
  # routes, locales, etc. This feature depends on the listen gem.
  config.file_watcher = ActiveSupport::EventedFileUpdateChecker

  config.logger = ActiveSupport::Logger.new($stdout)
  # syncを有効にしないと、バッファリングされてログが一定量溜まらないと出力されない
  $stdout.sync=true

参考

👇 buildkit を使ったDocker buildの解説

medium.com

👇 docker-compose で buildkit を使う方法の解説

qiita.com

👇 効率の良いDockerfileの書き方

www.slideshare.net

👇 jokerさんによる、最近のRailsのDockerfileの書き方

speakerdeck.com

👇 効率の良いRailsのDockerfileの書き方

techracho.bpsinc.jp

PumaとUnicornの違い

前提

どちらもアプリケーションサーバ(アプリケーションを動作させるためのサーバ)

アプリケーションサーバーは送られてきたリクエストに対して、rubyやphpなどを実行して、動的な処理をした結果を静的な要素に変換してwebサーバーに返すためのもの。つまり、動的なサイトを動かす上で必要なもののうち、静的ではない部分を作ってくれるもののイメージ。

qiita.com

それぞれ、rackというwebアプリとアプリケーションサーバ間のインターフェイスの仕様&実装に準拠していて、それを使ってRailsと通信を行っている(Railsも、もちろんrack準拠)

Railsアプリの動かし方の違い

  • Unicorn: マルチプロセス
  • Puma: マルチプロセス & マルチスレッド

どちらも、1つのアプリケーションサーバで複数のRailsプロセスを起動して、処理の効率化を図っている

Pumaは更に、プロセスをマルチスレッド化している。これにより、I/O wait が発生した時に、別スレッドの処理を進められるため、マルチプロセスのみより効率が良い

2020/08/07 追記

unicornはマルチプロセスではなく、

thinやmongrelみたいなマルチプロセスによるclusterではなく、forkを使ったmaster-slave

だそうです

master-slave のほうが以下のようなメリットがあるとのこと

  • メモリ効率が良いかも
  • プロセスが死ぬわけではないのでダウンタイムが発生しない
  • 結果的にデプロイ時にダウンタイムが発生しない
  • デプロイが早い

techracho.bpsinc.jp

Unicronの弱点

スロークライアント(リクエストが遅いクライアント) に弱い

スロークライアントが来た場合、workerが待ちのまま止まってしまうという特徴がある。そのためNginxなどリバースプロキシを挟んで、スロークライアントをバッファしてもらう必要がある

これは、Unicornの不具合というより仕様。Unix哲学の「一つのことをうまくやる」に沿って作られているため。スロークライアントのバッファは、リバースプロキシに任せるように作られている

Pumaはスロークライアントが来ても1つのスレッドが埋まるだけなので、スロークライアント対応のためのリバースプロキシは不要

Pumaのほうが優れているの?

そんなこともない

マルチスレッドということは逆に言うとスレッドセーフな実装をする必要があり、当然使用しているgemもスレッドセーフであることを保証しなければならない(たいへん

また、Pumaにすればリバースプロキシがいらないの? というとそんなこともなくて、リクエストのバッファ以外にもいろいろやりたいことが出てきたりするので (healthcheck.htmlでempty_gif返したり)、結局Nginxなどのwebサーバを間に挟むことが多いはず(多分)

あと、Pumaはマルチスレッドだけど、そもそもRubyが純粋なネイティブスレッドではないので、Pumaを本気を出させるには、JRubyとかネイティブスレッドに対応したRuby実装を使ったほうが良いらしいとのこと

t.co

個人的な結論

結局のところ、よっぽど大量のリクエストを高速にさばく必要がなければ、どちらでもいいんじゃないかな...

個人的には、Pumaのマルチスレッドは想定外の所でめんどくさい動きをしそうだから怖いな...

Heroku(Pumaを激推ししている)でRails動かしてる時に、マルチスレッドで困ることあるのかな?

参考

👇 ほぼこの記事の引き写しです

t.co

👇 pumaとunicornとpassengerの比較記事。かなりunicornをディスっている

t.co