【学習記録】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でできることって何?
ビルド
テスト
- lintツールによるコードスタイルチェック
- ○○specでのテスト実行
デプロイ
- ビルドしてテストが通ったものを本番/stg環境にデプロイすることができるようになります
パターンによりけりですが、CircleCIと連携させて下図のような開発体制を構築することができます。
開発者がCommitしたら自動でテストを走らせて、Slackに通知させるだとか
masterとかdevelopであれば、本番/stg環境に自動デプロイするといったことができるようになります。
なんでCircleCI?
CircleCIはSaaS型のサービスです。
Jenkinsのように自前でサーバを組んだりする必要がなく、環境構築/運用コストが非常に低いことが魅力的です。
ビルド~デプロイの設定はymlファイル
で設定するので、職人が作って中身がブラックボックスになりにくいことも魅力的です。
ただ、デメリットもあり、インフラ ~ ミドルまでのあたりはCircleCIの持ち物なので、
何か障害が起きた時だとかは自分たち原因究明するのはまず不可能です。
CircleCIを使ってみる
CircleCIはGithub
もしくはBitbucket
と連携させることが前提としてあります。
そのため、利用するにあたってはどちらかのアカウント登録が必要になります。
料金体系
無料枠
と有料枠
があります。
無料枠
個人開発なので無料枠を利用します。
- 同時実行ジョブ(同時に実行できる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.2
はRubyのインストールだとかテストのコマンドがすでに定義されているパッケージになります。
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から追加されたexecutors
やorbs
を展開して出力してくれます。
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
でこけたようでした)
まとめ
CircleCIについて、少し学習をしてみました。
CircleCIは、SaaS型のCI/CDサービス
- CI(継続的インテグレーション)は、ビルドやテストを短期間で繰り返して品質を向上させていく取り組み
- CD(継続的デリバリー)は、インフラ環境への自動デプロイを含めてCIを実行していく取り組み
CircleCIの
config.yml
にコンテナイメージや実行するジョブを定義していく
二番煎じどころではない今更勉強したので、かなり情報が溢れています。
ただ2.1
については、DRYに書くための新機能(executors)があったりするので、
公式のリファレンスを確認した方がいいですね。
色々と進んできたら、masterブランチだけデプロイをするjobを含めて、
もう一回記事を書きたいと思います。
【読書記録】WebPackに入門してみた
先週記事をサボってしまいました。 今週は書きます。(とはいえ読書記録なので内容は薄いですが)
今回読んだ本
「Webpack実践入門」という本になります。 KDP(Kindle ダイレクト・パブリッシング)に出版されているようで、 ワンコインで購入できる技術書になります。
なぜ読んだか
今扱っているシステムでは、RailsのMVCで構築されており一部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 単体)にまとめて出力してくれます。
webpackを使うと何が嬉しいの?
メリット1 機能後にファイルをモジュール化できる
フロントエンド開発では、機能ごとにファイルを分割して開発することが主流です。 なんで機能ごとにファイルを分割するかと言うと、
- 可読性の向上
- どこで使われているかわからないコードをみるとすごい疲れますよね。しかも安易に消せないと言う。。
- 機能ごとに別れていれば、システムのどの機能を実装していると言う全体把握にも役立ちます。
- 開発作業の分担とテストがしやすくなる(=開発効率の向上)
- 機能ごとにファイルが別れていれば、コンフリやデグレを引き起こす可能性を大幅に削減できます
- 名前空間を生成できる
- モジュールの保守性を高められる
- リリースして、ある程度の期間が立って、「ここもう少しこうしたいな」と要望を受けたとしましょう
- ここで、1つのファイルから該当のコードを探すの比べたら、機能ごとに別れていた方が機能拡張も修正もしやすいですよね
メリット2 外部モジュールも利用できる
フロントエンド周りでは、vue
やreact
といったモダンなライブラリやフレームワークを利用する機会が大幅に多くなると思います。
webpackは外部モジュールを含めてバンドルすることができます。
メリット3 リクエスト数を減らせる
HTTP2.0が普及してきているので、メリットか分かりませんが、
複数のファイルがまとめられるため、リクエスト数が減ります。
メリット4 依存関係を解決したファイルを出力できる
webpackが依存関係を自動的に解決してファイルを主力するため、依存関係による不具合を減らすことができます。 小規模なシステムでは、依存関係を追っていくことができそうですが、 中+大規模なシステムになってくるとまず依存関係を目グレップで追っていくのは無理ですし効率がかなり悪いです。
webpackを試してみる
実行環境はこんな感じです。
srcディレクトリ配下のapp.js
がモジュールadd.js
とtax.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.jsonのscripts
フィールドにエイリアスと対応するコマンドを記述することで
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
の場合は、ファイルの圧縮やモジュールの最適化などがされるので、本番環境に適用する場合にはこちらを選択します
- webpackの動作モードで、
entry
- 上でwebpackは依存関係を解析して、自動的に解決すると書きましたが、その依存関係の解析を開始する始点になります
output
- バンドルの設定です。
- filename
- バンドルした結果のファイル名
- path
- バンドルしたファイルの保存先
モジュール作成
モジュールadd.js
とtax.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以外のファイルを同じようにバンドルできる形に変換するプログラムがローダになります。
TypeScript
やVueコンポーネント
もローダを介してバンドルすることが可能です。
冒頭のWebpackのイメージをもう少し細かく分離すると下のような形になります。
代表的なローダーの紹介と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を使うためには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.jsonのscripts
に記述していきます。
yarn run dev
、yarn 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)
異なるテーブル同士を結合して、欲しいデータを取得する方法
IN
、EXSITS
を使った少し複雑なクエリ
今の時代、ほとんどの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マッパーが不要になる
KVS
あとは他にも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 BY
とWHERE
を併用した場合のクエリの実行順序に注意
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 TRANSACTION
でCOMMIT
or ROLLBACK
を指定する
START TRANSACTION; INSERT INTO ・・・ UPDATE SET ・・・ UPDATE SET ・・・ COMMIT;
ACID特性
トランザクション処理はACID
を守らなければならない
Atomicity(原子性)
- トランザクションが終わった時、全て実行された状態(
COMMIT
)か全て実行されない状態(ROOLBACK
)になる - かっこよくいうとAll Or Nothing
- トランザクションが終わった時、全て実行された状態(
Consistency(一貫性)
Isolation(独立性)
Durability(永続性)
第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パターンとは
Javaで学ぶデザインパターン
には下記のように例えられています。
ある元となるクラスに対して機能を拡張していく際に利用できるデザインパターンになると思われる。
まず中心となるスポンジケーキのようなオブジェクトがある それに飾り付けとなる機能を一皮一皮被せていって、より目的にあったオブジェクトに仕上げていくのです。
正直、頭の中でお花畑が見えましたが、使うと何が嬉しいかについては、下記のように理解しました。
- 元となるクラスを継承して、飾り枠(
ConcreteDecorator
)に機能を拡張していく- 元となるクラスに対して変更が発生しない
- 飾り枠をたくさん用意して組み合わせること多様なパターンの機能拡張ができる
- 表示などをカスタマイズする際には最適なパターンになるかも
- 元のクラスに対してコードを追加していく訳ではないので、1つのクラスが肥大化していくことを防ぐことができる
ただ、ConcreteDecorator
自体が肥大化したり、似たようなクラスが
増えてくると言うアンチパターンと隣合わせと言うことは認識しておかなければなりません。
実際にコードを書いてみた系は、別の記事を参照してください。。
クラス図
一般的なDecoratorパターンのクラス図は下記のようになります。
Component
- 機能を追加するときになる元となる抽象クラス
ConcreteComponent
- Componentのインターフェイス(図で言うところの
method~
)を実装している
- Componentのインターフェイス(図で言うところの
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
でやってみようと四苦八苦中です。
今回は前回作成した構成をより冗長性が高い構成にしようという記事です。
(経緯)
- 12月の記事で、Terraformを利用したインフラ構築をやってみました。
- 前回の記事で、Well-Architected フレームワーク(以下から、W-A)についてまとめました。
- 本記事では、作成したインフラ構成を少しでも
W-A
に沿った構成に改善していきたいと思います。
得られた内容
- 冗長性を高めるためのインフラ構成案
- ELB、AutoScalingについての概要
- RDSの概要
- Terraformを利用した、
マルチAZ
、ELB(Elastic Load Balancer)
、RDS(Relational Database Service)
の構築
12月に作成したインフラの構成
- 単一のAZに1つの
VPC
を作成 - 作成した
VPC
内にパブリックサブネットとプライベートサブネットを1つずつ構築 - パブリックサブネット内に、Webサーバを想定したEC2インスタンスを1つ構築
- プライベートサブネット内に、DBサーバを想定してEC2インスタンスを1つ構築
- DBサーバには、EC2インスタンスからしかトラフィックを受け取らないように、セキュリティグループを設定
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 を利用する |
インフラ構成を図に示すと下記のようなイメージです。
まだまだシンプルで簡易的な構成ですが、入門編として見逃してください。
予備知識
今回の構成で新規に追加するELB
、AutoScaling
、RDS
について簡単にまとめていきます。
ELB
マネージド型のロードバランシングサービスです。
EC2と組み合わせて複数リソースへの不可分散やヘルスチェックができるようになります。
現状主要なELB
としてはALB(アプリケーションロードバランサ:レイヤ7で負荷分散)
とNLB(レイヤ4で負荷分散)
がありますが、以降はALB
を前提とした説明です。
特徴
複数AZにまたがって、トラフィックの不可分散をすることができる
パブリックサブネット、プライベートサブネットどちらにも配置ができる
マネージドサービスで、リクエスト量に応じてELB自体も自動的にスケーリングされる
主要な機能
ヘルスチェック
負荷分散の方式
SSL Termination
- ロードバランサ側で
SSL認証
を実施する機能 - ロードバランサに
SSL証明書
を設定することで、クライアント~ロードバランサ間はHTTPS
で、ロードバランサ~リソース間はHTTP
で通信を流すことができる- いちいち、リソース1個ずつに対して証明書をダウンロードすることが不要になってくる
- ロードバランサ側で
スティッキーセッション
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
Auto-Scaling Plan
- どのようにスケール(台数を増やしたり減らしたりする)かを設定する。下記の方法を複数組み合わせることもできる。
- 手動でのスケーリング
- 事前にスケーリングする必要が分かっている(キャンペーンやユーザ告知)した場合には手動でスケーリングすることができる
- Auto-Scalingグループの
最小数
、最大数
、希望する数
を調整する
- スケジュールに応じてスケーリング
- ある一定時間(お昼休みなど)だけスケーリングさせるといった設定ができる
- 予定アクションを定義することで実現する
- 手動でのスケーリング
- 需要に応じてスケーリング
- CloudWatchのCPU Utilizationを監視してしきい値を超えた場合といった設定ができる
- ポリシーを定義することで実現する
- どのようにスケール(台数を増やしたり減らしたりする)かを設定する。下記の方法を複数組み合わせることもできる。
リソース削除の設定
需要が減ったりすれば、余剰なインスタンスを終了(Terminate)する必要があります。 この時、どのインスタンスを削除するかについて設定することが可能です。
- OldestInstance/NewestInstance
- 作成されてから最も古い/新しいインスタンスから削除
- 後からAuto-Scaling作成した場合、オリジナルのEC2(スケール時のクローン元)が削除される
- 作成されてから最も古い/新しいインスタンスから削除
- OldestLaunch Configration
- 最も古い起動設定により起動しているインスタンスなどから削除
- ClosestToNextInstanceHour
- 次の課金が始まるタイミングが最も近いインスタンスから削除
- デフォルト設定
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構成に変更する
ことがメインです。
実際にコードを見てもらった方が分かりやすいかと思います。
組み込み関数のlength
とlookup
とelement
を利用して、冗長な書き方にならないようにしました。
例えばこんな感じで、変数で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_snapshot
をtrue
にしておかないと、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 }
まとめ
今回の記事では、
ELB
、AutoScaling
を使って冗長性が高い構成についてterraform
で実装しました。- ついでに
RDS
を使ってマネージドサービスを利用しました。
まだまだシンプルなサービス(サービスを利用したとしても代表的な設定)しか利用していませんが、
来週以降も記事を投稿していきます。
【AWS】 Well-Architectedについてのメモ
こんにちは。新卒3年以内に退職して世間的には白い目で見られているだろう駆け出しエンジニアです。
先日退職しまして、来週から新しい職場で働くのですが(どこいった有給)、正直勉強不足すぎて不安で一杯です。
不安はありますが、今年は1週間に1回はアウトプットを出すことが目標ですので今週もやります
すでに他の方が既出の`AWS`の記事ばっかりですが、車輪の再開開発という言葉に動じません。
メンタルだけが取り柄です。
まだまだ、次の業務で必要なスキルは身についていないですが、今年はGo言語
も勉強していきたいです。
この記事で書くこと
- AWSの
Well-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?)が必要になってくるのはないでしょうか。
先ほど説明した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 Watch
でAWSリソース自体のログを記録・可視化する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
- コンピューティング
- 1つの
レビュー
AWS
の新機能を取りれているか
モニタリング
- パフォーマンスをモニタリングできるようにする
SQS
やLambda
を通じて、自動的にアクションを起こせるようにする
-
- どの部分を重視するのか
整合性
か耐久性
かレイテンシー
か容量
か- インメモリキャッシュ
ElastiCache
やCDNCloudFront
の利用を検討する
コスト最適化
不必要なリソースを削減したり、適切な料金選択を行って、需要対して最適なコストでサービスを続けましょうという観点
5つの設計原則があります。
消費モデルを導入する
- ビジネス要件に応じて使用するリソースの量を削減する
- 例えば、会社の稼働日だけテスト環境と開発環境を構築して休日中には、リソースを削除する
- ビジネス要件に応じて使用するリソースの量を削減する
全体的な効率を測定する
- ビジネスから得た利益とコストの差異を検証する
- ROIを得れていないリソースの継続検討
DC運用のための費用を排除する
- クラウドシフトしていって、DCの固定コストを排除していく
費用を分析し、帰結させる
- システムの利用状況とコストを分析して
ROI
を透明化する
- システムの利用状況とコストを分析して
マネージドサービスの利用を検討する
4つのベストプラクティスの分野があります。
費用認識
AWS Cost Explorer
やAWS Budgets
を利用して、各リソースの利用状況とコストをモニタリングする
費用対効果の高いリソース
Trusted Advisor
を利用して、支払い過ぎているリソースがないか定期的にレビューする
需要と供給を一致させる
Auto Scalling
を利用して、需要(アクセス)に対して、リソースを増加したり削除したりする
長期的な最適化
最後に
自分の忘備録的に書いているものなので、長々と書いているメモになってしまいました。
(資料の座学だったのでとてもしんどかった)
解釈が誤っている部分もあるかと思いますが、そこは悪しからず。
次回は前回作成したリソースを、改善していきたいと思います。
【AWS】TerraformでS3バケットを構築してみる
新年あけましておめでとうございます
スキルゼロの監視オペレータ(今年からWebエンジニアにジョブチェンジします)です。
今年は厳しい1年になるかもですので、糞ほど勉強してスキル向上に努めて、より良い1年にしたいと思います。
この記事で何をするか
AWSの基本的なサービスや操作を理解するためにAWSアソシエイト
を勉強しています。
ただUdemyの動画や本を読んでいてもつまらないなと思い、インフラ構成管理ツールのTerraform
と平行で勉強している最中です
前回は、VPC構築〜EC2構築までをやっていましたが今回はS3を作成したいと思います。
大まかな記事の流れは下記です。
- S3の基礎知識
- よくある本とかWebで入手できる内容を自分用にまとめたものです
- Terraformでバケット(パブリックアクセス無し)作成
- Terraformでバケット(パブリックアクセス有り)作成
- 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
)
- 更新・削除の結果が反映されるまで時間がかかる(
何が保存されるのか?
データ(オブジェクト)は指定したリージョンのバケット(データの入れ物)に保存されます。
- バケット
オブジェクト
- S3に格納されるデータ本体。
- 各オブジェクトにはキー(オブジェクト名)が付与され、「バケット名+キー名+バージョンID」で必ず一意なURLが作成される
-
- オブジェクトを管理するための情報
- オブジェクトの作成日時やサイズなどのメタデータやアプリケーションが必要な情報(ユーザ定義)が保持できる
- バージョンID
S3の機能
ここからはバージョン管理やライフサイクルなどのS3で使える機能群を簡単にまとめていきます。
アクセス管理
S3のバケット(もしくはオブジェクト)に対するアクセス管理には、3種類が存在します
{ "Version":"2012-10-17", "Statement":[ { "Sid":"PublicRead", "Effect":"Allow", "Principal": "*", "Action":["s3:GetObject"], "Resource":["arn:aws:s3:::examplebucket/*"] } ] }
IAMポリシー
-
- 各バケット/オブジェクトごとに付与された対象のアクセスを制御できる
暗号化
S3に保存するデータを暗号化して保存することができる
- サーバサイド暗号化
- S3のサーバリソースを利用して格納データを暗号化
- クライアントサイド暗号化
レプリケーション
バケット内のデータを異なる(もしくは同一の別AZ)のバケットにレプリケーション(同期)することが可能
バックアップやBCP用途に使うことができる
バージョン管理
バケット単位で有効
もしくは無効
を管理する
ライフサイクル管理
バケットに保存されたオブジェクトの利用頻度に基づいてライフサイクルを管理できる。
ライフサイクル管理によって、
- 古いデータはより安く保存できるストレージサービス(Glacier)に保存する
- それよりも古いデータは削除する
といったことができるようになる。
移行アクション
- データの利用頻度に応じて、ストレージクラスを変更するアクション
- 一定期間がすぎると利用頻度が低くなるオブジェクトをアーカイブとして保存するなど
- データの利用頻度に応じて、ストレージクラスを変更するアクション
有効期限アクション
- 指定された期限を超えたオブジェクトをS3から削除する
- S3は保存容量に応じて課金されるため、不要なデータを削除することでコスト削減が見込める
ホスティング
静的コンテンツ(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
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
を作成して、バケットポリシーをアタッチをしていきます。
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
- curlで
index.html
とerror.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>
まとめ
新年一発目の投稿がこれで終わりです。
(実用的な記事を投稿するにはまだまだ文章力や知識も足りていなくて自己嫌悪ですね。。)