あすたぴのブログ

astap(あすたぴ)のブログ

terraformで aws のいい感じの構成を作る (subnet編)

対象読者

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

terraform version 0.9.1

subnet とは何なのか。

VPCのネットワーク内で更に細かくネットワークを分割したもの。 http://docs.aws.amazon.com/ja_jp/AmazonVPC/latest/UserGuide/VPC_Subnets.html http://docs.aws.amazon.com/ja_jp/AmazonVPC/latest/UserGuide/VPC_Scenario2.html ここらへんの話しになります。

今回は、イカ3つのsubnet構成を作ります。

  • public
  • nat-private
  • private

public サブネット

public サブネットには、public ip アドレスを付与し、VPCの外からも見えるネットワークを作ります。 ELBを配置します。 また、nat-privateへの踏み台サーバーを建てたりします。

nat-private サブネット

外部ネットワークからアクセスは行えず、外のネットワークへ出る際に、nat を通るネットワーク おもに、自分で作ったアプリケーション(Railsアプリとか)を配置します。

private サブネット

外部ネットワークからアクセスは行えず、外のネットワークに出る必要のないネットワーク Database等のdatastoreを配置します。

multi-az

awsにはAvailabilityZoneと呼ばれる区分けがあります。 これは、物理的なインスタンスの位置の違いを示しており、片方のAZ(データセンター)が(天災とか)で壊れても、もう片方のAZにも配置しておくことでサービスが死なないようにするという手法に使います。

subnetはそれぞれAZごとに作ります。 東京リージョンでは a, c とAZがあるので2つ作ることになります。

subenet 作成のコード

resource "aws_subnet" "public1a" {
  vpc_id = "${aws_vpc.vpc.id}"

  cidr_block        = "${cidrsubnet(aws_vpc.vpc.cidr_block, 8, 0)}"
  availability_zone = "ap-northeast-1a"

  tags {
    Name    = "${var.service}-${var.envname}-public1a"
  }
}

resource "aws_subnet" "public1c" {
  vpc_id = "${aws_vpc.vpc.id}"

  cidr_block        = "${cidrsubnet(aws_vpc.vpc.cidr_block, 8, 1)}"
  availability_zone = "ap-northeast-1c"

  tags {
    Name    = "${var.service}-${var.envname}-public1c"
  }
}

resource "aws_subnet" "nat-private1a" {
  vpc_id = "${aws_vpc.vpc.id}"

  cidr_block        = "${cidrsubnet(aws_vpc.vpc.cidr_block, 8, 4)}"
  availability_zone = "ap-northeast-1a"

  tags {
    Name    = "${var.service}-${var.envname}-nat-private1a"
  }
}

resource "aws_subnet" "nat-private1c" {
  vpc_id = "${aws_vpc.vpc.id}"

  cidr_block        = "${cidrsubnet(aws_vpc.vpc.cidr_block, 8, 5)}"
  availability_zone = "ap-northeast-1c"

  tags {
    Name    = "${var.service}-${var.envname}-nat-private1c"
  }
}

resource "aws_subnet" "private1a" {
  vpc_id = "${aws_vpc.vpc.id}"

  cidr_block        = "${cidrsubnet(aws_vpc.vpc.cidr_block, 8, 2)}"
  availability_zone = "ap-northeast-1a"

  tags {
    Name    = "${var.service}-${var.envname}-private1a"
  }
}

resource "aws_subnet" "private1c" {
  vpc_id = "${aws_vpc.vpc.id}"

  cidr_block        = "${cidrsubnet(aws_vpc.vpc.cidr_block, 8, 3)}"
  availability_zone = "ap-northeast-1c"

  tags {
    Name    = "${var.service}-${var.envname}-private1c"
  }
}

cidr_blockは、192.168.0.0/24 192.168.1.0/24 みたいになってます。 1つのsubnetで 256 アドレス使えます。ただ、AWSがいろいろ使うので実際にはこれよりちょっと少ないです。

subnet のネットワーク

