頑張るときはいつも今

自称エンジニアのゴリラです。日々精進。

【学習記録】CircleCIに入門してみた

運動量よりも食べる量が異常に増えてきて、

体が重いです。

勉強も含めて個人でアプリを作りたいなと思って、

空き時間で取り掛かっています。

環境構築周りでCircleCIについて勉強したのでその際のメモです。

記事に書く内容

  • CircleCIの概要

  • CircleCIを使ってみる

  • CircleCIの設定ファイル(config.yml)について

CircleCIの概要

CircleCIって何?

CircleCIの公式&gclid=Cj0KCQiAyp7yBRCwARIsABfQsnTANY0-freoXkHqV5p4DuPaQ1JTlM7LM3DvcJaiyQ2mJieOsDp5bCIaAhcYEALw_wcB)

流行り(というかデファクトになりつつある)のSaaS型のCI/CDサービスです。

GitHubだとかBitbucketと連携して、テストだとかビルドができるようになります。

基本的なLinuxのコマンドは実行できるので、aws cliを利用してリソースの変更だとかもできるみたいです。

ところでCI/CDって何?

CI/CDを日本語に訳すと、*継続的インテグレーションと継続的デリバリー`という意味になります。

継続的インテグレーション
  - ビルドやテストを短期間で繰り返して開発効率をあげること
     - 定期的に実行(デイリービルド)
     - 短期間で繰り返しテスト
継続的デリバリー
  - CIの仕組みをインフラ構築まで拡張したような考え方
     - テストした成果物をそのまま本番環境に自動デプロイ
     - テスト環境と同じ構成で本番環境を動作させることができる

CircleCIでできることって何?

  • ビルド

    • ソースコードから実行可能なアプリケーションを構築できる
    • Dockerイメージのpullとか、依存パッケージのインストール、コンパイル
  • テスト

    • lintツールによるコードスタイルチェック
    • ○○specでのテスト実行
  • デプロイ

    • ビルドしてテストが通ったものを本番/stg環境にデプロイすることができるようになります

パターンによりけりですが、CircleCIと連携させて下図のような開発体制を構築することができます。

開発者がCommitしたら自動でテストを走らせて、Slackに通知させるだとか

masterとかdevelopであれば、本番/stg環境に自動デプロイするといったことができるようになります。

f:id:wa_football_1120:20200216132734p:plain

なんでCircleCI?

CircleCIはSaaS型のサービスです。

Jenkinsのように自前でサーバを組んだりする必要がなく、環境構築/運用コストが非常に低いことが魅力的です。

ビルド~デプロイの設定はymlファイルで設定するので、職人が作って中身がブラックボックスになりにくいことも魅力的です。

ただ、デメリットもあり、インフラ ~ ミドルまでのあたりはCircleCIの持ち物なので、

何か障害が起きた時だとかは自分たち原因究明するのはまず不可能です。

CircleCIを使ってみる

CircleCIはGithubもしくはBitbucketと連携させることが前提としてあります。

そのため、利用するにあたってはどちらかのアカウント登録が必要になります。

料金体系

CircleCI料金プラン

無料枠有料枠があります。

無料枠

個人開発なので無料枠を利用します。

  • 同時実行ジョブ(同時に実行できるCI/CD活動)
    • 1
  • ビルド時間の制限
    • 1000min/month

アカウント登録 ~ GitHub連携まで

CircleCIの公式&gclid=Cj0KCQiAyp7yBRCwARIsABfQsnTANY0-freoXkHqV5p4DuPaQ1JTlM7LM3DvcJaiyQ2mJieOsDp5bCIaAhcYEALw_wcB)

でアカウント登録 ~ GitHub連携までやっていくと、リポジトリcircleci-project-setupというリモートブランチが作成されます。 このブランチの中にはCircleCIの設定ファイル.circleci/config.ymlが含まれています。

git branch -r                                                                                                 [circleci-project-setup]
  origin/circleci-project-setup
  origin/master

git checkout -b circleci-project-setup

git pull origin circleci-project-setup                                                                        [circleci-project-setup]
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (4/4), done.
From github.com:*****/trainning-rails-vue
 * branch            circleci-project-setup -> FETCH_HEAD
 * [new branch]      circleci-project-setup -> origin/circleci-project-setup
Updating 2b6de12..6dfc64b
Fast-forward
 .circleci/config.yml | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
 create mode 100644 .circleci/config.yml

CircleCIの設定ファイル(config.yml)について

先ほどデフォルトで作成されたconfig.ymlの中身は下のようになっていました。

version: 2.1
orbs:
  ruby: circleci/ruby@0.1.2 

jobs:
  build:
    docker:
      - image: circleci/ruby:2.6.3-stretch-node
    executor: default
    steps:
      - checkout
      - run:
          name: Which bundler?
          command: bundle -v
      - ruby/bundle-install

orbs

Version2.1から追加された機能です。

CircleCIのcommandsやジョブ(設定)をパッケージとして公開し、再利用するための仕組みです。

今回設定されているruby@0.1.2Rubyのインストールだとかテストのコマンドがすでに定義されているパッケージになります。

参考

jobs

jobsのフィールドは一般的に下記のような構造になう量です。

jobs
  -job名(1つの場合はbuildという名前にする)
     - docker
     - executer 
     - steps

job名

1つ以上のjobを設定します。

jobが1つの場合はbuiltという名前にしなければいけません

docker

ほとんどのwebアプリの場合は、コンテナイメージを利用すると思いますが、

CircleCI上で動かすdocker imageの設定です。

  • image
    • ベースとなるdocker imageを指定する。複数指定もOK
    • environmentやcommandを使ってコンテナが動く際の動作を変えていく
  • auth(optional)
    • Docker hubを利用する場合の認証情報
    • username
    • password
  • aws_auth (optional)
  • steps
    • CI環境上で動作させるコマンドや実行結果の保存、キャッシュ操作などを設定

実際に設定したconfig設定

今回アプリケーションはデプロイまではせずに、テストを実行したいと思います。

(実際のリポジトリにはnuxtのプロジェクトも含まれていますが一旦はbackendのみを)

  • rubocopによるコードのLintチェック
  • Rspecによるテスト

上の基本構文に含まれていない、ブロックがありますが下のような設定です。

version: 2.1
orbs:
  ruby: circleci/ruby@0.1.2 
commands:
  bundleinstall:
    description: "bundle install --path vendor/bundle"
    steps:
      - run: cd back && bundle install --jobs=4 --retry=3 --path vendor/bundle
executors:
  backend-executor:
    docker:
      - image: circleci/ruby:2.6.3-stretch-node
        environment:
          RAILS_ENV: test
          RDS_HOST: 127.0.0.1
          TZ: Asia/Tokyo
          LANG: C.UTF-8
      - image: circleci/mysql
        command:
          mysqld --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: trainning_test
          MYSQL_ROOT_HOST: '%'
          TZ: Asia/Tokyo
jobs:
  rubocop:
    working_directory: ~/app
    executor: backend-executor
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "./back/Gemfile.lock" }}
            - v1-dependencies-
      - save_cache:
          paths:
            - ./back/vendor/bundle
          key: v1-dependencies-{{ checksum "./back/Gemfile.lock" }}
      - bundleinstall
      - run:
          name: Rubocop
          command: cd back && bundle exec rubocop
  rspec:
    working_directory: ~/app
    executor: backend-executor
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "./back/Gemfile.lock" }}
            - v1-dependencies-
      - save_cache:
          paths:
            - ./back/vendor/bundle
          key: v1-dependencies-{{ checksum "./back/Gemfile.lock" }}
      - bundleinstall
      - run: cd back && bundle exec rails db:schema:load
      - run:
          name: Rspec
          command: cd back && bundle exec rspec
workflows:
  version: 2
  backend_build_work_flow:
    jobs:
      - rubocop
      - rspec:
          requires:
            - rubocop

commandsブロック

複数のジョブで利用するコマンドを定義することができます。

今回は、front(nuxt)とbact(rails)を1つのリポジトリで管理する想定でしたので、

cd back &&で事前にRailsプロジェクトが入っているディレクトリに移動しています。

commands:
  bundleinstall:
    description: "bundle install --path vendor/bundle"
    steps:
      - run: cd back && bundle install --jobs=4 --retry=3 --path vendor/bundle
# parameterブロックを追加して引数を受け取ることもできる
commands:
  bundleinstall:
    description: "bundle install --path vendor/bundle"
    parameters:
         path:
           type: string
          default: "vendor/bundle"
    steps:
      - run: bundle install --jobs=4 --retry=3 --path << parameters.path >>

executorsブロック

複数のジョブで利用する実行環境を定義することができます。 今回はrubyのイメージとmysqlのイメージを利用しています。

localhostを複数のコンテナで共有するようです。 注意点としてはDBコンテナのホストを指定する際にはlocalhostではなく、127.0.0.1を指定しないとうまく動いてくれません。

CircleCIではDockerのNetwork Namespaceという機能を使ってlocalhostを複数のコンテナで共有しています。これを使うと複数のコンテナで使うネットワークを分けることができます。もともとはLinux Kernelが提供している機能でnetnsと呼ばれ、Dockerはそれをラップしている形となります。

executors:
  backend-executor:
    docker:
      - image: circleci/ruby:2.6.3-stretch-node
        environment:
          RAILS_ENV: test
          RDS_HOST: 127.0.0.1
          TZ: Asia/Tokyo
          LANG: C.UTF-8
      - image: circleci/mysql
        command:
          mysqld --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
          MYSQL_ROOT_PASSWORD: password
          MYSQL_DATABASE: trainning_test
          MYSQL_ROOT_HOST: '%'
          TZ: Asia/Tokyo

jobsブロック

rubocop

  • working_directoryでappというディレクトリを作業ディレクトリにしていきます。
  • restore_cacheで以前に保存しておいたbundleインストールしたライブラリを復元します。
    • 動作時間の短縮につながります。
  • bundleinstallは上で定義した再利用可能なコマンドです
  • save_cacheでvendor/bundleに入っているライブラリ群を保存します。
rubocop:
    working_directory: ~/app
    executor: backend-executor
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "./back/Gemfile.lock" }}
            - v1-dependencies-
      - save_cache:
          paths:
            - ./back/vendor/bundle
          key: v1-dependencies-{{ checksum "./back/Gemfile.lock" }}
      - bundleinstall
      - run:
          name: Rubocop
          command: cd back && bundle exec rubocop

rspec

rspecも同じような形です。

bundle exec rails db:schema:loadとすることで、schema.rbと同じ内容のDBを作ります。

マイグレーション忘れなどでエラーが発生しないための工夫です。

rspec:
    working_directory: ~/app
    executor: backend-executor
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "./back/Gemfile.lock" }}
            - v1-dependencies-
      - save_cache:
          paths:
            - ./back/vendor/bundle
          key: v1-dependencies-{{ checksum "./back/Gemfile.lock" }}
      - bundleinstall
      - run: cd back && bundle exec rails db:schema:load
      - run:
          name: Rspec
          command: cd back && bundle exec rspec

workflow

公式

workflowはバージョン2から追加された機能なのですが、

名前の通りjobのワークフローを作ります。

jobをいつ、どの順番で実行するかを定義することができます。

今回はrubocopの後にrspecを実行して欲しかったのでrequiresを使って順番関係を定義しています。

workflows:
  version: 2
  backend_build_work_flow:
    jobs:
      - rubocop
      - rspec:
          requires:
            - rubocop

ローカルでcofigファイルの検証

この時点でgithubにpushすればCircleCIが自動的に変更を検知して、

configに書かれた内容が実行されるためローカルじゃなくてもconfigファイルの検証はできます。

が、なんとなくダサいので、ローカルで検証してからpushしたいと思います。

CircleCI CLI

ローカル環境からCircleCIに対してビルドしたり、

config.ymlの文法が正しいか検証ができるコマンドです。

リファレンスに沿ってダウンロードしてください。

#クイックインストール
curl -fLSs https://circle.ci/cli | bash

configファイルの検証

circleci config validateコマンドで検証ができます。

デフォルトで.circleci/config.ymlを検証対象とします。

circleci config validate                                                                                       [circleci-project-setup]
Config file at .circleci/config.yml is valid.

ローカルでjobを実行

circleci local executeでローカル環境で作成したjobを実行ができるのですが、

2.1についてはまだ対応していないようで下のようなエラーが発生します。

circleci local execute                                                                                         [circleci-project-setup]
Error:
You attempted to run a local build with version '2.1' of configuration.
Local builds do not support that version at this time.
You can use 'circleci config process' to pre-process your config into a version that local builds can run (see 'circleci help config process' for more information)

CLI側で2.1から2.0形式に変換してくれるコマンドが用意されています。

2.1から追加されたexecutorsorbsを展開して出力してくれます。

circleci config process .circleci/config.yml > process.yml
circleci local execute -c process.yml --job [実行したいjob名]

githubへのpush

ここまで来て、githubへプッシュすると、

CircleCIが自動的に動作していると思います。

画像では、rubocopに成功したがrspecのjobが失敗していることがわかります。 (まだ、アプリ自体がまっさらな状態なので、rails db:schema:loadでこけたようでした)

f:id:wa_football_1120:20200217054408p:plain

まとめ

CircleCIについて、少し学習をしてみました。

  • CircleCIは、SaaS型のCI/CDサービス

    • CI(継続的インテグレーション)は、ビルドやテストを短期間で繰り返して品質を向上させていく取り組み
    • CD(継続的デリバリー)は、インフラ環境への自動デプロイを含めてCIを実行していく取り組み
  • CircleCIのconfig.ymlにコンテナイメージや実行するジョブを定義していく

二番煎じどころではない今更勉強したので、かなり情報が溢れています。

ただ2.1については、DRYに書くための新機能(executors)があったりするので、

公式のリファレンスを確認した方がいいですね。

色々と進んできたら、masterブランチだけデプロイをするjobを含めて、

もう一回記事を書きたいと思います。

【読書記録】WebPackに入門してみた

先週記事をサボってしまいました。 今週は書きます。(とはいえ読書記録なので内容は薄いですが)

今回読んだ本

「Webpack実践入門」という本になります。 KDP(Kindle ダイレクト・パブリッシング)に出版されているようで、 ワンコインで購入できる技術書になります。

なぜ読んだか

今扱っているシステムでは、RailsMVCで構築されており一部jQueryで非同期処理をするような構成です。

若干時代遅れな点もあり、Vue.jsも導入していきたいなと思っています。

Rails6ではwebpackerが標準ですが、巷では - カスタマイズ性が低い - webpacker側でサポートしていないパッケージと相性が悪い ということもあり、導入推進のためwebpackを勉強し始めました。

読んで何が得れたか

  • webpackに関する基本的な知識、利用用途、知識を身に着ける
    • 用語と仕組みの説明→ハンズオンという形式なので、理論と実装を紐づけることができました
  • 有名どころなローダー(babel, sass, url)やプラグインを使ってみることができる
    • webpack単体では恩恵はあまりなくて、babal-loaderやsass-loaderといったものと組み合わせることで、フロントエンド開発を効率化することができます
  • 本番環境と開発環境でのwebpack.config.jsの使い分けの方法を理解できる
    • 本番環境と開発環境では、利用するローダ・プラグインや設定を分けたいといったことがあります。
    • ただ共通している部分もあるといった場合に、ハンズオンを通して本番環境と開発環境でwebpack.config.jsの使い分けを身に着けることができます。

そもそもwebpackって何?

  • フロントエンド開発用のモジュールバンドラ
    • モジュールバンドラを介して、複数のモジュール(JSファイルやcssファイル)をまとめてくれる

雑に表すと下図のようなイメージ

フレームワークなども含めて、複数(or 単体)にまとめて出力してくれます。

f:id:wa_football_1120:20200215121701p:plain

webpackを使うと何が嬉しいの?

メリット1 機能後にファイルをモジュール化できる

フロントエンド開発では、機能ごとにファイルを分割して開発することが主流です。 なんで機能ごとにファイルを分割するかと言うと、

  • 可読性の向上
    • どこで使われているかわからないコードをみるとすごい疲れますよね。しかも安易に消せないと言う。。
    • 機能ごとに別れていれば、システムのどの機能を実装していると言う全体把握にも役立ちます。
  • 開発作業の分担とテストがしやすくなる(=開発効率の向上)
    • 機能ごとにファイルが別れていれば、コンフリやデグレを引き起こす可能性を大幅に削減できます
  • 名前空間を生成できる
    • グローバル変数だとか使われているとまず、競合やクリティカルなバグが起きやすくなります
    • 名前空間を作ることで、その空間のみに影響範囲を止めることができます。
  • モジュールの保守性を高められる
    • リリースして、ある程度の期間が立って、「ここもう少しこうしたいな」と要望を受けたとしましょう
    • ここで、1つのファイルから該当のコードを探すの比べたら、機能ごとに別れていた方が機能拡張も修正もしやすいですよね

メリット2 外部モジュールも利用できる

フロントエンド周りでは、vuereactといったモダンなライブラリやフレームワークを利用する機会が大幅に多くなると思います。

webpackは外部モジュールを含めてバンドルすることができます。

メリット3 リクエスト数を減らせる

HTTP2.0が普及してきているので、メリットか分かりませんが、

複数のファイルがまとめられるため、リクエスト数が減ります。

HTTP2.0と1.1の違いはこちらが分かりやすかった

メリット4 依存関係を解決したファイルを出力できる

webpackが依存関係を自動的に解決してファイルを主力するため、依存関係による不具合を減らすことができます。 小規模なシステムでは、依存関係を追っていくことができそうですが、 中+大規模なシステムになってくるとまず依存関係を目グレップで追っていくのは無理ですし効率がかなり悪いです。

f:id:wa_football_1120:20200215124345j:plain

webpackを試してみる

実行環境はこんな感じです。

srcディレクトリ配下のapp.jsがモジュールadd.jstax.jsを利用します(依存関係) これらをバンドルした結果をpublic/js配下のbundle.jsに出力します。

root@e9bc5cdc81ea:/my_webpack# yarn -v
1.21.1

tree                                                                                                                                ?[master]
.
├── Dockerfile
├── docker-compose.yaml
└── getting-started-webpack
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── public
    │   ├── index.html
    │   └── js
    │       └── bundle.js
    └── src
        ├── js
        │   └── app.js
        └── modules
            ├── add.js
            └── tax.js

package.json作成 & 必要なモジュール群をインストール

まずはpackage.jsonを作成して、webpackをインストールします。

root@e729ef118310:/my_webpack# yarn init -y
yarn init v1.21.1
warning The yes flag has been set. This will automatically answer yes to all questions, which may have security implications.
warning package.json: No license field
success Saved package.json
Done in 0.11s.

root@e729ef118310:/my_webpack# yarn add --dev webpack webpack-cli

root@e729ef118310:/my_webpack# yarn add jquery

package.jsonにモジュールが追加されていることを確認します。

  • dependencies
    • 出力されるファイルにバンドルされるモジュール
    • 本番環境で必要なモジュール群
    • yarn add {モジュール名}で入れます
  • devDependencies
    • 開発時に必要なモジュール
    • yarn add -dev {モジュール名}で入れます
{
  "name": "my_webpack",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "webpack": "^4.41.6",
    "webpack-cli": "^3.3.11"
  },
  "dependencies": {
    "jquery": "^3.4.1"
  }
}

package.jsonスクリプトを書く

package.jsonscriptsフィールドにエイリアスと対応するコマンドを記述することで

yarn run {エイリアス}で実行することができます。

  "scripts": {
      // yarn run devで実行ができるようになる
      "dev": "webpack --mode development --watch --hide-modules --config webpack.config.js"
  }

webpack.config.jsを書く

webpack.config.jsとはwebpackがエントリーポイント(どこを起点に依存関係を解決していくか)や出力先(バンドルしたファイルの保存先)を設定することができます

const path = require('path');


module.exports = {
    mode: 'development',
    entry: './src/js/app.js',
    output: {
        filname: 'bundle.js',
        path: path.resolve(__dirname, 'public/js')
    }
}
  • mode

    • webpackの動作モードで、development,production,noneから選択します
    • developmentの場合は、エラー表示やデバッグしやすいファイルを出力するため開発時にはこちらを選択します
    • productionの場合は、ファイルの圧縮やモジュールの最適化などがされるので、本番環境に適用する場合にはこちらを選択します
  • entry

    • 上でwebpackは依存関係を解析して、自動的に解決すると書きましたが、その依存関係の解析を開始する始点になります
  • output

    • バンドルの設定です。
    • filename
      • バンドルした結果のファイル名
    • path
      • バンドルしたファイルの保存先

モジュール作成

モジュールadd.jstax.jsを作成します。

add.jsはそのままの通り、2つの引数を受け取って合計を返すモジュールです。

tax.jsは値段と税率を引数で受け取り、税込み価格を返すモジュールです。

exportを利用して、外部のファイルが利用できるようにします。

//add.js
export default function add(n1, n2) {
    return n1 + n2;
}
//tax.js
export default function(price, salesTax) {
    return Math.round(price * salesTax);
}

エントリーポイントの作成

webpackが解析を始めるエントリーポイントを作成していきます。 先ほど作成したモジュールとjqueryをimportして、bodyにベタ書きするプログラムです。

import $ from 'jquery';
import add from './modules/add';
import tax from './modules/tax'

const price1          = 100;
const price2          = 500;
const totalPrice      = add(price1, price2);

const salesTax        = 1.08;
const priceIncludeTax = tax(totalPrice, salesTax);

$('boxy').text(priceIncludeTax);

ビルドしてみる

先ほど記述した scriptsでビルドするとpublic/js/bundle.jsが作成されていると思います。

root@e729ef118310:/my_webpack# yarn run dev
yarn run v1.21.1
warning package.json: No license field
$ webpack --mode development --watch --hide-modules --config webpack.config.js

webpack is watching the files…

Hash: 9e92343bbee09bc21d50
Version: webpack 4.41.6
Time: 559ms
Built at: 02/15/2020 6:11:34 AM
    Asset     Size  Chunks             Chunk Names
bundle.js  316 KiB    main  [emitted]  main
Entrypoint main = bundle.js

ビルドしたファイルを読み込む

public/index.htmlからはbundle.jsを読み込めば完了です。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>Getting Started </title>
    </head>
    <body>
        <script src="js/bundle.js"></script>
    </body>
</html>

ローダについて

フロントエンドはjavascriptだけではなく、css(scss)や画像ファイルも必要です。

js以外のファイルを同じようにバンドルできる形に変換するプログラムがローダになります。

TypeScriptVueコンポーネントもローダを介してバンドルすることが可能です。

冒頭のWebpackのイメージをもう少し細かく分離すると下のような形になります。 f:id:wa_football_1120:20200215152033j:plain

代表的なローダーの紹介とwebpack.config.jsへの記載方法をメモしていきます。

babel-loader

babel-loeaderはES6以降のコードをES5のコードに変換するローダーです。 IEなど古いブラウザでも動くようなjs構文に変換してくれます。

babel-loaderを追加

babale-loaderを使う場合には@babel/core@babel/preset-envが必須なので一緒に追加します。 - @babel/core : Babel本体 - @babel/preset-env: ECMAを変換するために使う

root@9e670eed7c44:/my_webpack# yarn add --dev babel @babel/core @babel/preset-env

webpack.config.jsを追記

ローダの設定はmoduleブロックに記述していくことになります。 - test - どの形式のファイルを対象とするか - include - 適用対象のフォルダ - use - 使うローダを設定

module: {
        rules: [
            {
                test: /\.js&/, //ローダの対象ファイルの形式?
                include: path.resolve(__dirname, 'src/js'), // ローダ対象のフォルダ(excludeで対象外のフォルダを設定できる)
                use: [
                    {
                        loader: 'babel-loader', 
                        options: {
                            presets: [
                                [
                                    '@babel/preset-env'
                                ]
                            ]
                        }
                    }
                ]
            }
        ]
    }

sass-loader, css-loader, style-loader

scssファイルをバンドルするために3つのローダを使います。

  • sass-loader
  • css-loader
    • cssをモジュールに変換する
  • style-loader
    • cssのスタイルが記述されたstyleタグをHTMLに追加する

モジュールを追加

sass-loaderを使うためにはnode-sassも同時にいれる必要があります。

root@9e670eed7c44:/my_webpack# yarn add --dev sass-loader node-sass css-loader style-loader

webpack.config.jsに追記

rulesブロックに追記します。

今回は複数のローダを利用するのですが、ローダは配列に入れた逆順で実行されます。

(今回の場合は、sass → css → styleの順番で実行する必要があります)

            {
                test: /\.scss$/,
                include: path.resolve(__dirname, 'src/scss'),

                use:[
                    //下から順番に実行される(scss-loader -> css-loader -> style-loader)
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            }

プラグイン

webpack自体を拡張させるためのプログラムです。

バンドルする際にプラグインで追加した処理を行うことができます。

ProvidePlugin

指定したモジュールを全てのファイルから変数として利用できるようにするプラグインです。

jqueryなどほぼ全てのファイルで利用されるモジュールを毎回importする必要がなくなります。

このプラグインについては、webpackをインストールした時点ですでに利用することができます。

webpack.config.js

プラグインを利用するために、webpack自体を読み込んおきます。

const webpack = require('webpack')

プラグインの設定はpluginsブロックに記述していきます。 ProvidePlugnを利用してjquery$で利用するようにしています。

plugins:[
        new webpack.ProvidePlugin({
            $: 'jquery' // juqueryを$として使うことができる
        })
    ]

Watchモード

watchモードは、ファイルの変更差分を監視して、

変更があった際に自動的にビルドを再実行してくれる機能です。

自分で都度、ビルドする必要がなくなります。

webpack.config.jsに追記することもできるのですが、

--watchをつけてコマンドを実行すればwatchモードになります。

なので、package.jsonのscriptで使い分けた方が利便性は高いです。

  "scripts": {
      "dev": "webpack --watch",
      "build": "webpack"
  }

webpack-dev-server

node.jsで作られた開発用サーバです。

jsファイルやリソースに変更があると即座に反映してくれます。

webpack-dev-serverの注意点としては、ファイルを出力しないことです。

ビルドした結果はオンメモリに保持するため、実際にビルドする際にはwebpackコマンドを実行する必要があります。

インストール

yarnで追加できます。

開発で使うものなのでローカルインストールしておきます。

root@9e670eed7c44:/my_webpack# yarn add --dev webpack-dev-server
yarn add v1.21.1

webpackに設定を追加

devServerプロパティにwebpack-dev-serverの設定を追加していきます。

    devServer: {
        open: false, //サーバ起動時に自動的にブラウザを起動するか?(コンテナ上で試していたのでfalse) 
        host: '0.0.0.0', //コンテナで動作させているため,
        disableHostCheck: true, // 
        port: 3000, // サーバが待ち構えるポート番号
        openPage: 'index.html', // 
        contentBase: path.join(__dirname, 'public'), // コンテンツのルートディレクトリ
        watchContentBase: true,
    }

開発環境と本番環境の切り分け

開発環境ではwatchモードを有効にしたり、 webpack-dev-serverを動かしたいとい要望が出てくると思います。

もちろんconfigをそれぞれ作ればいいのですが、重複部分なども同じように 書いていくのは無駄が多くなります。

そこでベース(共通部分)+本番のみの部分+開発のみの部分といった形で切り分けをしていきます。

ディレクトリ構造

tree -L 1
.
├── node_modules
├── package.json
├── public
├── src
├── webpack.base.config.js
├── webpack.dev.config.js
├── webpack.prod.config.js
├── yarn-error.log
└── yarn.lock

必要なモジュールをインストール

webpackのconfigをマージするwebpack-merge

本番環境で利用するterser-webpack-pluginをインストールします。

root@9e670eed7c44:/my_webpack# yarn add --dev webpack-dev-server
root@05d680e73065:/my_webpack# yarn add --dev terser-webpack-plugin

ベース部分の作成

webpack.base.config.jsにベース部分を記載していきます。

エントリーポイントの設定や出力先、ローダーは同じように設定します。

const path = require('path')

module.exports = {
    entry: '.src/js/app.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'public/js')
    },
    module: {
        rules: [
            {
                test: /\.js&/, //ローダの対象ファイルの形式?
                include: path.resolve(__dirname, 'src/js'), // ローダ対象のフォルダ(excludeで対象外のフォルダを設定できる)
                use: [
                    {
                        loader: 'babel-loader', 
                        options: {
                            presets: [
                                [
                                    '@babel/preset-env'
                                ]
                            ]
                        }
                    }
                ]
            },
            {
                test: /\.scss$/,
                include: path.resolve(__dirname, 'src/scss'),

                use:[
                    //下から順番に実行される(scss-loader -> css-loader -> style-loader)
                    'style-loader',
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    }
}

開発用の設定

開発環境では、developモードを有効にして、webpack-dev-serverも利用します。

webpack-mergeを利用して、共通設定に追加しているイメージです。

const merge = require('webpack-merge');

const baseConfig = require("./webpack.base.config.js");

module.exports   = merge(baseConfig, {
    mode: 'development',
    watch: true,
    devServer: {
        open: true,
        host: '0.0.0.0',
        port: 3000,
        openPage: 'index.html',
        contentBase: path.resolve(__dirname, 'public'),
        watchContentBase: true,
    },
    //ソースマップの表示
    devtool: 'cheap-module-eval-source-map'
});

本番環境の設定

本番環境でも同じようにwebpack-mergeを利用して、

共通設定に追加していきます。

const merge = require('webpack-merge');

const baseConfig = require('./webpack.base.config');

const TerserPlugin = require("terser-webpack-plugin")

module.exports = merge(baseConfig,{
    mode: 'production',
    // production時に有効になるoptimizationを上書き
    optimization: {
        minimizer: [
            new TerserPlugin({
                terserOptions: {
                    //console.logを削除する
                    compress: { drop_console: true}
                }
            })
        ]
    }
})

package.jsonにコマンドの使い分けを追記

最後に本番環境と開発環境で使い分けるコマンドを

package.jsonscriptsに記述していきます。

yarn run devyarn run prodで環境に合わせたビルドが可能になります。

  "scripts": {
    "start": "webpack-dev-server",
    "dev": "webpack --config webpack.dev.config.js",
    "prod": "webpack --config webpack.prod.config.js",
    "build": "webpack"
  }

まとめ

今回もつらづらと本を読んだメモを書いただけになりますが、 これまでコピペ使っていたwebpackが少しだけ読めるようになりました。

ただwebpackは設定できる項目がかなり多いようなので、 プロジェクトに合わせた設定が必要になってきます。

まだまだ習得には時間がかかりそうです。。

「ゼロからはじめるデータベース操作」を読んでSQLの基本を再復習した

新しい会社でWebエンジニアを初めて、2週間が経過しました。 Railsのコーディングとか、gitの操作だとかAWS周りとか毎日新しいことにチャレンジできてとにかく楽しいですね!!

今日は少しいつもとは違い、読書の防備録です。

今回読んだ本

SQLの入門書として、評判のいいミック先生の「ゼロからはじめるデータベース操作」です。

なぜ読んだか

現職で主に扱っている言語は、Ruby(RoR)PHP(Symphony+Zendとか色々濃縮された香ばしいシステム)の2つです。

フレームワーク側でORマッパーがあるので、直のSQL文を書くことはほとんどありませんが、

裏側でどんなSQLが動いているのか仕組みを理解しておきたくて、再復習も兼ねて読んでみました。

(Kindleで購入してしまったため写真がありません。。次から紙媒体で買います。。)

なぜ「SQL第2版 ゼロからはじめるデータベース操作」か

高専時代にSQLに関する授業があったのですが、その際の教科書でした。

ほぼ演習課題をひたすら実施していく授業スタイルだったのですが、

その際にかなり分かりやすい参考書という印象があったため再購入しました。

(当時の教科書は、ITにとても興味がない高専生だったので授業終わりと同時に捨ててしまいました。。)

何が得られたか

  • SQLの基本的な操作(SELECT, INSERT, DELETE, UPDATE)

  • 異なるテーブル同士を結合して、欲しいデータを取得する方法

  • INEXSITSを使った少し複雑なクエリ

今の時代、ほとんどのWebアプリ開発ではORマッパーが用意された

フレームワークを利用することがほとんどで、

裏側で動いているSQLクエリを意識せずともDBから欲しいデータが取ってくることができます。

ただ裏側の仕組みを知っておくことは大切だと思います。

裏側を知っていれば全体を意識しながらコーディングできる。

操作に時間がかかっている処理があった際に、トラブルシューティングができる。

アプリに関わるエンジニアは必読ではないでしょうか。

章末に演習があるのもありがたいです。

印象に残った部分の記録

演習に当たって、MysqlのDockerイメージとSequel Pro(DBを操作するGUIツール)を利用しました。

Mysql8系を利用したのですが、途中Sequel Proがクラッシュすることがあったので、

Table Plusとか別のツールの方がいいかもしれません。

version: "3"

services:
  db:
    image: mysql:latest
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: playground
      MYSQL_ROOT_PASSWORD: admin
      MYSQL_USER: docker
      MYSQL_PASSWORD: docker
      TZ: 'Asia/Tokyo'
    volumes:
      - ./data:/var/lib/mysql
      - ./log:/var/log/mysql
      - ./conf:/etc/mysql/conf.d

volumes: 
  data:
    driver: local
[client]
default-character-set=utf8mb4

[mysqld]
character-set-server=utf8mb4
general-log=1
general-log-file=/var/log/mysql/mysqld.log
default_authentication_plugin=mysql_native_password

第1章 データベースとSQL

DBの種類

  • 階層型DB

    • データを木構造で表現する
    • かなり前からあるデータベースで今はほとんど使われていない
  • リレーショナルDB

    • 今回学習したSQLを利用するDB
    • 列と行からなる2次元表でデータを管理するDB
  • オブジェクト指向DB

    • 保存されるデータ(オブジェクト)同士の関連をそのまま格納する
    • オブジェクトが保管されているため、ORマッパーが不要になる
  • XMLDB

    • XML形式のデータを大量かつ高速に扱うためのデータベース
    • SOAPとかで使われるのかな?
  • KVS

    • 検索に使うKeyとValueの組み合わせだけで保存するDB
    • RedisとかMemcachedが該当

あとは他にもJSON形式保管するドキュメント指向DBとかがありますよね。

SQLの文とその種類

下の用語を覚えていたからなんだと言う話ですが、

そう言う用語があるんだと思ったのでメモ。

  • DDL(Data Definition Language)

    • データ定義言語
    • CREATE
    • DROP
    • ALTER
  • DML(Data Manipulation Language)

    • データ操作言語
    • SELECT
    • INSERT
    • UPDATE
    • DELETE
  • DCL(Data Control Language)

    • データ制御言語
    • COMMIT
    • ROLLBACK
    • GRANT
    • REVOKE

テーブルの作成

テーブルの作成にはCREATE TABLEを使う。

CREATE TABLE (テーブル名)
( <列名> <データ型> <制約> ・・
create table
(shohin_id char(4) not null,
 shohi_mei varchar(100) not null,
 shohin_bunrui varchar(32) not null,
 hanbai_tanka integer ,
 shiire_tanka integer ,
 torokubi date ,
 primary key (shohin_id)
);

基本的なデータ型

型名 内容
INTEGER 数値
CHAR 固定長の文字列型。CHAR(最大長)
VARCHAR 可変長の文字列型。VARCHAR(最大長)
DATE 日付型

それぞれの列に対しての制約

列に入る値に対して制約をかけることができる

  • NOT NULL
    • 必ず値が入らなければならない
  • UNIQUE
    • 一意性の制約
  • DEFAULT
    • データ挿入時に値がなかった場合のデフォルト値
  • AUTO INCREMENT
    • 自動で連番を入力させる
  • FOREIGN KEY
    • 外部キー制約
    • 参照生合成を担保

テーブルの削除

DROP TABLE <テーブル名>

テーブル定義の変更

列を追加するとかテーブルのスキーマを変更する際に利用する

ALETER TABLE <テーブル名> <操作> <定義>;

列を追加する場合は下記。

ALTER TABLE <テーブル名> ADD COLUMN <列の定義>

ALTER TABLE Shohin ADD COLUMN shohin_mei_kana VARCHAR(100);

ちなみにテーブル名の変更はRENAMEを使う。

RENAME Table  Shohin to Item

データの登録

INSERT INTO <テーブル名> VALUES (<データ1>, <データ2>・・・)

INSERT INTO Shohin VALUES ( '0001', '鉛筆' ・・・・)

第2章検索の基本

重複行の削除

DISTINCTを使うと重複行を削除した状態で出力ができる。

SELECT DISTINCT shohin_bunrui
FROM Shohin;

比較演算子

演算子 意味
= ~と等しい
<> ~と等しくない
>= 以上
<= 以下
< より小さい
< より大きい

NULL判定

NULLを条件にしたい場合にはIS NULLを使い、

NULL以外を条件にした場合にはIS NOT NULLを使う。

SELECT * from Shohin
WHERE shiire_tanka IS NOT NULL;

第3章 集約と並べ替え

集約関数

データの合計値とか、平均を求めたい時には

DBが用意している集約関数を利用できる。

  • COUNT
    • レコード数
  • SUM
    • 合計
  • AVG
    • 平均
  • MAX
    • 最大値
  • MIN
    • 最小値

テーブルの行数を数えたい場合

SELECT COUNT(*) from Shohin
where shiire_tanka > 1000 AND shiire_tanka is not null

/* 重複行を除外して行数を数える場合には()内にDISTINCT */
SELECT COUNT(DISTINCT shohin_bunrui) from Shohin;

GROUP BY

~ごと~単位といった、集約にはGROUP BYを使う。 SELECTに指定できる列名は、GROUP BYで集約した列のみ。

SELECT <列名1> <列名2>
FROM <テーブル名>
GROUP BY <列名1> <列名2> 

GROUP BYWHEREを併用した場合のクエリの実行順序に注意

FROM -> WHERE -> GROUP BY -> SELECT

HAVING

集約した結果に対して、特定の条件を加えたい場合に使う

例えば、単価の合計値が1000円以上の商品分類など

集約関数を条件にすることが多い

SELECT <列名>
FROM <テーブル名>
GROUP BY <列名>
HAVING <集約したグループに対する条件>

/* HAVINGを利用した場合の実行順序 */
SELECT -> FROM -> WHERE -> GROUP BY -> HAVING

HAVING と WHEREどっち使う?

HAVINGで指定した集約関数によっては、

ソートが行われるため事前にWHEREでグループ化対象の行数を減らしておくことが望ましい。

  • HAVINGを使った方がいい場合
    • 条件に集約関数を使うとき
  • WHEREを使った方がいい場合
    • 集約した列に対する条件ではないとき

ORDER BY

検索結果を特定の列に対して並び替えを行って表示させたい時に使う

第2ソートキーを追加もできる。

SELECT <列名1> <列名2> ・・・
FROM <テーブル名>
ORDER BY <列名1> DESC(or ASC) <列名2> DESC(OR ASC)

4章 データの更新

INSERTで他のテーブルからデータをコピーする

INSERT INTO コピー先のテーブル名 (列名1 ,列名2, 列名3)
SELECT 列名1,列名2,列名3
FROM コピー元のテーブル名
WHERE 条件

トランザクション

例えば、銀行の振込みたいに、

処理AAさんの口座から1000円引くと処理BBさんの口座に1000円足す

みたいな複数の操作を連続的に行う一連の処理のことをトランザクションという。

START TRANSACTIONCOMMIT or ROLLBACKを指定する

  • COMMIT
  • ROLLBACK
    • 処理の引き戻しを示す
    • 一連の処理が全て取り消しされ、トランザクションが始まる前の状態になる
START TRANSACTION;

INSERT INTO ・・・

UPDATE SET ・・・

UPDATE SET ・・・

COMMIT;

ACID特性

トランザクション処理はACIDを守らなければならない

第5章 複雑な問い合わせ

ビュー

  • SELECT文をハードディスクに保存して、仮想的なテーブルを作成する
  • データを保存しないため、容量を節約することができる
CREATE VIEW <ビュー名> (<ビューの列名・・・>)
AS
SELECT・・・

作成したビューはテーブルのように呼び出すことができる。

SELECT *
FROM <ビュー名>

サブクエリ

FROMの配下にSELECTを繋げて、使い捨てのビュー的なものを作成できる

SELECT shohin_bunrui, cnt_shohin
FROM ( SELECT shohin_bunrui, COUNT(*) as shohin_bunrui
              FROM Shohin
              GROUP BY shohin_bunrui) as ShohinSum;

スカラサブクエリ

必ず1行1列だけの戻り値を返すサブクエリをスカラサブクエリと呼ぶ

スカラサブクエリを使うことで、where内で集約関数を使うことができる。

SELECT shohin_id, shohin_mei, hanbai_tanka
FROM Shohin
WHERE hanbai_tanka > ( SELECT AVG(*)
                                           FROM Shohin);                                          

相関サブクエリ

外側のクエリの値を使うサブクエリを相関サブクエリという。

上のスカラサブクエリでは下記のSQL文を実行できない。 サブクエリの実行結果が、複数行になるため。

SELECT shohin_id, shohin_mei, hanbai_tanka
FROM Shohin
WHERE hanbai_tanka > ( SELECT AVG(*)
                                           FROM Shohin
                                          ORDER BY shohin_bunrui);                                          

下記のように、外側のクエリの値を限定してあげることで実行できる。 shohin_bunruiを限定しているため、1行1列の値を返している。

SELECT shohin_id, shohin_mei, hanbai_tanka
FROM Shohin as T1
WHERE hanbai_tanka > ( SELECT AVG(*)
                                           FROM Shohin as T2
                                           WHERE T1.shohin_bunrui = T2.shohin_bunrui
                                          ORDER BY shohin_bunrui);                                          

第6章 関数、述語、CASE式

算術関数

  • MOD
    • 剰余を返す
  • ROUND(対象の値, 丸める桁数)
    • 四捨五入

文字列関数

  • CONCAT(文字列1, 文字列2)
    • 文字列連結
  • LENGTH(文字列)
    • 文字列の長さ
    • MYSQLはバイト数を数えるため日本語1文字(2byte)は2になる
  • LOWER, UPPER
    • 小文字、大文字にする
  • REPLACE(対象文字列, 置換前, 置換後)
    • 文字列置換
  • SUBSTRING(対象文字列 FROM 切り出し一のindex FOR 切り出す文字数)
    • 文字列の切り出し

日付関数

  • CURRENT_DATE
    • 現在の日付(Y-M-d)
  • CURRENT_TIME
    • 現在の時刻(h:m:s)
  • CURRENT_TIMESTAMP
    • 現在の日時(Y-M-d h:m:s)
  • EXTRACT(日付要素 FROM 日付)
    • 特定の日付要素(例えば年だけとか)を切り出す
      • EXTRACT(YEAR FROM CURRENT_TIME_STAMP)

変換関数

  • CAST(変換前のデータ型 AS 変換後のデータ型)
    • 型変換
  • COALESCE(データ1, データ2・・・)
    • NULLを別な値に変えて扱う
    • COALESCE(NULL, 'test', NULL)だとtestが表示される

LIKE述語

部分一致検索ができる

  • 前方一致
    • 文字列%
  • 後方一致
    • %文字列
  • 中間一致
    • %文字列%
SELECT *
from Shohin
WHERE name LIKE "%パンツ%"

IN 述語

IN内に含まれたデータを抽出できるようになる。

(否定の場合はNOT IN)

サブクエリと組み合わせることで柔軟な取り出しができそう

SELECT shohin_id, shohin_mei, hanbai_tanka
FROM Shohin
WHERE shohin_mei IN ( SELECT shohin_mei
                                        FROM Shohin
                                       WHERE shohin_id = "000c")

EXSITS述語

ある条件に合致するレコードの存在有無を調べる。

詳しい説明は下記がいいと思います。

[参考記事] (https://qiita.com/darkimpact0626/items/5a5d03c27ae7c849566f)

CASE式

プログラミング言語みたいに、case ~ whenができる

CASE WHEN <条件式> THEN <式>
          WHEN <条件式> THEN <式>
         ELSE    <式>
END

/* 例えば価格帯ごとの件数を調べる場合 */
SELECT 
     COUNT(CASE WHEN hanbai_tanka < 500 THEN 1 END) as low,
     COUNT(CASE WHEN hanbai_tanka BETWEEN 501 AND 1000 THEN 1 END) as mid,
     COUNT(CASE WHEN hanbai_tanka > 1000 THEN 1 END) as high
FROM Shohin;

第7章 集合演算

積集合(INTERSECT)

SELCT ~
FROM テーブル1
INTERSECT
SELECT ~
FROM テーブル2

差集合(EXCEPT)

SELECT ~
FROM テーブル1
EXCEPT
SELECT ~
FROM テーブル2

INNER JOIN

2つ以上のテーブルを結合することを結合(JOIN)という。

両方のテーブルが指定したカラムの値が一致するものだけを結合するのが内部結合(INNER JOIN)

SELECT T3.tenpo_id, T3.tenpo_mei, T1.shohin_mei, T2.zaiko
FROM Shohin as T1
INNER JOIN Zaiko as T2 ON T1.shohin_id = T2.shohin_id
INNER JOIN Tenpo as T3 ON T2.tenpo_id = T3.tenpo_id

OUTER JOIN

片方のテーブルの値が全て取得される結合を外部結合(OUTER JOIN)

下記の場合はShohinのテーブルの値が全て出力される

SELECT T2.tenpo_id, T2.tenpo_mei, T1.shohin_mei, T1.hanbai_tanka
FROM Zaiko as T1
LEFT OUTER JOIN Tenpo as T2 ON T1.tenpo_id = T2.tenpo_id

【Rails】Decoratorについての忘備録

記事を書くことになった背景

業務で知った内容です。

1月に開発未経験でのWebエンジニアに転職しているのですが、

まずは既存のアプリケーションの改修や小さい新機能を追加することが多くあります。

リソースの状態(大体5パターン)に応じて、Viewに表示させる項目を切り替える必要がありました。

そこでの先輩との会話

私「モデルにリソースの状態に応じて判断メソッドを書けば終わりっすね!」

先輩「Viewでしか使わないからDecoratorでもいいかもなー」

私「分からん!!」

と言う状況だったのですが、調べてみたらシンプルに感激したので記事に残します。

記事で書く内容

  • Decoratorパターンについての説明

  • Railsに置けるDecoratorについて

  • Decoratorパターンを簡単に実装するgem Draperの説明

Decoratorパターンとは

GoFデザインパターンの一つ

Javaで学ぶデザインパターンには下記のように例えられています。

ある元となるクラスに対して機能を拡張していく際に利用できるデザインパターンになると思われる。

まず中心となるスポンジケーキのようなオブジェクトがある それに飾り付けとなる機能を一皮一皮被せていって、より目的にあったオブジェクトに仕上げていくのです。

正直、頭の中でお花畑が見えましたが、使うと何が嬉しいかについては、下記のように理解しました。

  • 元となるクラスを継承して、飾り枠(ConcreteDecorator)に機能を拡張していく
    • 元となるクラスに対して変更が発生しない
  • 飾り枠をたくさん用意して組み合わせること多様なパターンの機能拡張ができる
    • 表示などをカスタマイズする際には最適なパターンになるかも
    • 元のクラスに対してコードを追加していく訳ではないので、1つのクラスが肥大化していくことを防ぐことができる

ただ、ConcreteDecorator自体が肥大化したり、似たようなクラスが

増えてくると言うアンチパターンと隣合わせと言うことは認識しておかなければなりません。

実際にコードを書いてみた系は、別の記事を参照してください。。

クラス図

一般的なDecoratorパターンのクラス図は下記のようになります。

f:id:wa_football_1120:20200125204722p:plain

  • Component

    • 機能を追加するときになる元となる抽象クラス
  • ConcreteComponent

  • Decorator

    • 装飾の元となる抽象クラス
    • Componentをインスタンス変数としてもち、飾り付けの対象を知っている
  • ConcreteDecorator

    • 実際に装飾の仕方を定義しているクラス
    • これを追加していくことで、多様な拡張が可能になる

RailsにおけるDecoratorについて

大まかな特徴としては、

  • Viewでしか使わないModelのメソッドをdecoratorを使うことで切り離すことができる
  • decorator内ではhelperを利用することができる
    • link_toといったViewで使うメソッドをdecoratorで定義することができる

decoratorを使うと何が嬉しいか言うと、

  • ビジネスロジックとプレゼンテーション(表示)に関わる関心を分離することができる

  • 上記を実装することで、1つのモデルが肥大化することを防ぐことができる

ことではないかと思います。

ただ、上のDecoratorパターンのところにも書きましたが、

Decoratorが肥大化する懸念があることについては認識しておく必要があります。

複数のモデルで使われる共通のロジックについてはhelperに記載した方がいいでしょう。

Draperについて

Draperって?

Draperは、decorator(プレゼンテーション)の実装を簡単にしてくれるgemになります。

ど素人目線で行ってしまうと、ある特定のモデルかつViewでしか使わないメソッドはdecoratorに書いた方がいいよっていう感じです。

特徴としては、

  • ViewとModelの中間に位置するVM(View Model)
    • Vue.jsとかで言われるVMMVとは異なる
  • プレゼンテーションのロジックやフォーマットの処理を実装することができるようになる

使いかた

ライブラリのインストール

おなじみのGemfileに下記を記述して、bundle installしてください。

# Gemfile
gem 'draper'

# Gemのインストール
bundle install

Decorator作成

bundle exec rails g decorator User

Running via Spring preloader in process 284
      create  app/decorators/user_decorator.rb
      invoke  rspec
      create    spec/decorators/user_decorator_spec.rb

こんな感じでViewで利用するhelperを書くことができます。

class UserDecorator < Draper::Decorator
  # 全ての属性にアクセスできるようにする
  delegate_all

  def welcome_message
    if user_signed_in? && !current_user.confirmed?
      return "ゲストユーザ(登録が完了していません)"
    else
      return "ようこそ、#{name}"
    end
  end
end

コントローラではdecoratorメソッドで、上で定義したメソッドを使えるようにしてあげます。

def index
     @user = User.find(id: ~~).decorate
end

そしてViewからは、以下のようにアクセスすればOKです。

View内で、If分岐させなくて済むのがポイントです。

h1
   = @user.welcome_message

まとめ

いつも通りの雑な記事ではありますが、学んだことは下記。

  • RailsではDecoratorを使うとことで、Viewでしか使わないロジックを分離できる
    • ModelやViewのコードが肥大化せずにすむ
  • Decoratorを使う際にはgemのdraperを使うのがおすすめ
  • Controllerでは抽出したモデルに対してdecorateメソッドを使ってあげる

【AWS】TerraformでマルチAZ構成のインフラを構築をやってみた(マルチAZ, RDS, ALB, AutoScaling)

こんにちは、駆け出しエンジニアです。 前回に引き続きAWSとTerraformの記事になります。 そろそろRoRとかVueの方を勉強していかないとまずいのではと焦っています。

何をやるか

前回同様に、AWSアソシエイトの勉強を継続しています。 自分の引き出しを増やしておきたいと思いどうせなら構築はTerraformでやってみようと四苦八苦中です。 今回は前回作成した構成をより冗長性が高い構成にしようという記事です。 (経緯)

得られた内容

  • 冗長性を高めるためのインフラ構成案
  • ELB、AutoScalingについての概要
  • RDSの概要
  • Terraformを利用した、マルチAZELB(Elastic Load Balancer)RDS(Relational Database Service)の構築

12月に作成したインフラの構成

  • 単一のAZに1つのVPCを作成
  • 作成したVPC内にパブリックサブネットとプライベートサブネットを1つずつ構築
  • パブリックサブネット内に、Webサーバを想定したEC2インスタンスを1つ構築
  • プライベートサブネット内に、DBサーバを想定してEC2インスタンスを1つ構築
  • DBサーバには、EC2インスタンスからしトラフィックを受け取らないように、セキュリティグループを設定

f:id:wa_football_1120:20191222142939j:plain

W-Aにこれでもかというぐらい意に反した、我が道を行く構成になっています。

改善すべき点の洗い出し

冗長性を高めるための基本的な改善点しか思いついていません。

1点目 単一のAZインフラ構成

個人利用程度の小さなWebサービス(ブログなど)であれば問題がないのかもしれませんが、

単一のAZで構成されているためAZ自体が障害に見舞われた時に、サービスが全てダウンしてしまいます。

これはW-Aでいう信頼性を担保できていない改善点となるのではないでしょうか。

2点目 単一のEC2で稼働しているWebサーバ

トラフィック増加や予期せぬサービスダウンにより、Webサーバがダウンすることはリスクとして捉える必要があります。

先ほどの単一AZの改善点同様に、EC2も複数台による冗長な構成を作る必要があります。

実際のサービスでは複数台動いているWebサーバに対して、不可分散をしながら特定のインスタンスへアクセスが集中しないロードバランサが必要です。

3点目 自前でEC2に構築したDBサーバ

特殊な業務要件(ファイルシステムと連携する必要がある場合やミドルウェアをもっとカスタマイズしたいなど)の場合には、EC2上に自前で構築でもいいかもしれません。

今回は、このような業務要件はないことを想定して構築しますので、マネージドサービスを利用した方が、構築や運用の効率がよくなります。

改善策

上記の改善点に対する、改善案は表のようにまとめました。

VPC冗長化については今回は別ものとさせてください。

改善点 改善後の構成
単一AZのインフラ構成 マルチAZ構成をとる
単一のEC2で稼働しているWebサーバ 前段にロードバランサを配置。要求(リクエスト)に応じて水平方向のスケーリング(AutoScaling)
自前でEC2に構築したDBサーバ マネージドサービスのRDSを利用する

インフラ構成を図に示すと下記のようなイメージです。

まだまだシンプルで簡易的な構成ですが、入門編として見逃してください。

f:id:wa_football_1120:20200112173408j:plain

予備知識

今回の構成で新規に追加するELBAutoScalingRDSについて簡単にまとめていきます。

ELB

マネージド型のロードバランシングサービスです。

EC2と組み合わせて複数リソースへの不可分散やヘルスチェックができるようになります。

現状主要なELBとしてはALB(アプリケーションロードバランサ:レイヤ7で負荷分散)NLB(レイヤ4で負荷分散)がありますが、以降はALBを前提とした説明です。

特徴

  • 複数AZにまたがって、トラフィックの不可分散をすることができる

  • パブリックサブネット、プライベートサブネットどちらにも配置ができる

  • インスタンスのヘルスチェックを行い、異常と判定されたインスタンスに対してはリクエストを分散しない

  • マネージドサービスで、リクエスト量に応じてELB自体も自動的にスケーリングされる

主要な機能

  • ヘルスチェック

    • 指定したプロトコルポート番号パス失敗時のしきい値を条件に指定して、インスタンスが正常に動作しているかチェックする
    • しきい値を超えた場合には、インスタンスの状態を異常と判定し、自動的に負荷分散先から切り離す f:id:wa_football_1120:20200113161105j:plain
  • 負荷分散の方式

    • AZ間の負荷分散は、DNSラウンドロビン(DNSキャッシュされている場合、特定のAZにしかリクエストが飛ばない可能性がある)
    • AZ内の負荷分散は、Least Connection(コネクション数がもっとも少ないリソースへ飛ばす)
  • SSL Termination

    • ロードバランサ側でSSL認証を実施する機能
    • ロードバランサにSSL証明書を設定することで、クライアント~ロードバランサ間はHTTPSで、ロードバランサ~リソース間はHTTPで通信を流すことができる
      • いちいち、リソース1個ずつに対して証明書をダウンロードすることが不要になってくる
  • スティッキーセッション

    • セッションを保持するアプリケーションなどで、同一のユーザからきたリクエストを全て同じインスタンスに転送する
    • HTTPレスポンスにELBがCookieを埋め込んで、リクエストの転送先を固定する
  • Connection Draining

    • メンテなどで負荷分散先から登録を削除しているインスタンスや異常が発生したインスタンスに対してリクエストを転送しないようにする
      • ただし、処理中のリクエストについては、処理を続ける
  • X-forwardedヘッダーのサポート

    • ロードバランサを介したHTTPリクエストの場合、インスタンスからみたリクエスト元のIPアドレスは全てロードバランサとなってしまう
    • X-forwardedをHTTPヘッダーに付与して、実際のリクエスト元(クライアント)のIPアドレスを判別することができる

      X-Forwarded-Forとは、HTTPヘッダフィールドの1つであり、ロードバランサなどの機器を経由して  Webサーバに接続するクライアントの送信元IPアドレスを特定する際のデファクトスタンダードです。  クライアントの送信元IPアドレスの特定は、ロードバランサなどでクライアントの送信元IPアドレスが  変換された場合でも、HTTPヘッダに元のクライアントIPアドレスの情報を付加することで実現します。

Auto-Scaling

需要(トラフィック量)やリソースの障害発生時に、自動的にインスタンスの数を増減することができる機能。

前述のELBと併用して利用されるケースが多くなる。

利用するメリット

  • 可用性の向上

    • 繁忙期やピーク時間帯には、需要量に応じて自動的にインスタンスの数をスケールアウト(台数を増やす)ことができる
    • 逆に閑散期については、スケールイン(台数を減らす)こともできる
  • 耐障害性の向上

  • コスト効率の向上

    • 需要量に応じて、適切な量のインスタンスを立ち上げるため、ピーク時に備えた台数を常に稼働させておくよりもコストがかからなくなる

設定する項目

  • Auto-Scalingグループ

    • 立ち上げるインスタンスの最小数、最大数、希望する数(増減数)を指定する
    • 設定した最小数と最大数の間でインスタンスの数を増減させる
  • Auto-Scaling Configration

    • 起動するインスタンスの種類(インスタンスタイプやストレージサイズ、AMI)を設定する
    • セキュリティグループやキーペアもここで設定
  • Auto-Scaling Plan

    • どのようにスケール(台数を増やしたり減らしたりする)かを設定する。下記の方法を複数組み合わせることもできる。
      • 手動でのスケーリング
        • 事前にスケーリングする必要が分かっている(キャンペーンやユーザ告知)した場合には手動でスケーリングすることができる
        • Auto-Scalingグループの 最小数最大数希望する数を調整する
      • スケジュールに応じてスケーリング
        • ある一定時間(お昼休みなど)だけスケーリングさせるといった設定ができる
        • 予定アクションを定義することで実現する
    • 需要に応じてスケーリング
      • CloudWatchのCPU Utilizationを監視してしきい値を超えた場合といった設定ができる
      • ポリシーを定義することで実現する

リソース削除の設定

需要が減ったりすれば、余剰なインスタンスを終了(Terminate)する必要があります。 この時、どのインスタンスを削除するかについて設定することが可能です。

  • OldestInstance/NewestInstance
    • 作成されてから最も古い/新しいインスタンスから削除
      • 後からAuto-Scaling作成した場合、オリジナルのEC2(スケール時のクローン元)が削除される
  • OldestLaunch Configration
  • ClosestToNextInstanceHour
  • デフォルト設定
    • インスタンスを削除する対象のAZを選択
      • 最も多くのインスタンスが設置されているAZを選択
      • 同数の場合にはランダム
    • 対象のAZから削除対象のインスタンスを削除
      • OldestLaunch ConfigrationからClosetLaunch Configrationの順に削除対象を決めていく
      • 複数の削除候補がいる場合には、候補からランダムで選択

RDS

マネージドサービス型のリレーショナルDBサーバです。

パッチ適用といった運用業務が不要なりますが、VPC内に配置されるため可用性の確保は利用者側で設定が必要です。

また、OSのログインは不可であることから、DBサーバの細かい設定をした場合やNFS(Network File System)を利用するといったカスタマイズはできません。

代表的な仕組みとしてマルチAZ構成(マスタ/スレーブ構成)リードレプリカがある。

マルチAZ構成

1つのリージョン内の2つのAZにDBインスタンスを配置して、障害発生時やメンテナンス時に自動的にフェイルオーバさせる

マスタとスレーブでインスタンスを待機させていて、それぞれのインスタンスは同期レプリケーションされている

リードレプリカ

参照専用のDBインスタンスを配置されることができる(マスタのインスタンスと別AZにも配置ができる)

非同期レプリケーションであるため、更新された内容が即時反映されていない場合がある

Terraformによるコード実装

実際に予備知識は置いたということで、Terraformで実装していきたいと思います。

ディレクトリ構成は下記の通りです。

.
├── README.md
├── alb
│   ├── main.tf
│   └── output.tf
├── autoscaling
│   ├── main.tf
│   └── output.tf
├── config.tfvars
├── ec2
│   ├── main.tf
│   ├── output.tf
│   └── setup.sh
├── main.tf
├── network
│   ├── main.tf
│   └── output.tf
├── rds
│   ├── main.tf
│   └── output.tf
├── sg
│   ├── main.tf
│   └── output.tf
|── variables.tf

ソースの細かい説明は省くので、詳細はgitリポジトリにコードを挙げていますので参照ください。

以下、自分用の忘備録になるので、飛ばしてもらって大丈夫です。

VPC構築

基本的に前回作成したコードを流用します。

前回との変更点としては、マルチAZ構成に変更することがメインです。

実際にコードを見てもらった方が分かりやすいかと思います。

組み込み関数のlengthlookupelementを利用して、冗長な書き方にならないようにしました。

組み込み関数を全て説明してくれている記事がありました

例えばこんな感じで、変数でmapを使って複数のサブネットを作成しました。

######################################
# Varibales                          #
######################################
variable "vpc_parameter" {
    default = {
        vpc_subnets       = "10.0.0.0/16"
        public_subnets    = {
            "0" = "10.0.1.0/24",
            "1" = "10.0.2.0/24"
        },
        private_subnets   = {
            "0" = "10.0.5.0/24",
            "1" = "10.0.6.0/24"
        },
        availability_zones = {
            "0" = "ap-northeast-1a",
            "1" = "ap-northeast-1c"
        }
    }
}

######################################
# VPC                                #
######################################
resource "aws_vpc" "example_vpc" {
    cidr_block           = var.vpc_parameter.vpc_subnets
    enable_dns_support   = true # DNS名前解決をサポート
    enable_dns_hostnames = true # パブリックIPアドレスをもつインスタンスの名前解決を有効化

    tags = {
        Name = "Example"
    }
}

######################################
# Public Subnet                      #
######################################
resource "aws_subnet" "example_public" {
    vpc_id                  = aws_vpc.example_vpc.id
    count                   = length(var.vpc_parameter.public_subnets)
    cidr_block              = lookup(var.vpc_parameter.public_subnets     , count.index, "Not Found")
    availability_zone       = lookup(var.vpc_parameter.availability_zones, count.index, "Not Found")
    map_public_ip_on_launch = true

    tags = {
        Name = format("Example-VPC-Public-Subnet-%d", count.index)
    }
}

複数作成したリソースの出力を取得したい場合は、joinを使えば良さそうです。

配列要素をマージして1つの文字列として出力してくれます。

output "private_subnets_id" {
    value = join(",", aws_subnet.example_private.*.id)
}

output "public_subnets_id" {
    value = join(",", aws_subnet.example_public.*.id)
}

Security Group作成

プライベートサブネットに作成したRDS用のセキュリティグループに、NAT Gatewayのみ(2つのAZにそれぞれ1つずつ)をルールに加える必要がありました。

サブネット/32で指定したかったので、forを使って実現しています。

resource "aws_security_group_rule" "egress_all_rds" {
    type                     = "egress"
    from_port                = 0
    to_port                  = 0
    protocol                 = "tcp"
    cidr_blocks              = [for ip in local.nat_gateway_ips : "${ip}/32"]
    security_group_id        = aws_security_group.rds_sg.id
}

EC2作成

EC2は各AZのパブリックサブネットに1つずつ作る必要がありましたが、aws_instanceを2ブロック書くのは冗長になってしまいます。。

countを使って、複数のリソースを1つのブロックで書けるようにしました。

resource "aws_instance" "web_servers" {
    count                  = length(var.subnet_ips)
    ami                    = data.aws_ami.recent_amazon_linux_2.id
    instance_type          = var.instance_type
    key_name               = aws_key_pair.instance_ssh_key_pairs.id
    vpc_security_group_ids = [var.sg_id]
    subnet_id              = element(split(",", var.subnet_ids), count.index)

    tags                   = {
        Name = "Test ELB Web Server ${count.index}"
    }

    user_data = file("./ec2/setup.sh")

    depends_on             = [var.subnet_ids]
}

ただ、countを利用する際には、terraform planの段階で何個リソースを作成するか分かっていないければいけません。

別のモジュールで作成したリソースの個数は指定することができません。

下記のようなErrorが出てしまうことがあります。

The "count" value depends on resource attributes that cannot be determined
until apply, so Terraform cannot predict how many instances will be created.
To work around this, use the -target argument to first apply only the
resources that the count depends on.

今回のコードではcountの値にvar.subnet_ipsを利用しているのですが、これは事前にvariables.tfに定義している変数なので、繰り返す回数がすでに分かっている状態です。

ALB作成

まずは、ALBのアクセスログを保管するたhttps://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/application/load-balancer-access-logs.htmlめのBucketを作成しました。

S3のBucketポリシーを定義していく必要があります。参考

dataリソースを定義して、Bucketポリシーを外部リソースとして定義することができます。

################################################
# S3 bucket to writing access logs Settings    #
################################################
resource "aws_s3_bucket" "alb-logs-bucket" {
    bucket        = "turedure-alb-logs-bucket-test-one"
    acl           = "private"
    # For test
    force_destroy = true

    lifecycle_rule {
        enabled = true
        id      = "alb-log"
        prefix  = "alb-log/"

        transition {
            days          = 30
            storage_class = "STANDARD_IA"
        }
        transition {
            days          = 60
            storage_class = "GLACIER"
        }
        expiration {
            days          = 90
        }
    }
}

resource "aws_s3_bucket_public_access_block" "alb-logs-bucket" {
    bucket                  = aws_s3_bucket.alb-logs-bucket.id
    block_public_acls       = true
    block_public_policy     = true
    ignore_public_acls      = true
    restrict_public_buckets = true
}

resource "aws_s3_bucket_policy" "alb-logs-bucket-policy" {
    bucket = aws_s3_bucket.alb-logs-bucket.id
    policy = data.aws_iam_policy_document.alb_log.json

    depends_on = [aws_s3_bucket_public_access_block.alb-logs-bucket]
}

# Bucket Policy
data "aws_iam_policy_document" "alb_log"{
    statement {
        effect    = "Allow"
        actions   = ["s3:PutObject"]
        resources = ["arn:aws:s3:::${aws_s3_bucket.alb-logs-bucket.id}/*"]

        principals {
            type = "AWS"
            identifiers = ["582318560864"]
        }
    }
}

次にALBを作成していきます。

作成時に定義する必要があるリソースは下記です。

リソース名 説明
aws_lb ELBの本体。LBのタイプやセキュリティグループをアタッチする。
aws_lb_target_group LBのターゲットグループ。負荷分散するグループを定義する。
aws_lb_listener LBのリスナーを作成。ターゲットグループをアタッチしてルーティングをどのようにするかを設定する
aws_lb_target_group_attachment ターゲットグループとインスタンス(ECSの場合はコンテナ)を紐付ける

aws_lb

パラメータ名 説明 必須か?
name ALBの名前 No
internal trueであればInternal LB(プライベートサブネット用のLB) No
security_groups アタッチするセキュリティグループ No
subnets ALBを紐付けるサブネットID No
access_logs ELBのアクセスログの保存先 No
resource "aws_lb" "example_alb" {
    name               = "turedure-example-alb"
    internal           = false
    load_balancer_type = "application"
    security_groups    = [var.alb_sg_id]
    subnets            = local.public_subnets_list

    access_logs {
        bucket         = aws_s3_bucket.alb-logs-bucket.bucket
        enabled        = true
    }

    tags = {
        Name           = "Test-Alb"
        Environment    = "development"
    }

    # S3のアクセス許可が出るまでペンディング
    depends_on         = [aws_s3_bucket_policy.alb-logs-bucket-policy]
}

aws_lb_target_group

パラメータ名 説明 必須か?
name ターゲットグループの名前 Yes
port ターゲットグループがトラフィックを待ち受けるポート Yes
protocol ターゲットグループがトラフィックを待ち受けるプロトコル Yes
vpc_id ターゲットグループが所属するVPCのID Yes
deregistration_delay ELBが異常と判断してから実際にターゲットを登録解除するまでに待つ時間 No
health_check ヘルスチェックの設定 No
resource "aws_lb_target_group" "example_alb" {
    name                 = "turedure-example-target-group"
    port                 = 80
    protocol             = "HTTP"
    deregistration_delay = 300
    vpc_id               = var.vpc_id

    health_check {
        interval            = 30
        path                = "/index.html"
        port                = 80
        timeout             = 5
        unhealthy_threshold = 2
        matcher             = 200
    }
    depends_on          = [aws_lb.example_alb]
}

aws_lb_listener

今回は証明書などは発行していないく、HTTPでクライアントからリクエストが来る想定です。

パラメータ名 説明 必須か?
load_balancer_arn LBのARN Yes
port LBが待ち受けるポート Yes
protocol クライアント~LB間のプロトコル No
default_action LBがトラフィックを受けた際にどのように処理するか Yes
resource "aws_lb_listener" "example_alb" {
    load_balancer_arn  = aws_lb.example_alb.arn
    port               = "80"
    protocol           = "HTTP"
    default_action {
        target_group_arn = aws_lb_target_group.example_alb.arn
        type             = "forward"
    }
    depends_on         = [aws_lb_listener.example_alb]
}

aws_lb_target_group_attachment

パラメータ名 説明 必須か?
target_group_arn 紐づけるターゲットグループのARN Yes
target_id ターゲットグループに紐づけるインスタンスのID Yes
port ターゲットが待ち受けるポート番号 Yes

複数のインスタンスを紐付ける必要があったためcountで回しています。

resource "aws_lb_target_group_attachment" "example_alb" {
    count              = length(var.public_subnet_ips)
    target_group_arn   = aws_lb_target_group.example_alb.arn
    target_id          = element(local.instance_ids_list, count.index)
    port               = 80
    depends_on         = [aws_lb.example_alb]
}

Auto Scaling作成

|リソース名|説明| |aws_launch_configuration| Auto Scalingから作成されるインスタンスの起動設定| |aws_autoscaling_group| Auto Scalingグループ| |aws_autoscaling_policy| スケール(イン or アウト)の設定| |aws_cloudwatch_metric_alarm| CloudWatchとAutoScalingを連携させるためのアラート処理|

aws_launch_configuration

事前にAMIを作成済みであればここら辺の細かい設定が不要になると思います。

パラメータ名 説明 必須か?
name_prefix AMIの設定名(nameにすると、terraformで再作成できなくなる。) No
image_id AMIのID Yes
instance_type インスタンスタイプ Yes
key_name 作成したインスタンスに接続するためのキー名 No
security_groups アタッチするセキュリティグループ No
associate_public_ip_address 作成したインスタンスにパブリックIPアドレスをアタッチするか No
user_data インスタンスを作成する際に実行するスクリプト No
resource "aws_launch_configuration" "example_as_conf" {
    name_prefix                 = "webserver"
    image_id                    = var.ami_id
    instance_type               = var.instance_type
    key_name                    = var.key_name
    security_groups             = [var.sg_id]
    associate_public_ip_address = true
    user_data                   = file("./ec2/setup.sh")

    lifecycle {
        create_before_destroy   = true
    }
}

aws_autoscaling_group

パラメータ名 説明 必須か?
name_prefix グループ名 Yes
max_size インスタンスの最大数 Yes
min_size インスタンスの最小数 Yes
launch_configuration 起動するAMIの設定 No
vpc_zone_identifier インスタンスを配置するサブネットのID No
desired_capacity デフォルト(Auto Scaling起動時)のインスタンスの台数 No
health_check_grace_period インスタンスが起動してからヘルスチェックするまでの時間 No
health_check_type ヘルスチェックの方式(EC2単体かELBと連携するか) No
target_group_arns ターゲットグループのARN No
resource "aws_autoscaling_group" "example_asg" {
    name_prefix                 = "turedure-websv-v1"
    max_size                    = 4
    min_size                    = 1
    launch_configuration        = aws_launch_configuration.example_as_conf.name
    vpc_zone_identifier         = split(",", var.public_subnets_id)
    # Number of creating instance size when initialize auto scaling group
    desired_capacity            = 1

    health_check_grace_period   = 300
    health_check_type           = "ELB"
    target_group_arns           = [var.alb_target_group_arn]

    force_delete                = true

    lifecycle {
        create_before_destroy   = true
    }

    tags = [
        {
            key                 = "Name"
            value               = "test-autoscaling-group"
            propagate_at_launch  = false
        },
        {
            key                 = "Environment"
            value               = "development"
            propagate_at_launch = false
        }
    ]
}

auto_scaling_policy, aws_cloudwatch_metric_alarm

auto_scaling_policyで増減する方法を定義して、aws_cloudwatch_metric_alarmと連携させるイメージです。

  • auto_scaling_policy
パラメータ名 説明 必須か?
name ポリシー名 Yes
scaling_adjustment 何台増減させるか No
adjustment_type 増減のさせかた(ChangeInCapacityは指定した値だけ既存の値から増減) No
cooldown 一度増減させてから、次に増減できるまでの間隔(一時的なリクエスト増で何台もインスタンスが作成されないようにする) No
autoscaling_group_name AutoScalingグループの名前 Yes
  • aws_cloudwatch_metric_alarm
パラメータ名 説明 必須か?
alarm_name アラートの名前 Yes
comparison_operator しきい値からどうなった場合にアラートが発生するか Yes
evaluation_periods しきい値を超えるイベントが繰り返し発生した回数 Yes
metric_name しきい値に使うメトリック名 No
namespace メトリックの名前空間。metric_nameとセットで使う No
period チェックする間隔 No
statistic どの統計値を判断材料に利用するか No
dimentions AutoScalingグループ名を指定して、通知されるように設定(よくわからん) No
alarm_actions アラートが生成された際に、自動的に行う動作
resource "aws_autoscaling_policy" "scale_out" {
    name                   = "Instance-Scaleout-Policy"
    scaling_adjustment     = 1
    adjustment_type        = "ChangeInCapacity"
    cooldown               = 300
    autoscaling_group_name = aws_autoscaling_group.example_asg.name
}

resource "aws_autoscaling_policy" "scale_in" {
    name                   = "Instance-Scalein-policy"
    scaling_adjustment     = -1
    adjustment_type        = "ChangeInCapacity"
    cooldown               = 300
    autoscaling_group_name = aws_autoscaling_group.example_asg.name
}

resource "aws_cloudwatch_metric_alarm" "cpu_usage_high" {
    alarm_name             = "test-cpu-usage-high"
    comparison_operator    = "GreaterThanOrEqualToThreshold"
    evaluation_periods     = "1"
    metric_name            = "CPUUtilization"
    namespace              = "AWS/EC2"
    period                 = "300"
    statistic              = "Average"
    dimensions             = {
        AutoScalingGroupName = aws_autoscaling_group.example_asg.name
    }
    alarm_actions          = [aws_autoscaling_policy.scale_out.arn]
}
resource "aws_cloudwatch_metric_alarm" "cpu_usage_low" {
    alarm_name             = "test-cpu-usage-low"
    comparison_operator    = "LessThanThreshold"
    evaluation_periods     = "1"
    metric_name            = "CPUUtilization"
    namespace              = "AWS/EC2"
    period                 = "300"
    statistic              = "Average"
    dimensions             = {
        AutoScalingGroupName = aws_autoscaling_group.example_asg.name
    }
    alarm_actions          = [aws_autoscaling_policy.scale_in.arn]
}

RDS

RDSのリソース名は、シンプルなので説明を省きます。(疲れました)

aws_db_parameter_groupで動作するDBのパラメータをいじることができます。

あとは、skip_final_snapshottrueにしておかないと、terraform destory時にエラーが吐かれて削除できません。

(GUIでも、スナップショットをどうするか問われるダイアログがあったので、コードベースだと明示的に定義する必要があるのだろうと思っています)

resource "aws_db_parameter_group" "db_parameter" {
    name     = "${var.db_parameter_group.name}-parameter"
    family   = var.db_parameter_group.family

    parameter {
        name  = "character_set_server"
        value = "utf8"
    }

    parameter {
        name  = "character_set_client"
        value = "utf8"
    }
    
}

resource "aws_db_subnet_group" "db_subnets" {
    name       = "private-subnet"
    subnet_ids = split(",", var.private_subnets_ids)

    tags       = {
        Name = "Private Subnet"
    }
}

resource "aws_db_instance" "db_server" {
    identifier                 = "test"
    allocated_storage          = 20
    engine                     = "mysql"
    engine_version             = "5.7.22"
    instance_class             = "db.t2.micro"
    name                       = var.rds_name
    db_subnet_group_name       = aws_db_subnet_group.db_subnets.name
    vpc_security_group_ids     = [var.db_security_group_id]
    parameter_group_name       = aws_db_parameter_group.db_parameter.name
    multi_az                   = true
    backup_retention_period    = "7"
    backup_window              = "23:00-23:30"
    apply_immediately          = true
    auto_minor_version_upgrade = false
    username                  = var.db_identifilter.user_name
    password                   = var.db_identifilter.password
    skip_final_snapshot        = true
}

まとめ

今回の記事では、

  • ELBAutoScalingを使って冗長性が高い構成についてterraformで実装しました。
  • ついでにRDSを使ってマネージドサービスを利用しました。

まだまだシンプルなサービス(サービスを利用したとしても代表的な設定)しか利用していませんが、

来週以降も記事を投稿していきます。

【AWS】 Well-Architectedについてのメモ

こんにちは。新卒3年以内に退職して世間的には白い目で見られているだろう駆け出しエンジニアです。

先日退職しまして、来週から新しい職場で働くのですが(どこいった有給)、正直勉強不足すぎて不安で一杯です。

不安はありますが、今年は1週間に1回はアウトプットを出すことが目標ですので今週もやります

すでに他の方が既出の`AWS`の記事ばっかりですが、車輪の再開開発という言葉に動じません。

メンタルだけが取り柄です。

まだまだ、次の業務で必要なスキルは身についていないですが、今年はGo言語も勉強していきたいです。

この記事で書くこと

  • AWSWell-Architeted Frameworkについて、Udemyと原文から学んだことを書きます。
  • 学んだことを踏まえて、前回の記事で作成した構成をまともな構成にしていきたいと思います。

    • マルチAZ構成
    • RDSの利用
  • Terraformの実装は次の記事に回しています。

文字が多くなってしまうので先にまとめ

  • Well-Architectedフレームワークは大局的な設計、運用のベストプラクティス集

    • 絶対に従わないといけないというものではない。自分たちのシステムにどこが適用できるか。何が足りていないかを把握するための指標
  • 継続的にWell-Architectedフレームワークに沿ったレビューを行い、適切なサービス選択とコスト最適化が必要になってくる

Well-Architectedフレームワークとは

原文はAWSの公式ページを参照してください 。

  • 平たくいうと、AWSでインフラを構築するときはこれは意識して設計しないとねという設計や運用に関するベストプラクティス集
  • 5つの柱で構成されていて、それぞれはトレードオフの関係となっている
  • 今勉強しているAWSアソシエイトの試験範囲もこのフレームワークが試験の範囲となっている

5つの柱

あくまで個人の解釈です

名前 説明(すごいざっくりとしたイメージ)
運用上の優秀性 構成変更や予期せぬイベント(障害)発生時に対してもなるべく自動化とか社内でレビューされた手順を考えていきましょうよ
セキュリティ 言葉の通り。アクセス追跡やアクションの追跡など全てのレイヤーでセキュリティ保護できるようにしましょうよ
信頼性 障害による、中断・停止と障害復旧による影響を軽減するインフラ構成。なるべく早く復旧できるアーキテクチャにしましょうよ
パフォーマンス効率 せっかくクラウド利用するんだから、要求されるリソースを満たす最小限のものを利用しましょうよ
コスト最適化 無駄にコストを払わず、最小限のコストでサービスを提供しましょうよ

どんな時に使えるのよ??

そもそもWell-ArchitectedフレームワークはプロであるAmazon Web Serviceのエンジニアたちの長年の経験から得たベストプラクティスです。

フレームワークを活用することで、プロたちのノウハウを既存環境のレビュー新規システムの設計へのインプットに活用することができるとされています。

要件定義~運用まで全てに利用する必要があり、改善を続けることがWell-Architectedフレームワークの根本ではないかとど素人ながら思います。

つまり、継続的なレビュー&改善(PDCA?)が必要になってくるのはないでしょうか。

f:id:wa_football_1120:20200110212617p:plain

先ほど説明した5つの柱についてざっくりと説明していきます。

運用上の優秀性(Operational Excellence)

AWSの公式(日本語版)では下記のように説明されております。

運⽤上の優秀性の柱には、ビジネス価値を提供し、サポートのプロセスと⼿順を継続的に向上させるためにシステムを稼働およびモニタリングする能⼒ が含まれます。

設計原則は6つあります。

  • 運用のコード化
    • IaC使っていこうぜ!!
  • ドキュメントへの注釈
    • IaCで作詞したコードに注釈とか、リソースに対してタグをつけよう!!
  • 高頻度で小規模は可逆的変更
    • 定期的に改善していこう!!
      • 結構無茶だろと思ったけど、IaC利用してコードのバージョン管理ができていれば実現可能なのではないでしょうか。
  • 障害の予測
    • 障害を自分たちで気づけるようにモニタリングをしましょうよ!!
  • 運用手順の頻繁な更新
  • 運用上の失敗の改善
    • 何か起きた時に対処できる運用手順はみんなでレビューして都度改善していこうよ!!
      • 一番難しいのでは。属人化の排除ってすごい難しいですよね。。

運用上の優秀性では3つのベストプラクティスの分野から成り立っています。

  • 準備
    • Cloud Formationとか使って、開発環境~本番環境まで自動的に構築できようにしていく
    • Cloud Watchを使って、モニタリングできるようにしていく
  • 運用

    • Cloud Watchとか使って、改善の指標となる値をモニタリングする
    • レビューされたランブック(運用手順書)プレイブック(障害対応手順書)を準備する
  • 進化

    • 運用フェーズで得られた指標から、継続的な改善を検討/実施していく
      • 障害発生から復旧までの時間とか
    • ランブックプレイブックの再レビューを実施する

セキュリティ

AWS上に構築したシステムのリスク評価(何がリスクか?どれだけの影響を与えるか?)とリスク軽減(リスクが発生する可能性・影響を小さくする)という観点です。

設計原則は7つです。

  • 強力な認証基盤の実装

    • 最小権限で、各リソース同士の通信には認証をかけましょう
  • トレーサビリティの実現

    • ログ監視/保管して、モニタリングできるようにしましょう
  • 全レイヤーへのセキュリティの適用

    • オンプレと同様に多層防御が必要ですよ
  • セキュリティの自動化

    • アクセスコントロールとかをIaCとかでテンプレート化して、改善できるようにしていきましょう
  • データ保護

    • データには暗号化をかけようね
  • データに人の手を入れない

    • 保管されているデータに対して、人が手動で直接アクセスしたり編集したりする必要性を無くす
  • セキュリティイベントへの備え

ベストプラクティスは5つの分野です。

オンプレと同様に、権限管理アクセス管理ログ監視多層防御データ保護(暗号化)IRを考える必要があります。

  • アイデンティティ管理とアクセス管理

    • IAMを利用して、AWSリソースにアクセスできるユーザおよびリソースを制限する
      • 権限はフルオープンにせず、アクセスが必要なリソースだけにPrincipalを使って最小限にする
  • 発見的統制

    • セキュリティインシデントが発生した際に、すぐに検知・状況把握ができる構成が必要になる
    • Cloud TrailログAWSリソースに対して実行された操作を記録する
    • Cloud WatchAWSリソース自体のログを記録・可視化する
    • Amazon GuardDuty(脅威検出サービス)を利用して、悪意のある動作や不正な操作を検知できる仕組みを作る
  • インフラストラクチャ保護

    • 多層防御の考え方を適用する
    • パブリックとプライベートの分離
    • WAFやIDS(マーケットプレイスでIDSがあるようです)の導入
  • データ保護

    • データ暗号化(S3やRDSなど)をして、素のデータが開示されるリスクを減らす
    • KMSを利用して、暗号鍵の管理や定期的なロテートを実施する
    • S3にデータを保管して、データがなくなってしまうことを防ぐ
  • インシデント対応

    • セキュリティインシデントに備えて、ファイルに対するアクセスや変更に対するログを記録する
    • インシデント訓練を実施して、インスタンスの隔離や自動化ができるか検証する
      • CloudWatchをトリガーとして、Lambdaを動作させるなど

信頼性

障害発生時に、中断・停止と障害復旧による影響を軽減するという観点です。

また、障害発生時にすぐに復旧できるかということも重要です。

可用性を高くすることは大事なのですが、何かあった時に適切に復旧できるかという考え方が必要なんですね。

設計原則は5つです。

  • 復旧手順をテストする
    • クラウド環境なんだから、本番環境と同じ構成を複製して、障害を発生させてテストしよう
  • 障害から自動的に復旧する
    • システムのKPIをモニタリングして、しきい値を超えた時にシステム側で自動的にアクションを起こせるようにしよう
  • 水平方向へのスケール
    • 垂直方向(リソース拡張)ではなくて、複数の小規模リソースを配置して単一障害点を無くしましょう
  • キャパシティーを予測しない
    • 需要(Webシステムで言えばアクセス)に対して、適切なスケーリングができるようにしましょう
  • オートメーションで変更を管理する
    • IaCを使ってインフラ構成に発生した変更を管理できるようにしよう

ベストプラクティスは3つの分野に別れています。

  • 基盤

    • オンプレで考慮点となるNW帯域コンピューティング性能(物理レベルの性能)AWS側の責任範囲
    • リソースに必要なストレージのサイズやリソースのサイズ(割り当てるCPUやメモリ)を適切に選択する
  • 変更管理

    • 需要の変化に応じて、リソースが自動的に追加や削除される対応ができているか
    • ログとリソースのモニタリングを実施して、KPIの設定やしきい値を超えた時のアクションを自動化する
    • 変更管理をログとして記録する
  • 障害の管理

    • 障害を検出して、自動的に復旧できているか
      • Cloud Watch
    • バックアップデータの保存先に耐久性の高いS3を利用する

パフォーマンス効率

システム要件を満たすためのコンピューティングリソースを効率化するという観点です。

AWSサービスの進化(新サービス)を柔軟に取り入れて、インフラの効率化が必要とのことです。

5つの設計原則があります。

  • 最新テクノロジーの標準化
    • 最新技術を簡単に取り入れるようにAWS側が頑張るから、積極的に取り入れてみてよ
  • 数分でグローバルに展開
    • 世界中にリージョンがあるから、システムを複数のリージョンに置けるよ
  • サーバレスアーキテクチャを利用
    • 自分たちでサーバリソースをなるべく持たない構成にしようよ
  • より頻繁に実験可能
    • 簡単にリソースを作ったり、消したりできるから比較テストを実施しよう
  • システムを深く理解
    • NoSQLとかストレージを使ってもいいけど、根本的な技術の理解は必要よ

そして4つのベストプラクティス

  • 選択

    • 1つのAWSサービスではなく複数のAWSサービスを利用する
    • 検討すべきリソースタイプは4つ
      • コンピューティング
        • EC2のリソースタイプ
        • Lambda
        • コンテナの利用
      • ストレージ
        • アクセス方式や参照/更新頻度から選択する
        • EBS
        • S3
        • Glacier
        • EFS
      • データベース
        • システムに対して適切なデータベースを選択する
        • RDS
        • Aurora
        • DynamoDB
        • Elastic Service
  • レビュー

    • AWSの新機能を取りれているか
  • モニタリング

    • パフォーマンスをモニタリングできるようにする
    • SQSLambdaを通じて、自動的にアクションを起こせるようにする
  • トレードオフ

    • どの部分を重視するのか
    • 整合性耐久性
    • レイテンシー容量
    • インメモリキャッシュElastiCacheCDNCloudFrontの利用を検討する

コスト最適化

不必要なリソースを削減したり、適切な料金選択を行って、需要対して最適なコストでサービスを続けましょうという観点

5つの設計原則があります。

  • 消費モデルを導入する

    • ビジネス要件に応じて使用するリソースの量を削減する
      • 例えば、会社の稼働日だけテスト環境と開発環境を構築して休日中には、リソースを削除する
  • 全体的な効率を測定する

    • ビジネスから得た利益とコストの差異を検証する
    • ROIを得れていないリソースの継続検討
  • DC運用のための費用を排除する

    • クラウドシフトしていって、DCの固定コストを排除していく
  • 費用を分析し、帰結させる

    • システムの利用状況とコストを分析してROIを透明化する
  • マネージドサービスの利用を検討する

4つのベストプラクティスの分野があります。

  • 費用認識

    • AWS Cost ExplorerAWS Budgetsを利用して、各リソースの利用状況とコストをモニタリングする
  • 費用対効果の高いリソース

    • Trusted Advisorを利用して、支払い過ぎているリソースがないか定期的にレビューする
  • 需要と供給を一致させる

    • Auto Scallingを利用して、需要(アクセス)に対して、リソースを増加したり削除したりする
  • 長期的な最適化

    • 新しいAWSサービスをウォッチして、既存のアーキテクチャのコストが下がらないかレビューする

最後に

自分の忘備録的に書いているものなので、長々と書いているメモになってしまいました。

(資料の座学だったのでとてもしんどかった)

解釈が誤っている部分もあるかと思いますが、そこは悪しからず。

次回は前回作成したリソースを、改善していきたいと思います。

【AWS】TerraformでS3バケットを構築してみる

新年あけましておめでとうございます

スキルゼロの監視オペレータ(今年からWebエンジニアにジョブチェンジします)です。

今年は厳しい1年になるかもですので、糞ほど勉強してスキル向上に努めて、より良い1年にしたいと思います。

この記事で何をするか

AWSの基本的なサービスや操作を理解するためにAWSアソシエイトを勉強しています。

ただUdemyの動画や本を読んでいてもつまらないなと思い、インフラ構成管理ツールのTerraformと平行で勉強している最中です

前回は、VPC構築〜EC2構築までをやっていましたが今回はS3を作成したいと思います。

大まかな記事の流れは下記です。

  1. S3の基礎知識
    • よくある本とかWebで入手できる内容を自分用にまとめたものです
  2. Terraformでバケット(パブリックアクセス無し)作成
    • AWS CLIを利用してファイルのアップロードから内容確認、ダウンロード(Pull)までを行います
  3. Terraformでバケット(パブリックアクセス有り)作成
  4. HTMLファイルをホスティングしてアクセスできる事を確認します。

この記事で得られるもの

  • S3って何か、基本的なサービスについて理解できる
  • Terraformを使って、S3バケットを構築することができる

S3の基礎知識

そもそもS3って?

AWSが、マネージド型で提供しているストレージサービスです。

S3以外のストレージサービスにはEBS,インスタンスストアEFSなどがありますが、今回のS3はオブジェクト型のストレージとなります。

各ストレージの特徴を簡単にまとめました。

ストレージタイプ AWSサービスの例 特徴 プロトコル
ブロック型 EBS、インスタンスストア 保存されるファイルやオブジェクトのデータを複数のブロックに分散する。頻繁かつ高速なアクセスが必要な場合の用途 SATA, SCSI
オブジェクト型 S3 ファイルに任意のメタデータを追加してオブジェクトとして管理する。作成済みデータに対するCRD(Create, Read, Delete)のみが可能 HTTP(S)
ファイル型 EFS ブロックストレージ上にファイルシステムを構築。複数クライアントからNW経由でファイルアクセスするデータ共有など。ファイルサーバ的なやつ NFS

何が特徴的なのか?

  • 高い耐久性(イレブンナイン)と可用性(99.99%)
    • ストレージクラスがSTANDARDの場合
    • S3作成時にリージョンを選択するが、S3は3箇所以上のAZに同期される
  • HTTP(S)を利用して、AWS CLIやWEB APIから操作が可能
  • 結果整合性モデルを採用している
    • 更新・削除の結果が反映されるまで時間がかかる(Eventual Consistency Read)
    • 新規登録は即時にデータが反映される(Consistency Read)

何が保存されるのか?

データ(オブジェクト)は指定したリージョンのバケット(データの入れ物)に保存されます。

  • バケット
    • オブジェクトを保存する領域。
    • バケット名はAWS内で一意にする必要がある。
  • オブジェクト

    • S3に格納されるデータ本体。
    • 各オブジェクトにはキー(オブジェクト名)が付与され、「バケット名+キー名+バージョンID」で必ず一意なURLが作成される
  • メタデータ

    • オブジェクトを管理するための情報
    • オブジェクトの作成日時やサイズなどのメタデータやアプリケーションが必要な情報(ユーザ定義)が保持できる
  • バージョンID

f:id:wa_football_1120:20200104102927j:plain

S3の機能

ここからはバージョン管理やライフサイクルなどのS3で使える機能群を簡単にまとめていきます。

アクセス管理

S3のバケット(もしくはオブジェクト)に対するアクセス管理には、3種類が存在します

ポリシーに関してはこちらの記事がとても参考になりました

  • バケットポリシー
    • バケット単位でアクセスを制御する
    • バケットに保存するオブジェクト全てに適用されるため、バケット全体的なアクセス制御をするときに有効
    • JSON形式でポリシーは記述する
 {
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"PublicRead",
      "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject"],
      "Resource":["arn:aws:s3:::examplebucket/*"]
    }
  ]
}
  • IAMポリシー

    • バケットポリシーと同じようにJSON形式で制御をかける
    • 特定ユーザへのアクセス権を設定する場合にはこちらを使った方が良さそう
  • ACL

    • バケット/オブジェクトごとに付与された対象のアクセスを制御できる

暗号化

S3に保存するデータを暗号化して保存することができる

  • サーバサイド暗号化
    • S3のサーバリソースを利用して格納データを暗号化
  • クライアントサイド暗号化
    • 暗号化のプロセスをクライアント(利用者)側で用意する
    • AWS KMSやクライアントが保持するマスターキーで暗号化する
      • オブジェクトのメタデータを元にどのキーで復号化するのかS3側で判別する

レプリケーション

バケット内のデータを異なる(もしくは同一の別AZ)のバケットレプリケーション(同期)することが可能

バックアップやBCP用途に使うことができる

バージョン管理

バケット単位で有効もしくは無効を管理する

ライフサイクル管理

バケットに保存されたオブジェクトの利用頻度に基づいてライフサイクルを管理できる。

ライフサイクル管理によって、

  • 古いデータはより安く保存できるストレージサービス(Glacier)に保存する
  • それよりも古いデータは削除する

といったことができるようになる。

  • 移行アクション

    • データの利用頻度に応じて、ストレージクラスを変更するアクション
      • 一定期間がすぎると利用頻度が低くなるオブジェクトをアーカイブとして保存するなど
  • 有効期限アクション

  • 指定された期限を超えたオブジェクトをS3から削除する
  • S3は保存容量に応じて課金されるため、不要なデータを削除することでコスト削減が見込める

f:id:wa_football_1120:20200104112009j:plain

ホスティング

静的コンテンツ(HTML, 画像や動画, Javascript)をバケット内に保管して、Webサイトとしてホスティングさせることができる。

独自ドメインホスティングする場合にはバケット名とドメイン名を一致させる必要がある

Terraformでバケットを作成する

本記事で扱っていない機能だとかオプションは公式を参照してください。

https://www.terraform.io/docs/providers/aws/r/s3_bucket.html

下準備

  • バケットポリシーも設定しいきたいので、IAMユーザを作成しておきます。
  • ポリシーは何もアタッチしていない状態。
aws iam list-users \
> --query 'Users[].UserName'
[
    Your User Name,
    "unauthorize-user"
]
aws iam list-attached-user-policies \
> --user-name 'unauthorize-user'
{
    "AttachedPolicies": []
}

パブリックアクセス無しのバケットを作成

作成するバケットの要件は下記とします。

  • ログ保管用とデータ保管用のバケット2つを作成する
  • バケットは共に外部(インターネット)に公開しない
  • バケットにCRDできるのは、作成してユーザ(上で言うところのYour User Name)のみ
    • unauthorize-userからはアクセスできないことを確認する

実装コード

ログ保管用バケット

  • リソースはaws_s3_bucketで作成ができる
  • bucketバケット名を指定する
    • 前述だが、グローバルで一意な名前でなければならない
  • aclバケットへのACLを設定
    • log-delivery-writeでログの書き込みができるはず
  • lifecycle_ruleでは、有効期限アクションを設定
##############################################
# Create Log  Backet for Private Bucket      #
##############################################
resource "aws_s3_bucket" "logging_for_sapmle_private_bucket" {
    bucket       = "logging-for-private-turedure-study-bucket"
    acl          = "log-delivery-write"

    lifecycle_rule {
        enabled  = true

        expiration {
            days = "180"
        }
    }
}

データ保管用のバケット

重複している部分の説明は割愛します。

  • versioningでバージョニングを有効化
  • server_side_encryption_configurationでサーバサイド暗号化を有効化
  • loggingで上で作成したバケットにログを記録するように設定
  • 別リソースaws_s3_bucket_public_access_blockでパブリックへオブジェクトが公開されることを防止
##############################################
# Create Private Bucket                      #
##############################################

resource "aws_s3_bucket" "sample_private_bucket" {
    bucket      = "private-turedure-study-bucket"

    versioning {
        enabled = true
    }

    server_side_encryption_configuration {
        rule {
            apply_server_side_encryption_by_default {
                sse_algorithm = "AES256"
            }
        }
    }
    
    logging {
        target_bucket = "${aws_s3_bucket.logging_for_sapmle_private_bucket.id}"
        target_prefix = "log/"
    }
}

resource "aws_s3_bucket_public_access_block" "for_sample_private_bucket" {
    bucket                  = "${aws_s3_bucket.sample_private_bucket.id}"
    block_public_acls       = true
    block_public_policy     = true
    ignore_public_acls      = true
    restrict_public_buckets = true
}

結果確認

  • terraform plan ~ terraform applyを実行後にバケットの一覧を取得してみると作成ができている
aws s3 ls
2020-01-04 16:20:39 logging-for-private-turedure-study-bucket
2020-01-04 16:20:56 private-turedure-study-bucket
  • 続いてファイルをアップロードしてみる
aws s3 cp ./sample.txt s3://private-turedure-study-bucket
upload: ./sample.txt to s3://private-turedure-study-bucket/sample.txt
aws s3 ls s3://private-turedure-study-bucket
2020-01-04 16:24:37         20 sample.txt
  • 下準備で作成したユーザ(unauthorize-user)がバケット内を参照しようとすると拒否される
aws s3 ls s3://private-turedure-study-bucket --profile terraform

An error occurred (AccessDenied) when calling the ListObjectsV2 operation: Access Denied

試しに作成したバケットの設定は上手くいってるみたいです。

下準備で作成したユーザがアクセスできるようにバケットポリシーを作成していこうと思います。

バケットポリシーを作成

新しくリソースaws_s3_bucket_policyを作成して、バケットポリシーをアタッチをしていきます。

  • bucketでアタッチするバケットを指定
  • policyには、ヒアドキュメントでJSONをインラインで記述
    • 作成したユーザ(unauthorize-user)がバケット内を参照できるように設定
resource "aws_s3_bucket_policy" "private_bucket_policy" {
    bucket                  = "${aws_s3_bucket.sample_private_bucket.id}"
    policy                  =<<POLICY
{
    "Version": "2012-10-17",
    "Id": "PrivateBucketPolicy",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::{作成したユーザのARN}"
            },
            "Action": "S3:ListBucket",
            "Resource": "${aws_s3_bucket.sample_private_bucket.arn}"
        }
    ]
}
POLICY
}
  • terraform planからterraform applyをもう一度実行して、S3のオブジェクトを取得ができている
aws s3 ls s3://private-turedure-study-bucket --profile terraform
2020-01-04 16:24:37         20 sample.txt

プライベートバケットの作成ハンズオンはここで終わり!

バケット(パブリックアクセス有り)作成

HTMLをホスティングしてインターネットから参照できるバケットを作成します。

先ほどとほぼ同じではあるが、作成するバケットの要件は下記とします。

ホスティング用のバケット

ログ保管用のバケットは先ほどとほぼ同じなので割愛します

  • websiteホスティングサービスを有効化
    • index_documentでデフォルトルートのオブジェクトを指定
    • error_documentで400番系のエラーが発生した際に表示するオブジェクトを指定する
  • バケットポリシーは、AWSの公式ドキュメントをそのまま設定
##############################################
# Create Public Bucket                       #
##############################################
resource "aws_s3_bucket" "hosting_bucket" {
    bucket = "hosting-turedure-study-bucket"

    versioning {
        enabled = true
    }

    acl    = "public-read"

    website {
        index_document = "index.html"
        error_document = "error.html"
    }
}

resource "aws_s3_bucket_policy" "hosting_bucket_policy" {
    bucket = "${aws_s3_bucket.hosting_bucket.id}"
    policy = <<POLICY
{
    "Version": "2012-10-17",
    "Id": "HostingPolicy",
    "Statement": [
        {
            "Sid": "PubcliReadForGetBucketObjects",
            "Effect": "Allow",
            "Principal": "*",
            "Action": ["s3:GetObject"],
            "Resource": "${aws_s3_bucket.hosting_bucket.arn}/*"
        }
    ]
}
POLICY
}
  • terraform apply後にindex.htmlとerror.htmlをバケットに配置
aws s3 cp index.html s3://hosting-turedure-study-bucket
upload: ./index.html to s3://hosting-turedure-study-bucket/index.html
aws s3 cp error.html s3://hosting-turedure-study-bucket
upload: ./error.html to s3://hosting-turedure-study-bucket/error.html
  • curlindex.htmlerror.html表示できることが確認できる
curl http://hosting-turedure-study-bucket.s3-website-ap-northeast-1.amazonaws.com/
<!doctype html>
<html lang="ja">
    <head>
        <title>Sample Hosting</title>
    </head>
    <body>
        <h1>This is Sample Hosting Page </h1>
    </body>
</html>
curl http://hosting-turedure-study-bucket.s3-website-ap-northeast-1.amazonaws.com/error
<!doctype html>
<html lang="ja">
    <head>
        <title>Sample Hosting</title>
    </head>
    <body>
        <h1>Sorry, error is occured</h1>
    </body>
</html>

まとめ

  • S3はAWSが提供しているマネージド型のオブジェクトストレージ
  • 操作にはHTTP(S)プロトコルとして提供している
  • ホスティングやログ保管、BCP対策を実現機能群が提供されている

新年一発目の投稿がこれで終わりです。

(実用的な記事を投稿するにはまだまだ文章力や知識も足りていなくて自己嫌悪ですね。。)