あすたぴのブログ

astap(あすたぴ)のブログ

Rail5.1 rc1+Dockerでいい感じの環境を構築する

目的

いい感じに使える、Dockerの開発環境を構築する。 最終的な目的はProductionでDockerを使うイメージを固めること。

環境

  • Vagrant
  • Docker
    • Docker version 17.03.1-ce, build c6d412e
  • Rails5.1 rc1

docker for mac はいい噂を聞かないのでVagrant上でDockerを使用する。 当然、docker for mac でも動く。

Railsアプリ作成

bundle init
gem 'rails', '~> 5.1.0.rc1'
bundle instlal --path vendor/bundle

sprockets, turbolinksを抜いています。 DBはmysqlを使用

bundle exec rails new --skip-bundle -d mysql --skip-sprockets --skip-turbolinks .

ローカルにbundle設定があるとダメなので消します。

rm -rf vendor/bundle
rm -rf .bundle

Docker install

sudo apt-get update
sudo apt-get install \
    linux-image-extra-$(uname -r) \
    linux-image-extra-virtual

sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

sudo apt-get install docker-ce

dockerグループが出来ているので、vagrantユーザーをdockerグループに追加する必要があります。 その後、vagrantからlogoutし再度sshで入ります。

sudo gpasswd -a vagrant docker

docker-compose install

sudo curl -L "https://github.com/docker/compose/releases/download/1.11.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Dockerfile

Dockerfileを作成します。 alpine linuxをベースにRuby2.4.1の入っているイメージを元にします。 必要なパッケージをインストール後、不要なファイルを消したりします。 mysql2 gem をbuildするために必要なmariadb-devを入れるとmariadb本体も付いてきてしまうため、mariadb本体のファイルを削除しています。

cmd, entrypointはrun時に指定するので付けていません。

Dockerfile

FROM ruby:2.4.1-alpine

ENV LANG ja_JP.UTF-8
ENV BUILD_PACKAGES="curl-dev build-base" \
    DEV_PACKAGE="mariadb-libs mariadb-client mariadb-client-libs tzdata"

RUN gem install bundler \
  && apk --update --upgrade add $BUILD_PACKAGES \
  && apk add mariadb-dev tzdata linux-headers postgresql-dev sqlite-dev git nodejs \
  && rm /usr/lib/libmysqld* \
  && echo 'gem: --no-document' > /etc/gemrc

WORKDIR /app
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN bundle install

EXPOSE  3000

docker-compose.ymlを作成します。

spring server起動用のコンテナをwebと同一Dockerfileから作っています。 springについては以下を参考にさせていただきました。

参考

https://github.com/jonleighton/spring-docker-example http://tech.degica.com/ja/2016/06/14/dockerized-rails-development/

user指定時に、ホストOSのユーザーIDとグループIDを指定している。 ユーザーを指定しない場合、rootで実行されるため、Railsコマンドで生成したファイルがrootの所有となって、ホストOSのエディタから編集が出来なくなる。

version: '2'
services:
  db:
    image: mysql:5.7.17
    volumes:
      - ./store:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'

  web:
    build: .
    command: bundle exec rails s
    user: "${uid}:${gid}"
    working_dir: /app
    environment:
      RACK_ENV: 'development'
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    depends_on:
      - db

  spring:
    build: .
    command: docker/run.sh spring
    volumes:
      - .:/app

mysqlの設定

※追記 mysqlコンテナを用意するようにしたのでこちらは不要になりました。

DockerコンテナからホストOSのMySQLに接続するためにホストOS側のMySQLの設定を修正します。

sudo vi /etc/mysql/my.cnf

bindするアドレスに0.0.0.0を追加します。

bind-address    = 127.0.0.1
bind-address    = 0.0.0.0

MySQLを再起動します。

sudo service mysql restart

bindアドレス同様に、 rootアカウントがlocalhostからしかログインできないためユーザーを追加します。

mysql -u root
CREATE USER 'root'@'172.17.0.2';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'172.17.0.2';
FLUSH PRIVILEGES;

開発