subnet内でのネットワークの流れを定義する必要があります。 どうやって外と通信するか。という感じです。

public subnetでは、internet gateway を用意します。 これはVPCとしてのネットワークの出入り口でもあります。

nat-private subnetでは nat_gateway というものを用意します。 これは実態としてはEC2インスタンス(だとおもう)で、public ipアドレスを付与する必要があります。 以前は、AWSが用意したnat用のAMIを自身で起動して用意していましたが、nat_gatewayというマネージドなnatが利用できるようになりました。

イカ、コード

resource "aws_internet_gateway" "gw" {
  vpc_id = "${aws_vpc.vpc.id}"

  tags {
    Name    = "${var.service}_${var.envname}_gateway"
  }
}

resource "aws_eip" "nat_gateway" {
  vpc = true
}

resource "aws_nat_gateway" "nat_gw" {
  allocation_id = "${aws_eip.nat_gateway.id}"
  subnet_id     = "${aws_subnet.public1a.id}"
  depends_on    = ["aws_internet_gateway.gw"]
}

ルートテーブル

実際の、subnetのルーティングを設定します。 0.0.0.0/0 、つまりすべてのネットワークをgatewayに流しています。

resource "aws_route_table" "public" {
  vpc_id = "${aws_vpc.vpc.id}"

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = "${aws_internet_gateway.gw.id}"
  }

  tags {
    Name    = "${var.service}_${var.envname}_public"
  }
}

resource "aws_route_table" "nat-private" {
  vpc_id = "${aws_vpc.vpc.id}"

  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = "${aws_nat_gateway.nat_gw.id}"
  }

  tags {
    Name    = "${var.service}_${var.envname}_nat-private"
  }
}

ルートテーブルを作っただけではダメで、それをどのsubnetで使うかを設定します。

resource "aws_route_table_association" "public1a" {
  route_table_id = "${aws_route_table.public.id}"
  subnet_id      = "${aws_subnet.public1a.id}"
}

resource "aws_route_table_association" "public1c" {
  route_table_id = "${aws_route_table.public.id}"
  subnet_id      = "${aws_subnet.public1c.id}"
}

resource "aws_route_table_association" "nat-private1a" {
  route_table_id = "${aws_route_table.nat-private.id}"
  subnet_id      = "${aws_subnet.nat-private1a.id}"
}

resource "aws_route_table_association" "nat-private1c" {
  route_table_id = "${aws_route_table.nat-private.id}"
  subnet_id      = "${aws_subnet.nat-private1c.id}"
}

まとめ

正直、かなりハマりました。 というものの、terraformで作らないでAWSコンソールで作ると、最初からいい感じに設定されているんですよね。 自分で、terraformで細かく作って初めて知ったものは多かったです。 nat-privateのネットワークがハマりどころだと思います。

CircleCI2.0でRailsアプリをdocker multi stage buildをする

※2017/05/29 時点くらいの話しです。

現在は改善されているかもしれません。

multi stage buildとは

https://speakerdeck.com/toricls/understanding-dockers-multi-stage-builds こちらが詳しい。

build環境と、実行環境を別イメージとしてbuildしたい。 それを簡単に出来るようになったよ。という話し。

なぜ別イメージにしたいかと言うと、Goの例がわかりやすいが、 Goはワンバイナリで実行可能な為、 Goをコンパイルするために必要なライブラリ等は、実行環境のイメージには必要がない。 dockerのイメージは小さいほど、(docker pull含めて)実行が早いので軽いほうがいい。

CircleCI2.0 で実行する方法

CircleCI2.0では、build時に自分で好きなDockerのvesionを使えるよ。とうたっている。 https://circleci.com/docs/2.0/building-docker-images/

      - run:
          name: Install Docker client
          command: |
            set -x
            VER="17.03.0-ce"
            curl -L -o /tmp/docker-$VER.tgz https://get.docker.com/builds/Linux/x86_64/docker-$VER.tgz
            tar -xz -C /tmp -f /tmp/docker-$VER.tgz
            mv /tmp/docker/* /usr/bin

ここで 17.03.0-ce を指定しているのですが、multi-stage buildを行うためには、17.05.0-ce が必要になる。 そのため、ここで 17.05.0-ce と指定すると、そのバージョンのdockerをinstalできる。 出来るのだが、そのバージョンは使えない。 なぜかと言うと docker は client 側と docker daemon側で2つのversionがあり、 setup_remote_docker では、docker daemon側のバージョンは 17.03.0-ce で固定されている。 daemon側が低いとそちらに合わせられるため、multi-stage buildは行うことができない。

では、どうするのが良いか。 machine実行で、自分で docker を install する。

  deploy:
    working_directory: ~/app
    machine: true
    steps:
      - checkout
      - run:
          name: update docker
          command: |
            docker version
            sudo service docker stop
            curl -fsSL https://get.docker.com/ | sudo sh
            docker version

上記では、(おそらく)最新のdockerを自分で入れている。

Client:
 Version:      17.05.0-ce
 API version:  1.29
 Go version:   go1.7.5
 Git commit:   89658be
 Built:        Thu May  4 22:06:06 2017
 OS/Arch:      linux/amd64

Server:
 Version:      17.05.0-ce
 API version:  1.29 (minimum version 1.12)
 Go version:   go1.7.5
 Git commit:   89658be
 Built:        Thu May  4 22:06:06 2017
 OS/Arch:      linux/amd64
 Experimental: false

build時に、debugでversionを出すとこんな感じに 17.05.0-ce が入っていることがわかる。

あとは普通に、docker imageをbuildして push したりすればよい。 雑な例を出すとこんな感じ。

      - deploy:
          name: run build & deploy
          command: |
            $(aws ecr get-login --region ap-northeast-1)
            $REPO={{ECRのリポジトリとか}}
            $PUSH_TAG=latest
            docker build -t hoge:latest .
            docker tag hoge:latest $REPO:$PUSH_TAG
            docker push $REPO

最後に

Railsでmulti-stage buildに使っているDockerfileを載せておく。 あんまり詰めれていないので、もっと良く出来るとは思う。 考えとしては、gemをbuildするために必要なものを、最初のイメージで入れて 実行に必要なものだけど後半のイメージにいれている。

FROM ruby:2.4.1-alpine as builder

RUN apk --update add --virtual build-dependencies build-base curl-dev linux-headers
RUN apk --update add mariadb-dev
RUN echo 'gem: --no-document' > /etc/gemrc

WORKDIR /app
ADD . /app
RUN bundle install --jobs=4
RUN apk del build-dependencies


FROM ruby:2.4.1-alpine

ENV LANG ja_JP.UTF-8
COPY --from=builder /usr/local/bundle /usr/local/bundle

RUN apk --update add tzdata imagemagick
RUN apk --update add mariadb-dev && rm /usr/lib/libmysqld*
RUN apk del openssl-dev mariadb-client-libs mariadb-common

ADD . /app
RUN chown -R nobody:nogroup /app
USER nobody

WORKDIR /app
EXPOSE 9292
CMD [ "bundle", "exec", "unicorn", "-c", "config/unicorn.rb" ]

webpack でそれっぽい構成を作る

それっぽいとは

webpackは、bundleツールである。 bundleとは、まとめること。 なので、 webpackの役目は、javascript等の依存を理解しその依存関係が崩れないように1つのファイルにまとめること。 である。

しかしそれで嬉しいのは主にSPAの時であり、普通のwebサイトであれば全ページで使う javascriptcss をすべて1つにまとめられるのは困ったりする。

今回目指しているのは普通のwebサイトを作る際に必要なことを満たすこと。

具体的には以下になる。

  • bundle単位を制御できること。
  • 共通の依存は各bundleに含めないこと。
  • CSSは別ファイルにbundleすること。
  • CSS内の画像ファイル参照は画像ファイルとして別にすること。
  • CSSはSASSで書けること。
  • ソースマップが作成されてデバッグできること。
  • JSは、es6で書けること。

結果ファイル

const path = require('path');
var webpack = require('webpack')
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
  context: path.resolve(__dirname, 'assets/js/'),
  entry: {
    home: './home.js',
    second: './second.js'
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'js/[name].js'
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ['env']
          }
        }
      },
      {
        test: /\.sass$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: ['css-loader', 'sass-loader'],
          publicPath: '../'
        })
      },
      {
        test: /\.(jpg|jpeg|png)$/,
        use: {
          loader: 'file-loader?name=images/[hash].[ext]'
        }
      }
    ]
  },
  devtool: 'cheap-module-eval-source-map',
  plugins: [
    new ExtractTextPlugin({
      filename: 'css/[name].css',
      allChunks: true
    }),
    new CopyWebpackPlugin([{
      from: '../static_images',
      to: 'images'
    }]),
    new webpack.optimize.CommonsChunkPlugin({
      name: 'commons',
      filename: 'js/commons-[hash].js',
    })
  ]
};

ディレクトリ構成

assets
├── css
│   ├── app.sass
│   ├── hoge.sass
│   └── vendor
│       └── huga.sass
├── images
│   └── ganbaruzoi.jpg
├── js
│   ├── home.js
│   ├── second.js
│   └── vendor.js
└── static_images
    └── moga.jpg

public
├── css
│   ├── home.css
│   └── second.css
├── images
│   ├── 66ad830e621b1b231887708a3c8b4b52.jpg
│   └── moga.jpg
└── js
    ├── commons-ad736945b12c5cd3ca17.js
    ├── home.js
    └── second.js

解説

bundle単位を制御できること。

ちと、面倒なのだが、entryで分けたい単位ごとに記述し、 outputで filename: '[name].js と記述することで出来る。

  entry: {
    home: './home.js',
    second: './second.js'
  },
  output: {
    path: path.resolve(__dirname, 'public'),
    filename: 'js/[name].js'
  },

共通の依存は各bundleに含めないこと。

webpackにbuildinされているpluginのCommonsChunkPluginを使うと出来る。

    new webpack.optimize.CommonsChunkPlugin({
      name: 'commons',
      filename: 'js/commons-[hash].js',
    })

home.js, second.js両方で以下のように記述シてある場合。 これら共通の依存は別ファイルにbundleしてくれる。

import './vendor.js'

CSSは別ファイルにbundleすること。

      {
        test: /\.sass$/,
        use: ExtractTextPlugin.extract({
          fallback: "style-loader",
          use: ['css-loader', 'sass-loader'],
          publicPath: '../'
        })
      },
      {
        test: /\.(jpg|jpeg|png)$/,
        use: {
          loader: 'file-loader?name=images/[hash].[ext]'
        }
      }
~~~~

    new ExtractTextPlugin({
      filename: 'css/[name].css',
      allChunks: true
    }),

以下2つも一緒に解説する。

  • CSS内の画像ファイル参照は画像ファイルとして別にすること。
  • CSSはSASSで書けること。

webpackが理解するのは、javascriptのみである。 その為、webpack経由でcssを扱うためにはjs内でcssをimportする必要がある。

import '../css/app.sass'

moduleのrulesでは、entryポイントから読み込まれている依存をルールにマッチングしたものを指定した方法で解決する。

test: /\.sass$/, では、jsから読み込まれているファイルの拡張子が.sassだった場合に、 ExtractTextPluginを使って、sass-loader,css-loaderの順番に適用する。 sass-loaderでcssの構文に分解し、css-loaderでcssとして処理する。 ExtractTextPluginでjsにbundleされた後にbundleファイルから引き抜く。

引き抜いたあとに保存先は、 filename: 'css/[name].css', で指定している。

css内で background-image: url('../images/ganbaruzoi.jpg') のように、画像参照を行っている場合は、file-loaderで処理を行う。 url-loaderと併用してもいい。url-loaderは画像パスを解決し、画像をbase64エンコードしてCSSファイルを書き換える。 file-loaderはパスを解決して、書き換える。

注意点として、ExtractTextPluginはbundle後のファイルからCSS部分を引き抜くため、 デフォルトではその時点でのパスで file-loaderが適用されている。 その後、 public/css ディレクトリに引き抜いたCSSをファイルとして配置するため、パスがずれる。 以下のようにpublicPathを適用することでこれは解決するが、必ずしも良い解決策とは言えない。

publicPath: '../'

develop環境であればこれで問題ないが、productionではassetsをCDNに配置したりする。 その際は、相対パスではなく http://cdn.com/images/ みたいなURLになってほしい。

その時はproduction用の設定でCDNのURLをpublicPathに記述する必要がある。

ソースマップが作成されてデバッグできること。

devtool: 'cheap-module-eval-source-map',

これは develop 用なので、 production用はまた別になる。 詳しくは公式のdocumentに書いてある。

JSは、es6で書けること。

      {
        test: /\.(js|jsx)$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ['env']
          }
        }
      },

excludeが必要なのかどうかは、まだよくわかっていない。 これは、testを満たしたファイルのうち、node_modules, bower_componets配下のものは対象としない。という意味になる。

まとめ

今回の設定は develop 向けになる。 そのため、production時には [hash] だったり、 [id] のプレースホルダーを使用して、ブラウザのキャッシュ対策を施す必要がある。

一般的には、各ファイルに [name]-[hash].js みたいに hash を付ける。

普段、一切関わりのない frontend 周りだった為、この構成を作るには結構時間がかかった。 といっても、4,5時間?

productionを見据えてたり、css,image周り含めてまでしっかりした構成が取られている情報は少なかった印象だった。 公式のドキュメントを全部読んで、あたりを付けてからぐぐると情報は出てきた。

また、しっかりとした開発を進めているわけでないのでこれでもまた問題はでてくると思う。

私がプログラミングをする理由

あすたぴ です

ふと思い立ったので整理してみます。

別にプログラミングが大好きなわけではない

好きか?って言われたらまぁ好き寄りって感じです。 プログラミングは手段です。 学ぶ理由は目的を遂行するためによりよい方法、より楽な方法を使いたいからです。 目的を遂行するに当たって、よりかっこいい方法をとりたい。みたいな気持ちはあります。

みんなに使ってもらえるようなライブラリとかフレームワークを作りたい。といった気持ちはないです。 世界の天才たちに立ち向かえると思うような頭は持ち合わせていません。

じゃあなんでプログラミングしてるの?

テクノロジーの力は偉大です。 日常生活のなかで周りにはプログラムが多くなってきました。 そういったテクノロジー、技術を理解したいし、自分でも作りたいと思います。

ある程度、出来るようになったら別によくね?

そういった考えもあると思います。

ある程度プログラミングが出来るようになったら、効率の良い人は飯を食うのに困らないかもしれません。 なぜ継続するかといいますと、わからない(理解ができない)ことが多いからです。 例えば、Dockerを使える人は多いと思いますが、DockerがLinux上でどう動いているか。 というところまでわかる人はそれほど多くないのではないかと思います。

そうなると、Dockerを使える。だけであって、 (Dockerと似たようなコンテナ技術とかの)Dockerではないものを使おうとした場合はそれをまた学ぶ必要があります。 私はそれの効率をあげたいと思っています。

技術が使えるようになるために学ぶのではなく、技術を学ぶスピードを上げるために学びます。

最終的にはどうなりたいの?

最終的には、 世界にインパクトを与えたい です。

ポケモンGoはすごかった。 テクノロジーで世界の景色を変えました。(街中の人がポケモンGoをやっている景色という意味)

私はサービスを考える力がある方だと思っていません。 すごいサービスを考えれる人がいた場合に、私はそのサービスを実現できる立場にいたいのです。

そのために必要だと考えていることが以下です。

  • ふつうのシステムだったらサクっと作れる技術力、経験
  • 新しいことを取り込むスピードがはやいこと

ちなみに現職でいまの立場は、この力を付けるのに非常にいい具合です。