docker-compose系のコマンドを叩くのが面倒な為、scriptを用意しています。

  • 引数なしでヘルプ表示
  • 第一引数がupなら、docker-compose up
  • 第一引数がrspecならRACK_ENV=testでrspec起動
  • 第一引数がsならフォアグラウンドでRails起動 port指定
  • 第一引数がspringならspringサーバーをバックグラウンドで起動
  • 引数ありの場合は引数をそのまま渡す。

docker/bundle.sh

#!/bin/bash
ENV=$RACK_ENV
export uid=$UID
export gid=$GID

if [ "$ENV" = 'production' ]; then
  docker-compose run --entrypoint=$entrypoint -e RACK_ENV=$ENV web s
  exit 0
fi

entrypoint=docker/run.sh
if [ "$1" = 'up' ]; then
  docker-compose up
elif [ "$1" = 'rspec' ]; then
  docker-compose run --entrypoint=$entrypoint -e RACK_ENV=test web $@
elif [ "$1" = 'spring' ]; then
  docker-compose run -u "$UID:$GID" --entrypoint=$entrypoint -d spring spring
elif [ "$1" = 's' ]; then
  docker-compose run -p 3000:3000 -u "" --entrypoint=$entrypoint -e RACK_ENV=development web s
else
  docker-compose run -u "$UID:$GID" --entrypoint=$entrypoint -e RACK_ENV=development web $@
fi

docker/run.sh

alpine linuxにはbash等が入っていなく、わざわざ入れる必要もないコードなのでデフォルトのシェルであるashで書いています。

#!/bin/ash

if [ "$RACK_ENV" = 'development' -o "$RACK_ENV" = 'test' -o "$RACK_ENV" = '' ]; then
  if [ $# -eq 0 ]; then
    bundle exec rails -h
  elif [ $1 = 'rspec' ]; then
    bundle exec $@
  elif [ $1 = 'spring' ]; then
    bundle exec spring binstub --all
    #bundle exec spring binstub --remove --all
    bin/spring server
  else
    time bundle exec rails $@
  fi
  exit 0
fi

bundle exec unicorn -c config/unicorn.rb -E $RACK_ENV

deploy

deploy時に使用するDockerfileを作る

FROM ruby:2.4.1-alpine

ENV LANG ja_JP.UTF-8
ENV BUILD_PACKAGES="curl-dev build-base" \
    DEV_PACKAGE="mariadb-libs mariadb-client mariadb-client-libs tzdata"

RUN gem install bundler \
  && apk --update --upgrade add $BUILD_PACKAGES \
  && apk add mariadb-dev tzdata linux-headers postgresql-dev sqlite-dev git nodejs \
  && rm /usr/lib/libmysqld* \
  && echo 'gem: --no-document' > /etc/gemrc

WORKDIR /app
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN bundle install

ENV APP_HOME /app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
COPY . $APP_HOME

EXPOSE  3000

circle_ciでbuildして、testを実行。 testが通ったら、imageをECRなり、DockerHubなり、GCRへpushする。

まとめ

当初、DockerfileでアプリのソースをCOPYしてイメージを作っていたが、 ソースを含めてイメージを作るのはdeploy時だけでいいのでやめた。 軽く開発してみたが上手く進められている。 deploy周り(CricleCI含め)を妄想しかしていないので、いざやってみたらまた変わるかもしれない。

問題点が1つあって、user指定で実行するとbundler関係のディレクトリがroot所有なのでwarningが出る。 解決策が思いつかない。どなたかいい解決策があれば教えてください。

`/` is not writable.
Bundler will use `/tmp/bundler/home/unknown' as your home directory temporarily.

今回のソースは以下にあります。 https://github.com/astapi/development_rails_with_docker

terraform で aws のいい感じの構成を作る(基盤編1)

対象読者

terraformが何かを知っていて、 terraformを使おうと考えている人。

terraform version 0.9.1

初期設定

適当にディレクトリを作成します。

mkdir terraform_test
cd terraform_test

0.9.1からstate environmentsを設定できるようになったので、 dev, stg, prod みたいに同一ソースで分岐ができるようになる。

vagrant-ubuntu-trusty-64% terraform env new dev
Created and switched to environment "dev"!

You're now on a new, empty environment. Environments isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.

env new をすると、作成したenvに移る。 gitのbranchみたいだな。

vagrant-ubuntu-trusty-64% terraform env list
  default
* dev

AWS 設定

provider "aws" {
  region = "ap-northeast-1"
}
variable "aws_account_id" {
  default = "1111111111"
}

IAMアカウントの作成 AWSのIAMベストプラクティスをちゃんとやる(見る) 上記エントリに沿って、初期設定を行う。

resource "aws_iam_user" "astapi" {
  name = "astapi"
  path = "/"
}

resource "aws_iam_access_key" "astapi" {
  user = "${aws_iam_user.astapi.name}"
}

plan

IAMのベストプラクティスでは、ルートアカウントのアクセスキーとか消さないといけないのだけど、 terraformでIAMユーザーから作りたいので、最初だけルートアカウントのキーを使う。

export AWS_ACCESS_KEY_ID="anaccesskey"
export AWS_SECRET_ACCESS_KEY="asecretkey"
export AWS_DEFAULT_REGION="ap-northeast-1"

planコマンドで作成予定のリソースを確認する。

terraform plan
中略

+ aws_iam_access_key.astapi
    encrypted_secret:  "<computed>"
    key_fingerprint:   "<computed>"
    secret:            "<computed>"
    ses_smtp_password: "<computed>"
    status:            "<computed>"
    user:              "astapi"

+ aws_iam_user.astapi
    arn:           "<computed>"
    force_destroy: "false"
    name:          "astapi"
    path:          "/"
    unique_id:     "<computed>"

apply

terraform apply

f:id:astap:20170326234644p:plain

IAMユーザーができました。

terraform show

このコマンドで作成したアクセスキーの情報が閲覧できます。

vagrant-ubuntu-trusty-64% terraform show
aws_iam_access_key.astapi:
  id = AKJLAKJDA
  secret = aslkfjaslfkjasljfas;ljfalskjdfalsjkfak
  ses_smtp_password = asdklfjasldkfjasdkfjasldjkfas;l
  status = Active
  user = astapi
aws_iam_user.astapi:
  id = astapi
  arn = arn:aws:iam::1111111111:user/astapi
  force_destroy = false
  name = astapi
  path = /
  unique_id = AAKLJALKFJAKDJALKJD

ポリシーのアタッチ

ルートアカウントを使わなくてもterraformで全ての権限が必要なので、AdministratorAccessが必要。

resource "aws_iam_policy_attachment" "admin" {
  name = "admin-policy"
  users = ["${aws_iam_user.astapi.name}"]
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}

今後は作成したIAMユーザーのACCESS_KEYを使って実行していきます。 ルートアカウントのACCESS_KEYを削除しましょう。

まとめ

terraform でIAMユーザーのアクセスキーまで作成したが、terraform showでキーが参照できるため、 複数人開発ではこの方式だとNGになりますね。 また、Backend(terraformで作成したAWSの状態を保存しておくところ)がlocalのため、 そもそも自分のローカルでしか実行が出来ない。 backendとしては色々用意されているが、S3 backendでは、terraform envに対応していなかった為、 counsul等、自前サーバーを用意したりする必要がありそう。 そのconsulサーバーを用意するterraformが必要・・・。 うーん。

プログラミングを魔法だと思っていた

昔、すごいプログラムだったりすごいシステムを魔法のように感じていた。 自分には全くどうやっているのか想像がつかなかった。 今からすると、なぜ魔法だと思っていたのかなっと思って、考えてみた。

フレームワークなんて、よくわからずにアクションを書いたら、 なぜかテンプレートに移動して、テンプレートを書くとブラウザにHTMLが表示された。 特定のURLを叩いたときに、特定のActionが呼ばれる理由もわからない。 そこにあるのは命名ルールだけだった。 特に疑問も持たずにルールに沿ってプログラムを書いて、 なんとなく画面が表示されてた。 プログラミングは辛かった。 もともとあるプログラムをコピペしていじって、なんとなく作っていた。 新しいものは作れない。自分にはわからない魔法だから。

ここまで読んでわかったヒトもいると思うけど、おれが触っていたのはJavaだった。 正直いまでもJavaはわからない。あれ以降やってないから。 Javaが苦手だったのは、外部ライブラリのコードが読みにくいから。 jarに固められた状態で手元に渡ってきて、その状態でなんとなく使う。 ドキュメントもあんまりないイメージだった。 jarという圧縮ファイルのブラックボックスだった。 たまに、逆コンパイルでコードを読んだりもしたけど、全然楽しくなかった。 なんで、わざわざ固められているものを戻して、解読みたいなことしてんだろって。 たぶん、これは自分の知識不足なだけで、きっと、元ソースもどっかにあったんだろうし、きっと、ドキュメントも探せばあったんだと思う。Javadocが。

そういう経緯があって、phpを触ったときはコードを奥深くまで読み進められるのが嬉しかった。 この頃から少しずつ、魔法ではなくなってきたかもしれない。

あるときちゃんと魔法に向き合おうと思ってコンピュータサイエンスとかhttpの仕様とか学ぶようになった。 そこからは、いろいろなことがクリアになったと思う。 蓋を開けてみれば、魔法だと思っていたことが実は泥臭いコードの塊だったり、 簡単なプログラムをうまく組み合わせることで魔法のように見えていただけだったりした。

魔法が魔法ではなかったと気づいた時から、仕組みがわからないものは仕組みを理解するように心がけるようになった。 そうした事を続けていると面白いことに、初めて触るライブラリやツールでも、 こういう機能がたぶんあるはず。こういう風に使うといいはず。 内部的にはたぶんこうなってるんだろうな。 みたいな事がなんとなくわかるようになっていた。 今はプログラミングはすごく楽しい。

自分のように、わけもわからずコードを書いている人がこの世にはまだいっぱいいると思う。 そういう人たちにも、プログラミングが楽しめるようになってほしい。

AWSのIAMベストプラクティスをちゃんとやる(見る)

IAM のベストプラクティスをちゃんと理解しようというお話。

目的

このエントリでは、細かいやり方は書かない。 (ドキュメントを見ればいいし、コンソールを開けばわかる) こういうことをしたほうがいいんだよ。という内容を把握し、いざ、必要な時がきたときに知ってるいる状態にすることが目的

対象読者

  • AWSはEC2とかたてたりするけど、*1ルートアカウントでコンソールをいじってる人
  • EC2にIAMインスタンスプロファイルのロールを付けるとは??な人
  • 権限とか難しいから、フルアクセス付けてるよ?な人

IAMとは?

AWS Identity and Access Management (IAM) の略 アイアムって読んでます。(正しいかは知らない)

IAMを使うと、どんな事ができるのか

  • ルートアカウント以外にアカウントを作成できる。
    • 会社のAWSアカウントを複数人で触る際に、個人個人にアカウントを発行する。
    • 外部(CI等)から、AWSにアクセスする必要がある際に使うアカウントを発行する。
  • 作成したアカウントに対して、*2リソースごとにアクセスを制限できる

ルートアカウントを共有しない。 フルアクセス権限を全員に渡さない。 誰が何のリソースをどこまで操作できるか。を制限しよう。 というのがIAMを使う理由の認識です。

やること

AWS アカウント(ルート)のアクセスキーをロックする

ルートアカウントはすべての権限があるため、 ルートアカウントの権限を用いて、AWSコマンド等を使用すると、請求情報まで取得できる。 見られて困るものは、見れないようにしましょうね。という話し

  • アクセスキーを持ってるなら削除する
  • IAMで、管理権限を持つユーザーを作成する。
  • ルートアカウントのパスワードは強度の高いものを設定しよう。
  • ルートアカウントにMFAを設定する。

個々の IAM ユーザーを作成する

AWSにアクセスする必要がある場合は、IAMでユーザーを作成する。 IAMユーザーのアクセス許可は、いつでも、有効、無効にすることができる。 ※ルートアカウントは他人に絶対に教えてはいけない。 たとえ、個々のIAMユーザーに管理権限を付与するとしても、 ルートアカウントでない限りはアクセスを制限できるので、 必ず、IAMユーザーを都度、作る。

AWS 定義のポリシーを使用して可能な限り権限を割り当てる

ポリシーとは AWSではアクセス権限を制御するのにポリシーという定義を利用する。

ポリシー例

{
    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action": [
            "aws-portal:*Billing",
            "aws-portal:*Usage",
            "aws-portal:*PaymentMethods",
            "budgets:ViewBudget",
            "budgets:ModifyBudget"
        ],
        "Resource": "*"
    }]

}

AWS内でデフォルトで定義されているポリシーを使って可能な限り、権限を割り当てる。 メリットとしては、簡単。 デメリットとしては、権限の単位がざっくりしている。

例 - AdministratorAccess - フルアクセス権限 - AWSCloudTrailReadOnlyAccess - ClourTrailの読み取り権限

職務によって、ポリシーがAWSで定義されている。 職務機能の AWS 管理ポリシー

IAM ユーザーへのアクセス許可を割り当てるためにグループを使います。

ポリシーをIAMユーザー一人ひとりに付けるのは面倒だし、 複数人いても大体同じ権限になる。という場合に、グループを使う。

グループの作成

ここらへんで、単語が増えてきて混乱してきたと思うので、 以下にユーザ、ポリシー、グループの関係を整理。

  1. グループ作成
  2. グループにポリシーをアタッチ(付与】
  3. ユーザーをグループに追加

ポリシーはユーザーに直接アタッチも可能。

最小限の特権を認める。

まずは、最小限のアクセス(特定サービスへの権限、リードオンリー)を割当て、 必要になった際に権限を解放していくようにしよう。という話し

Access Advisorを使う アクセスアドバイザーは、コンソールでIAMユーザーを見る場合にタブの1つである。 該当ユーザーが、権限が付与されているサービスにいつアクセスしたかがわかる。 この項目を参照し、1度もアクセスをしていないサービスがあれば、 その権限はそのユーザーには不要な可能性があるため、 権限の見直しに使える。

ユーザーのために強度の高いパスワードポリシーを設定する。

IAM ユーザー用のアカウントパスワードポリシーの設定

IAMユーザーのパスワードに、パスワードポリシーを設定できる

特権ユーザーに対して、MFA を有効化する。

AWS での多要素認証 (MFA) の使用 ルートアカウント同様に、重要なリソースへの権限を持っているユーザーにはMFAを設定しようという話し。 例えば、(どのサービスでも)削除権限を持ってる人とか。

Amazon EC2 インスタンスで作動するアプリケーションに対し、ロールを使用する。

EC2上から別AWSサービスにアクセスすることはよくある。 そのアクセス権限は適切に定義しよう。という話し

ロールとは、AWSのリソースにアクセスできる権限の役割の定義

また新しい言葉が出てきたので、整理する。

  • IAM ポリシー
    • AWS上で扱う権限の一番小さい単位
  • IAM ユーザー
    • AWSにアクセスするユーザーの単位
    • グループに含めたり、ポリシーをアタッチして権限を付与する
  • IAM グループ
    • 複数のポリシーをまとめたもの
    • 複数のユーザーに同一の権限を与えるために使う
  • IAM ロール
    • 複数のポリシーをまとめたもの
    • ロールを付与したものに権限を委任することができる。
    • 主にはEC2インスタンスへ権限を付与する場合に使う
    • AWSアカウントのIAMユーザーにロールを付与することもできる。

認証情報を共有するのではなく、ロールを使って委託する。

さっきのロールの使い方での他AWSアカウントへは、アクセスキーを共有するんじゃなくて、 ロールを使おうね。という話し

認証情報を定期的にローテーションする。

定期的に認証情報を更新しようという話し

不要な認証情報の削除

使われていない認証情報があったら消そう。という話し

認証情報レポートや、awsコマンドで最後に使用した日付とかがわかるので、 それを見て、消す対象を精査できる。

追加セキュリティに対するポリシー条件を使用する。

ポリシーに対して、IPアドレスの制限が時間の制限を行うことができるので、 可能な限り、制限したほうがいいよ。という話し

AWS アカウントのアクティビティの監視

  • cloudfront
  • cloudtrail
  • cloudwatch
  • aws config
  • S3

の、アクセスログを利用して、不正なアクセス、利用と監視しようという話し

まとめ

全部やるのは過剰なのかな?という気もしますが、 そこは自分たちのやり方に合わせて、うまくやっていくのがいいのかなと思います。

*1:ルートアカウントとは、メールアドレス、パスワードでログインをするユーザーのこと

*2:AWSでは、ARNごとに、1リソースとして扱う。