1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

画像はhtmlとcssのどちらで定義をすべきか

developer.mozilla.org

支援技術は背景画像を解析できません。画像がページの全体的な目的を理解する上で重要な情報を含んでいるのでいる場合は、文書上で意味を記述してください。

MDNのドキュメント内にある、cssbackground-imageプロパティには上記のような記載があります。
アイコンなどを含む画像をページに埋め込む際に、htmlのimgタグで指定するのか?それともcssbackgroundプロパティを利用するのか悩ましいところです。 SEOには詳しくはないですが、ある程度SEOにも絡んでくるのではないでしょうか。

企業によってどのように使い分けているか見てみた

企業を選ぶ基準は特にありませんが、個人的にアクセシビリティSEOを意識していそうだなと思ったところを見ています。

Google

Google SlideやGoogle Keep、コーポレートサイトを見ました。
ロゴ以外は基本的にcssbackgroud-imageプロパティを使っているようです。ただ、アクセシビリティを上げるために、ボタン代わりの画像はdiv要素を割り当てて、rolearia-lebelといったようなAttributeを付与しています。

Mozilla

Firefoxの配布ページとかを見ました。
こちらはばらつきがあり、PC用配布ページはbackgroud-imageプロパティを使っていたが、モバイルアプリ用配布ページはimgタグを使っていた。装飾的に使用されているimgタグalt=""という属性がつけていた。

アクセシビリティの考慮

WAI-ARIAW3C によって定められた仕様で、要素に適用できる追加の意味論を提供する一連の HTML 属性を定義しており、それが欠けているどのような場所でもアクセシビリティを向上させます。

developer.mozilla.org

momdo.github.io

WAI-ARIA role属性

定義済のロールの中から選択し付与することでhtml5のセマンティクスでは表現しきれない(もしくは代替要素を利用しているため表現できない)要素を意味を足すことができます。
ロールは、「抽象」「ウィジェット」「ランドマーク」などのいくつかの種類に分かれている。

aria-label

area-*属性のプロパティのひとつ!
「ラベルテキストが画面上で可視である場合、著者はaria-labelledbyを使用すべきであり、aria-labelを使用すべきでない」とのことなので画面上に表示されていないものに対して、それこそ例えばクリッカブルなアイコンなどに利用するべきと思います。 これで指定したラベルはスクリーンリーダーなどに読み上げられるそうです。

f:id:jrywm121:20200510092016p:plain

まとめ

基本的にはcssbackgroundプロパティを利用する運用で問題なさそうです。意味のある要素であれば、アクセシビリティを考慮したAttributeを付与したdivspanを用意して、その要素に対してcssで画像を付けてあげるというので自分の中では整理がつきました。
imgタグを使った場合に、装飾利用のみの画像であれば、alt属性を空と明示するべきと分かりました。

<div role="button" area-label="修正" class="modify"></div>
<img src="decoration.png" alt>
.modify {
   // url x軸の位置 y軸の位置 / x軸のサイズ y軸のサイズ 繰り返さない
   background: url('...') center center/15px 15px no-repeat;
}

JSの履歴APIを使って同一ページ内での疑似遷移でもブラウザバックできるようにする

タイトルを付けるのが難しかった…未来の自分が見返したときにこれでピンとくるのだろうか?
例えば、下記のようなケースで同一ページでもユーザからするとページ遷移したかのように感じるということがあります。

  • Ajax通信でデータをフェッチし、既存のデータをリプレイスする形で画面に反映させる
  • サマリーページと詳細ページを同一HTML内で返却していて、JSによって表示を切り替えている

このような場合に、何もしなければ、ページ内でのまるで遷移したかのような行動は反映されていないため、「戻ると想定していたページと違う」ことが予想されます。この行動をブラウザの履歴に反映させようというやつです。

JSのHistory APIを使う

実質、下記を見れば終わり!
どうしてMDNのサイトってこんなに見やすいのだろうか?神か?ありがとうございます!

developer.mozilla.org

pushstateで履歴を挿入する

履歴エントリ群はスタックのデータ構造を持っている。history.pushState()を使うことで、そのスタックに履歴エントリを追加することができる。
最初に出した例で言うと、Ajax通信が正常終了したときにそのAjax通信を再現できる情報を詰めて突っ込めば良さそう。キャッシュ可能なデータならば、Ajax通信のレスポンスヘッダにCache-Controlヘッダ付けて、privateかつmax-age: 120にでもしておけば、2分以内にブラウザバックを押したときに再度通信が走ることはないので快適かもしれません。

    let stateObj = {
        page: 2,
    };
    
    let title = `${document.title} - 2` 
    
    /**
     * JSDocの使い方間違っているけど、分かりやすいので…
     * @param {Object} stateObj エントリと一緒に保存できるオブジェクト(空でもOK)
     * @param {string} title 文書のタイトル(Safari 以外のブラウザはこのパラメータを無視)
     * @param {string} URL 絶対パス相対パス問わず
     */
    history.pushState(stateObj, title, "/product?page=2");

popstateをフックする

サマリーページと詳細ページを同一HTML内で返却していて、JSによって表示を切り替えている

最初に挙げたこのケースの場合には、history.pushState()を使わなくとも、history.popStateへのhookとURLの#(ハッシュ)を使えば実現可能。

  • 詳細への遷移の際に、URLの#(ハッシュ)にどの要素を展開しているかを残しておく
  • ユーザがブラウザバックをしたときに発生するイベント popstateをフックして処理を行う
// pureJS
window.onpopstate = function() {
    let currentState = history.state;  // 現在の履歴のstateを読み込む
}

// jQuery
$(window).on('popState', function () {
    if (location.hash != undefined) {
        $(location.hash).css('display', 'block');
        $('#summmary').css('display', 'none');
    }
})

感想

SPAのページ遷移もHistory APIを使って実現しているのかなと思いました。
windowオブジェクトにぶら下がっているオブジェクトを全然把握できていなくて、JSで実現できることは自分の想像以上に多くあるんだろうなとも感じますね。

JSをさらっと学んでみた

ちゃんと書いたことあるのが、GoとJava(とC#)だけの自分がJavaScriptを速習しました。というのも、業務で急に必要になってしまったからです。
以前少し触ったことがあるものの中途半端な状態で触るのも無責任と思い、下記を読んで、気をつけようと思った点をまとめてみました。
クライアントサイドJS前提ですが、FetchAPIとかクライアントサイドに特化した部分はここではメモしていない。ES5をメインで使わなくてはいけないシチュエーションですが、今後ES6を使っていけると信じてES6も併記していきます。

gihyo.jp

google.github.io

Variables

変数はデフォルトでundifinedという値を割り当たる

  • ある変数が宣言済みであるものの値を与えられていない場合
  • 未定義のプロパティを参照しようとした場合
  • 関数で値が返されなかった場合

変数の命名ルールはJavaと同じっぽい

  • 定数: SNAKE_CASE
  • 変数: camelCase
  • クラス: PascalCase

[ES6] ローカル変数は基本constを使って、再代入をする場合のみletを使う!varは癖がるので使わない!

  • varは宣言のスコープがブロックでなく関数
  • 変数の巻き上げによって、関数の最初に宣言されるような挙動となる
    • 宣言される前に既に変数は割り当てられているが、代入は宣言された行までされない
    • そのため、YAGNI原則に背き関数の一番最初に使用する変数を全て宣言する書き方がされる
    • 同じ変数名で再宣言できてしまう(Rustのshadowingに似ているというと聞こえはいい)
  • 「関数内でグローバル変数を書き換える」ような用途を除いては、原則としてvar命令を省略すべきではない。グローバル変数を宣言する場合には省略してもしなくても挙動は同じだが、混乱を防ぐためにvarで宣言すべき
// 定数は大文字+スネークケース
var TRAJA_LEADER_NAME = "chaka";

// クラス名はPascalCase
var BoyBand = function(leader) {
    this.leader = leader;
    this.greet = function() {
        // 変数の巻き上げによって、関数の最初に宣言されるような挙動となる(が変数の代入は行われないため「undefined」となる)
        console.log(greeting) // undefined
        var greeting = "Good morning! ";
        let selfIntroduction = "We are TravisJapan!"
        {
            var greeting = "Good Evening! ";
            const selfIntroduction = "We are SixTONES!";
        }
        // varのスコープはブロックではなく、関数なので後から宣言した方が優先される
        // let,const はブロックスコープ
        console.log(greeting + selfIntroduction);
    }
}

var leader;
console.log(leader); // 初期化していない変数のため「undefined」となる
var leader = TRAJA_LEADER_NAME;  // なんと同じ変数で再宣言できてしまう

// 変数名はCamelCase
var travisJapan = new BoyBand(leader); 
console.log(travisJapan.leader); // 「chaka」
console.log(travisJapan.KaraokeMovie); // 未定義のプロパティのため「undefined」となる

var result = travisJapan.greet();  // Good Evening! We are TravisJapan!
console.log(result); // 戻り値のない関数のため「undefined」となる

データ型

型名 データの持ち方 備考
number プリミティブ 64ビットの浮動小数点数 数値型はこの他に任意の精度を持つ整数型のBigIntが存在する(※1)
string プリミティブ 文字の集合、内部では0 個以上の 16 bit 符号なし整数の列として扱われる GoやJavaと違い参照ではない(※2)が、必要に応じて暗黙的にboxingされてStringオブジェクトとなる(※3)
文字列リテラルはダブルクォートよりもシングルクォートを優先して使う(※4)
[ES6] 文字列や複数行になるときや複雑な結合になる場合には、テンプレート文字列を利用する
boolean プリミティブ true/falseの真偽値 暗黙的なfalseを取る値がある
・ 空文字列
・ 数値の0, NaN
・ null, undefined
特殊値 プリミティブ null/undefined nullは明示的に空を表すときに使用する
array 参照 各要素に対してインデックス番号でアクセス可能なデータの集合 要素の追加/削除/反復などのメソッドを持つ
[ES6] 分割代入が便利(※5)
object 参照 各要素に対してkeyとなる名前でアクセス可能なデータ集合 JSにおいて連想配列=オブジェクト
[ES6] オブジェクト内の個々のデータをプロパティ、中でも関数が格納されたプロパティはメソッドと呼称する
function 参照 一連の処理をまとめたもの 宣言ではfunction命令よりもアロー関数が好ましいとされる(※6)

※1: JavaScript のデータ型とデータ構造 - JavaScript | MDN
※2: Goはstructだが実質byteスライスのwrapperという認識です(https://go101.org/article/string.html
※3: String - JavaScript | MDN
※4: https://google.github.io/styleguide/jsguide.html#features-strings-use-single-quotes
※5: 分割代入 - JavaScript | MDN
※6: https://google.github.io/styleguide/jsguide.html#features-functions-arrow-functions

// 初期化方法
const PI = 3.14;
console.log(typeof PI); // number
let charset = 'utf-8';
console.log(typeof charset); // string
let finished = false;
console.log(typeof finished); // boolean
const bucket = null;
console.log(typeof bucket); // object
let newSongOfSixtones;
console.log(typeof newSongOfSixtones); // undefined
const signalColor = ['red', 'yellow', 'green'];
console.log(signalColor instanceof Array);  // true
const Member = function(){};
member = new Member();
console.log(typeof member);   // object
const bark = () => { console.log("bowbow"); };
console.log(typeof bark);   // function

// [ES6]分割代入を使った典型的なswap
let x=1;
let y=2;
[x,y]=[y,x];
// [ES6]テンプレート文字列で直感的に変数を埋め込める
console.log(`x: ${x}, y: ${y}`); // x: 2, y: 1

control structures

条件分岐

等価のチェックには、===または!==を使用する、ただnullundefinedの両方をキャッチしたいときのみ== undefinedとする

  • ==はデータ型が違う場合にもJS側が変換して無理やり比較するため想定外の結果が得られることがある
  • Javaと違いObject.equalメソッドなど無いため、オブジェクトは単純にアドレスの比較になる
let n = null;
// nullとundefinedどちらも引っ掛けたいときのみ ==,!= を利用する 
if (n == undefined) {
    console.log('Null or Undefined'); // 出力される
    n = n === undefined ? 'undefined' : 'null'; // 3項演算子アリ!個人的には嬉しい!
}
// 基本は ===, !== で比較
if (n === 'null')  console.log('null'); // 1行であれば中括弧を省ける

// breakの後の1行開けると見通しが良い
const signal = 'red';
switch(signal) {
    case 'green':
        console.log('Go!');
        break;

    case 'yellow':
        console.log('Pay Attention!');
        break;
        
    default:
        console.log('Stop');
}

ループ

ループの制御構文としては、while, do ~ while, forに加えて、連想配列を順に処理するfor...in、配列やArrayなどEumerableなオブジェクトを順に処理するfor...ofがある
また、Go, Javaと同じようにラベル付きbreak/continueがあるのでネストが深くなったときには利用しても良さそう

// for, do while, whileの構文はJavaとほとんど変わらない
for (var i=0;i<10;i++) { }
do {} while (i < 5)
markLoop:
while (true) {
    while(true) {
        console.log(`i: ${i}`);  // iはvarで宣言されているのでアクセス出来てしまう(ES6以降はlet使う)
        if (++i > 11) {
            // ラベル付きBreakが可能で、外側のループに付与されたラベルをもとにbreakするので
            // 外側ループの無限ループに巻き込まれない
            break markLoop;
        }
    }
}
/** 上記の処理の出力
i: 10
i: 11 
**/

// for...inはオブジェクトのプロパティに対して反復する
var ExamScore = function(japanese, english) {
    this.japanese = japanese;
    this.english = english;
    this.getSum = function() {
        return this.japanese + this.english;
    }
}
var secondRegularExam = new ExamScore(87, 64);
for (var key in secondRegularExam) {
    console.log(`${key}: ${secondRegularExam[key]}`);
}
/** 上記の処理の出力
japanese: 87
english: 64
getSum: function() {
        return this.japanese + this.english;
    }
**/

// array, Map(ES6以降)などに関してはfor...ofを使用する
// for...inを使用するとユーザ定義の関数なども合わせて列挙されてしまうため
var fruits = ['banana', 'apple', 'melon'];
for (var e of fruits) {
    console.log(e);
}
/** 上記の処理の出力
banana
apple
melon
**/

例外処理

  • 例外処理はJavaと同じく、try...catch~finally
  • throwで意図的に例外発生させることができるが、Javaと違いExceptionオブジェクトを継承したものでなく、ErrorオブジェクトもしくはXxxxxErrorオブジェクトをthrowすることになる
    • 文字列等いろんな型をthrowで投げることが出来てしまう
const price = -1;
try {
    if (price < 0) {
        // 組み込みのError型がいくつか存在する
        throw new RangeError('price must be positive')
    }
} catch(e) {
    console.log(e);
} finally {
    // コネクションのクローズ処理など
}

オブジェクト指向プログラミング

主要な組み込みオブジェクト

// Stringオブジェクト
const str = '夢のHollywood'.substring(2, 11); // 内部ではboxing -> メソッド呼び出し -> unboxingが行われている?
console.log(`Type: ${typeof str}  Value: ${str}`); // Type: string  Value: Hollywood

// Numberオブジェクト
console.log(4.66665.toPrecision(4)); // 4.667

// Mapオブジェクト
const map = new Map([['hokuto', 'black'], ['kyomo', 'pink'], ['shintaro', 'green']]);
map.set('Jessy', 'red');
console.log(map.size);  // 4

// Dateオブジェクト
const today = new Date();
console.log(today.getFullYear()); // 2020

// RegExpオブジェクト
var re = new RegExp('[0-9]{4}-[a-zA-Z]+'); // オブジェクト生成時に評価
var re = /([0-9]{4})-([a-zA-Z]+)/; // 正規表現が不変のときはリテラル
var text = '2020-olynpic';
var newtext = text.replace(re, '$2_$1'); // olynpic_2020
console.log(newtext);
  • Object

    • すべてのオブジェクトの雛形
    • preventExtensions/seal/freezeといったメソッドを利用することで、Immutableにできる
  • Globalオブジェクト

    • グローバル変数やグローバル関数を管理するために、JavaScriptが自動的に生成する便宜的なオブジェクト
    • encodeURI()のようによく使われる機能などをグローバルオブジェクトに紐づく関数としてJSがデフォルトで提供している
    • ブラウザにおいてはwindowオブジェクトがグローバルオブジェクトにあたる、そのためwindow.location.hrefなどはlocation.hrefでアクセスできるし、グローバル空間において宣言したvar nwindow.nでアクセスできる
  • JSON.stringify(obj)でobjectをJSONとして吐くことができる

関数

  • 関数宣言
    • functionキーワードで宣言するよりも簡潔かつthisを束縛しないことからarrow関数で書くほうがより好ましい(特にネストした場合には)
      • アロー関数でない場合に、thisは関数の呼び出し方法によって変わる
        • コンストラクタであれば新しいオブジェクト
        • 「オブジェクトのメソッド」として呼び出された関数ではそのときのオブジェクト
        • 通常の関数呼び出しであればグローバルオブジェクト(strictのときにはundefined
      • アロー関数であれば、レキシカルスコープの this 値を使うため、通常の変数検索ルールに従う -> 同じスコープ内になければ外側のスコープを見に行く
// arrow関数
const moduleLocalFunc = (numParam, strParam) => numParam + Number(strParam);

// function命令
function moduleLocalFunction(numParam, strParam) {
  return numParam + Number(strParam);
}
  • ES2015より前はJavaScriptは引数の数をチェックしない
    • argumentsオブジェクトというのが関数配下でのみ使える特別な変数になっており、それを使って引数のチェックなどを行う
    • 可変長引数もargumentsオブジェクトを利用する
  • ES2015以降はデフォルト引数や可変長引数、名前付き引数が言語の機能として提供されるようになった
    • functiongetTriangle(base=1,height=1){...}
    • functionsum(...nums){}
    • getTriangle({base:5,height:4}))
  • ES2015以降は分割代入を利用してGoのように多値返却も可能に
functiongetMaxMin(...nums){
   return[Math.max(...nums),Math.min(...nums)];
}
let[max,min]=getMaxMin(10,35,5,78,0);
  • ES2015以降で、タグ付きテンプレート文字列を使えば、文字列を加工する関数を用意しておいて文字列定義と一緒に利用することが可能
  • Callオブジェクトとは、「関数呼び出しの都度、内部的に自動生成されるオブジェクト」で関数内で定義されたローカル変数を管理するための便宜的なオブジェクトであり、argumentsもこれで管理されている
    • 内部のCallオブジェクト、外部のCallオブジェクト、グローバルオブジェクトという順でスコープチェーンがある場合、内部から順番にオブジェクトに紐づくプロパティを参照していき、変数を解決する
  • クロージャも機能として提供されており、自分が定義されたスコープの変数をキャプチャすることができる

関数内のどこにでもvar文を使用して変数を宣言することができる。 そして、これらの変数は関数内のいかなる場所で宣言されたとしても、 その関数の先頭で宣言されたのと同じように動作します。

ES2015より前のオブジェクト指向

プロトタイプベースのオブジェクト指向
Functionにクラスとしての役割を与えている。
プロパティの定義は、自分自身を指し示すthisキーワードを使って変数を宣言する。
インスタンス化した後に動的にメソッドを追加することができるが、個人的には混乱を招きそうなのでsealメソッドを利用したほうが良さそう。

function Car(make, model) {
  this.make = make;  // プロパティの定義
  this.model = model;
  this.getName()=function(){
    return this.make + ' ' + this.model
  }  // メソッドの宣言
  Object.seal(this);  // 後からプロパティと削除/追加されないようにする
}

var car1 = new Car('Eagle', 'Talon TSi');

↓ 個人的な所感

インスタンスを生成するたびに、それぞれのインスタンスのためにメモリを確保する -> 無駄
prototypeプロパティに格納されたメンバーは、インスタンス化された先のオブジェクトに引き継がれる

JSで保守性を上げる

JSDoc

メソッド引数などに型の宣言が無い以上、ドキュメンテーションによる判断が重要になってくる。JSDocは下記のようにVSCodeIntelliJなどのIDEでサポートされているもので非常に有用。
また、node.jsで動くjsdocというツールでドキュメント生成することも可能。アノテーション一覧はこれ

f:id:jrywm121:20200510005657p:plain

namespace

いわゆるJavaのpackageのような仕組みが無いため、空のオブジェクトを利用して擬似的に名前空間を作成します。階層を持つことがより望ましいため、下記のような関数を用意すると良いようです。
共通利用する関数などはnamespaceを使うべき。

function namespace(ns) {
    let names = ns.split('.');
    let parent = window; // 最上位のwindowオブジェクト
    
// 'parent[v] || {}' とすることによって、parent[v]が定義されていないときのみ新たなオブジェクトを生成するという形
    names.forEach(v => {
        parent[v] = parent[v] || {};
        parent = parent[v];
    });
    return parent;
}

var common = namespace('com.example.app.common');
common.Util = function() { ... };

ファイル分割

以前までだと、別のファイルの関数を読み込むためには、html内でのスクリプトファイルの読み込みの順番を気をつける必要がありました。呼び出される側のファイルを先に定義しておかなければいけないためです。
ですが、ES2015以降ではモジュールという機能が登場したおかげでその状況は変わったようです。
本格入門では「執筆時点でモジュールをサポートしているブラウザーはありません」と記載がありますが、現在ではIE以外の主要なブラウザではサポートされているよう。
詳しい情報はここ参照。

private

JSではプライベートなメンバを定義する機能は無いが、クロージャを利用することで擬似的に再現できる

var Member = function() {
    // プライベートメンバはvarで定義する
    var _firstName;
    var _lastName;

    var _checkArgs = function(val) {
        return val.length > 0;
    }

    this.setFirstName = function(firstName) {
        if (_checkArgs(firstName)) {
            _firstName = firstName
        }
    }
    // 以下省略
}

感想

如何にグローバル空間を汚さないか、型の制約が無い状態で期待通りの動きをさせるかが肝なのだなと思った。
JS特有の表現で真偽値分かれるのは(例えば0であれば偽)、他の言語畑から来た人からすると分かりづらくてしんどいですね。
以前、軽くAngularを触ったときにはTypeScriptだったので、pureJSの良し悪しが今回わかってよかった。

apacheのmod_cacheでお手軽キャッシュ機構

ユースケース

下記のような条件下にあるが、どうしてもキャッシュを使ってみたい場合に、mod_cacheは有効なのではというお話。

  • Amazon CloudFrontAkamaiなどのCDNが諸事情によって使えない場合
    • お金をこれ以上かけられない
    • ログを特定のフォーマットで出力する必要がある
  • 既にアプリケーションの前段にApache(サーバ or コンテナ)がいる

キャッシュをさせる上で知っておく必要があること

developer.mozilla.org

上記の記事からいくつか掻い摘まんで読んでみます。
今回はブラウザのキャッシュではなく、プロキシのキャッシュ(共有キャッシュ)が主題になります。

キャッシュを制御する上で、必要になるのはCache-controlヘッダです。リクエストおよびレスポンスでキャッシュ機能に関するディレクティブを指定するために使用します。

このヘッダは、レスポンスに付与された時にはプロキシでどのくらいキャッシュを保持するか(もしくはしないか)ということを設定できます。逆にリクエストに付与した場合には、キャッシュの鮮度を指定して、キャッシュから取得するのかオリジンから取得するのかが分かれます。

このヘッダが付与されていないリクエストはクエリパラメータが付いているリクエストをキャッシュの対象にしないことがRFCに明記されており、Apacheのmod_cacheの実装も同じです。

  • Cache-Control: no-store
    • キャッシュストレージを一切利用しない = キャッシュに保存してはいけない
  • Cache-Control: no-cache
    • いちどキャッシュに記録されたコンテンツは、現在でも有効か否かを本来のWebサーバに問い合わせて確認がとれない限り再利用してはならない
  • Cache-Control: privateCache-Control: public
    • publicの場合は共有キャッシュでキャッシュさせて良い
    • privateの場合にはブラウザのプライベートキャッシュでしかキャッシュしない
  • Cache-Control: max-age=31536000
    • リソースの鮮度を保証するもの
    • 単位は秒数
    • responseTime + freshnessLifetime - currentAge の時刻までキャッシュを保持することになる
    • もしこのヘッダーが与えられない場合には、Expiresヘッダーを見に行くことになる
  • Cache-control: must-revalidateが付与されていると、キャッシュはリソースを使用する前に陳腐化の状態を検証しなくてはならない。ブラウザの再読み込みをしても、同じく検証が走る
    • キャッシュされた文書の有効期限に達すると
  • HTTP/1.0との互換性を担保したい場合には、pragmaヘッダーを利用する(完全な大体にはならない)

キャッシュ機構を扱う上で重要となるレスポンスヘッダにVaryがあります。キャッシュを提供する際に考慮すべきヘッダを指定します。指定したヘッダのvalueがキャッシュしたオブジェクトとリクエストで同一だった場合にはオリジンに新しくリクエストの発行を行わず、キャッシュを返却します。

例えばUser-Agentを指定した場合には、モバイルに対して誤ってデスクトップ版の画面を表示することがなくなります。

バイスごとにキャッシュさせる

例えば、Amazon CloudFrontであれば、内部でデバイス判断ロジックを持っており、デバイスタイプに基づいてキャッシュを返却します

Apacheのmod_cacheはデフォルトだとシンプルなプロキシキャッシュを提供するので、判断ロジックなどはこちらで設定してあげる必要があります。 

CloudFrontと同じような動作をmod_cacheにさせるためには以下のことを設定してあげる必要があります。

  • レスポンスヘッダにCache-Controlを付与する
  • リクエストヘッダに独自定義のデバイスタイプのヘッダをセットする
  • レスポンスヘッダにVaryヘッダを付与する(指定するヘッダは独自に定義したデバイスタイプ)

2つ目のリクエストヘッダの付与は、cacheハンドラが動く前に行う必要性があります。

mod_cacheはデフォルトだと、一番キャッシュ効率が良くなるように、設定ファイルへの記述順に関わらず一番最初に処理される作りになっています。

CacheQuickHandler offと宣言することで、一番最初に処理されるのを避けることが出来ます。日本語の方のドキュメントには載っていないためハマりどころです。

最低限の記述としては下記のようになるかと思います

# 独自のデバイスタイプ用ヘッダを付与する
    RequestHeader set x-custom-device-type pc
    SetEnvIf User-Agent "<regexp>" device_tablet
    SetEnvIf User-Agent "<regexp>" device_mobile
    RequestHeader set x-custom-device-type mobile env=device_mobile
    RequestHeader set x-custom-device-type tablet env=device_tablet

  <IfModule mod_cache.c>
      <IfModule mod_cache_disk.c>
        CacheRoot /var/www/html/cache
        # リクエストヘッダのCache-Controlを無視
        CacheIgnoreCacheControl On
        CacheDetailHeader on
        CacheEnable disk /
        CacheDirLevels 5
        CacheDirLength 3
   # mod_cacheが一番最初に処理を行わないようにする
        CacheQuickHandler off
      </IfModule>
  </IfModule>

  <Location "/">
    Header append Vary x-custom-device-type
    Header append Cache-Control max-age=60

# 以下は省略

システム監視の基礎についてまとめてみる

今度、社内でシステムの監視について話す機会があるので、自分用にまとめてみました。   

参考にした書籍は「入門監視」「Site Reliability Engineering」「k8sで実践するクラウドネイティブDevOps」です。 SRE Workbookの方も読んで後で追記したい。

監視の定義

あるシステムやそのシステムのコンポーネントの振る舞いや出力を観察しチェックし続ける行為である by Greg Poirier

監視とはアラートのことだけに関心があるのではなく、監視の中には多くの要素が含まれます。
例えば、メトリクスやログ、オンコール、ポストモーテム(振り返り)、統計などが挙げられるでしょう。

監視の必要性

必要な理由は多く挙げられますが、主なものは下記になります。

  • 長期的なトレンドの予測
    • DBのデータボリュームはどのくらいのペースで増えているか?
    • 日次のアクティブユーザの増加ペースは?
  • 時間や、実験グループ間での比較
    • DBコネクションプールの実装はdbcp2とTomcat JDBC Poolどちらの方が高速か
    • サイトの速度は先週と比べて落ちているか
  • アラート
    • 何かが壊れている(あるいは近い未来に壊れる)ので、誰かが確認するべきだということを通知する
  • ダッシュボードの構築
  • アドホックな振り返り分析の進行
    • レイテンシが急上昇した際に、他になにか同時期に生じていなかったか
    • エラーが急増したときに、バックエンドのレイテンシはどうだったか

書籍やブログ等でメインで取り扱われることは少ないですが、YelpやRedditなどはビジネスにおけるKPIもアプリケーションのメトリクスから取得してモニタリングを行っているそうです。例えば、検索実行数やユーザのサインアップ等です。都度ログを収集することで可視化できますが、リアルタイムで描画することで判断が早く行えるという利点があるようです。

GoogleではSLO(Service Level Objective)を計測し、エラーバジェットという指標を設けることで、運用と開発チームの対立を避けるようにしています。運用チームは稼働時間に対する目標があり、開発チームは新機能などに関する目標があるケースが多いかと思います。つまり、この対立はイノベーションの速度とプロダクトの安定性の間に生まれるものです。
Googleでは、SLOを99.995%(4.5ナインズ)に決めたら、0.005%は損失可能な信頼性となり、四半期のうちこの値に達するまでは新しいリリースをPushできるという仕組みを持っている(現実はもっと複雑な条件分岐によるアクションを持つ)。 定量的な指標を持つことで交渉から政治を取り除くことができます。
※ SLOの例:GETリクエストの99%が100ミリ秒以内に成功する

f:id:jrywm121:20200324230026p:plain

監視の立ち位置

企業がスケールするとメンバは特定の役割を持つことが普通になってきますが、監視は他の仕組みから孤立したものではなくサービスのパフォーマンスのために重要になる要素になります。知らないシステムについての監視(特に高度な監視)は難しいものです。例えば、「アプリケーションが動いているか」どうかを監視したいと思ったときに、何をもって「動いている」かをアプリケーション開発者以外の方は定義することに労力を要するでしょう。HTTP 200が返ってくればいいのか?レスポンスに特定の文字列が含まれているべきか?レイテンシはいくつ以下であるべきか?気になるのは平均なのかテイルレイテンシなのか?高度な監視を行うために検討が必要になります。
役割としての監視はアンチパターンとなっており、監視はスキルとしてチームメンバ全員が持つべきものと言えます。

サービスの健全性はマズローの欲求階層と同様に、サービスとして一通り機能するための最も基本的な条件から、サービスの方向性を能動的にコントロールするような、高いレベルの機能に至るまでの段階に分類できる。
その階層の中で一番下、最も基本的な必要条件がモニタリングとなります。

f:id:jrywm121:20200324230142p:plain

監視サービスの構成要素

専門化されたコンポーネント疎結合で組み合わせて監視プラットフォームを作ります
「あるツールのやり方が合わなくなってきた」ときに、監視プラットフォーム全てを取り替えるのではなく、そのツールだけ削除して他のもので置き換えればよいというわけです

  • データ収集
  • データストレージ
  • 可視化
  • アラート
  • 分析

データ収集

ブラックボックス型/ホワイトボックス型監視

ユーザーが目にする外部の振る舞いをテストするのがブラックボックス監視で、システムの内部によって公開されているメトリクスに基づく監視がホワイトボックス監視になります。

f:id:jrywm121:20200324230737p:plain

ブラックボックス監視には限界があります。

  • 「何が」壊れているのかは分かるが、「なぜ」壊れているのかはわからない
  • 事後対応であり、問題が発生した後にしか通知しない

ですが、ブラックボックス監視は不要というわけではありません。 DNSレコードの誤りやネットワーク分断、ルータの設定ミス、クラウドプロバイダの機能停止などは内部のメトリクスでは拾うことが出来ません。そのため外部の場所からサービスの可用性を監視する必要性があります。サードパーティのサービスが多くあり、MaaS(Monitoring as a Service)と呼ばれれます。
主要なサービスを下記に示します。

オブザーバビリティををより向上させるためには、外部だけでなくアプリケーションからデータを収集する必要があります。それがホワイトボックス監視です。ホワイトボックス監視は、「なぜ」壊れたのか?を答えてくれるものであり、問題の予測に役立ちます。
代表的なホワイトボックス監視において収集するデータとして、ログとメトリクスがあります。

ログ

メトリクスより多くの情報を持てるので、情報を抽出するためにはなんらかのパースが必要になります。パースさせるために構造化してログを収集できるようにするべきです。
例えば、プレーンテキストでログを吐くのではなくJSONで出力させることも検討するほうが良いということです。Cloud Watch InsightやStackdriver Loggingでのフィルタリングが楽になります。下記はCloudWatchInsightでのクエリ例です。

f:id:jrywm121:20200324232014p:plain

ログは役に立つが、ログに何を吐くかはアプリケーションの開発時にプログラマが判断します。 そのため、答えられる疑問や検出できる問題は事前に予想できるものに限られてしまうという側面を持ちます。事前に予想できないものまで検知するためにログの量を増やしてしまうと、雑音が増えてしまい問題の特定が難しくなったり、ログを蓄積するために使用している従量課金のマネージドサービス(例えば AWS CloudWatch)での課金額が高くなってしまうデメリットがあります。

メトリクス 

ログの弱点をカバーするような特徴を持つのがメトリクスです。メトリクスはすべて数値であるため、ログと違い計算や統計を行うことができます。メトリクス値は大きく2つのタイプに分かれます。

  • カウンタ

    • 増えていく(または0にリセットする)ことしかできない
    • リクエストの数や捕捉したエラーの数の測定に優れている
    • 自動車での走行距離計に例えられる
  • ゲージ

    • 増減ともに可能
    • CPU使用率のように継続的に変化する量や他の数量との率を表すのに便利
    • 自動車の速度計に例えられる

アラート

アラート疲れにならないように設定する必要があります。まず、アラートにメールを使うのをやめた方が良いです。あまり見られておらず機能していないことが多いです。そして事象の深刻度に合わせて、どの媒体に通知するかを分けるべきです。

  • すぐに応答/アクションが必要なアラートはPagerDutyやSMSを使う
  • 注意は必要だが、すぐにアクションは必要ないアラートはslackなどの社内のチャットルームに送る
  • 履歴や診断のために保存しておくアラートはログファイルに送る

f:id:jrywm121:20200324233242p:plain

また、うるさいアラートはオンコール担当の疲弊にも繋がるし、いずれ無視されるものなってしまう可能性が高いです。モニタリングやアラートのルールを作成する際には、以下の質問をしてみると良いでしょう。

  • ルールが検出する状況は、そのルールなしでは検出されない状況にあるか?緊急で対応が可能か?その対応は自動化できないか?
  • アラートは間違いなくユーザーに悪影響が生じていることを示すか?

アラートには手順書(runbook)へのリンクも一緒に含めるべきです。

統計

平均値での可視化/アラートには落とし穴があります。特に一方方向に大きな偏りがあるデータセットの場合は気をつける必要があるでしょう。
例えば、静的ファイルの配布もJavaアプリケーションで行っている場合かつそのリクエストが多い場合には、例えば特定のパスが遅くなっていても、レイテンシの小さいリクエストのせいで平滑化されて見えなくなってしまいます。

そのためテイルレイテンシをSLOに掲げるところも多いです。例えば、「95パーセンタイルを3秒以下」と定めます。
パーセンタイル(分位数)は昇順に並べ、最小値から数えて何%かというものです。

f:id:jrywm121:20200324233439p:plain

監視の始め方

監視についての取り組みはミッション駆動でなくツール駆動になりがち!後者が前者より効率高くなることはありません。
監視は単一の問題ではない = つまり単一のツールでは対応できないので、成熟した環境においては汎用的なツールと専門的なツールを組み合わせて使うべきです。

ツールは賢く注意深く選ぶべきで、ツールが必要なら増やすことを恐れなくてもよいです。むしろツール統合の名のもとに必要性に見合ってないツールを使うのは良くありません。ツール選定において、使えるのであればSaaS使った方が良いと思われます。ほとんどの場合はそれで事足りるし、結果的に安くなります。Run Less Softwareのphilosophyにも通じます。

一番関心があるのは「ユーザに影響があるかどうか?」です。そのためユーザ視点での開始から始めるのが基本と思います。このことで個別のノードの監視から開放されます。 例えば、DBサーバのCPUが上がっていてもユーザに影響が無いのなら影響になるでしょうか?
監視をどういった指標で扱うかに関しては既にパターンが存在します。

The Four Golden Signals

  • レイテンシ

    • リクエストを処理してレスポンスを返すまでにかかる時間
    • 処理に成功したレスポンスと処理に失敗したレスポンスでレイテンシを区別することが重要
    • 極めて早く返される500エラーが混じるとノイズになる
    • 一方で低速なエラーは高速なエラーよりも厄介なので、エラーをフィルタリングして外すのではなく、分けて追跡するのが大事
  • トラフィック

    • システムに対するリクエストの量
    • 通常は毎秒のHTTPリクエスト数だが、リクエストの性格によって異なる。KVSであれば、毎秒のトランザクションや取得数を計測することになる
  • エラー

    • 処理に失敗したリクエストのレート
    • 失敗には種類があり、例えば下記の3つが挙げられる
      • 明示的な場合(HTTPの500番台)
      • 暗黙的な場合(HTTP200で返却されているものの内容に間違いがある)
      • ポリシーによる場合(レスポンス1秒以下にすると約束しているのであれば、1秒以上かかったリクエストはエラー)
  • サチュレーション

    • サービスがどれだけ「手一杯」になっているかを示す
    • 最も制約のあるリソース(メモリやI/O)に重点をおいてシステムの利用率を計測
    • 近々発生しそうな予測も可能(例えばDBがディスクの空き容量を圧迫し、4時間後にハードディスクの空き容量が無くなりそう等)

RED Method

WeaveWorksが全てのマイクロサービスで共通して取得する3つのメトリクスを定義したものです。ソフトウェアからサチュレーションを取得するのは難しいため、The Four Golden Signalsからそれを除いた3つを挙げています。

Requests-Errors-Duration

  • 1秒あたりの受信リクエスト数
  • エラーを返したリクエストの割合
  • リクエストの持続時間(レイテンシ)

サービスが示している性能と、それをユーザが体験している状況について関心を持つ監視です。元記事はこちらです。

USE Method

NETFLIXのPFエンジニアが考案した性能に関する問題の分析とボトルネックの発見に役立てることを意図したアプローチです。関心はサービスではなくリソースです。

Utilization-Saturation-Errors

  • リソースがリクエストの処理でビジーになっていた平均時間、またはリソースキパシティに対する現在の使用量
  • リソースが過負荷となっている程度、このリソースが利用できるのを待つリクエトが格納されているキューの長さ
  • リソースに対するオペレーションが失敗した回数

元記事はこちらです。

まとめ

真っ先にやらなくてはと思ったのは下記です。副次的な良い効果が得られそうとも思いました。

  • 手順書のリンクをアラートに含める
  • アラートの精査を行う
  • レイテンシをレスポンスの成否で分ける
  • パーセンタイルを利用してレイテンシを計測する

【読書メモ】 k8sで実践するクラウドネイティブDevOps

読書メモ

www.oreilly.co.jp

↑の書籍の自分用読書メモ
現在の知識からの差分もしくは知っていても整理できていなかった部分を中心に
途中、自分で調べた補足なども入っているので本の内容でない部分もあり

chapterごとに気になった点

3つの革命

  • クラウドの創造
  • DevOpsの黎明
    • 20世紀中頃、ソフトウェア開発もコンピュータの運用も非常に専門性が高く重なり合う部分が無かった
    • 新機能をリリースしたい開発者と安定性・信頼性を上げたい運用者は対立しがち
    • クラウドの登場で状況が変化
      • システムの運用に必要となる専門事項(障害からの復旧、バージョンアップグレード)をシステムの設計、アーキテクチャ、実装から切り離せない
      • システムは自社開発のソフトウェアだけで完結せず、ソフトウェアを書く人はシステム全体との関連性を理解する必要がある。運用者はソフトウェアが機能する仕組みを理解する。
    • クラウドが極めて大規模であり、DevOpsの動向はコラボレーション型でコード中心の性質を帯びていることから、運用はソフトウェアの問題へと変容している
  • コンテナの到来
    • これまでの状況
      • 構成管理を使うためにはあらかじめ、言語によって異なるランタイムを入れる必要がある
      • 仮想マシンイメージは重くて運用に適さない
      • 運用の観点からすると、こうした多種多様なパッケージを管理するだけでなく、それを実行する一連のサーバも管理する必要性がある -> 大変
    • SeaLand社はコンテナを用いて商品を従来より遥かに安く船舶輸送することによって成功を収めた。これをテクノロジ業界が取り入れたものがコンテナ
    • 仮想化されたワークロードは同等のコンテナと比較して実行速度が30%遅くなる(それほどコンテナが軽量かつ効率よくリソースを使用しているということ)
    • 運用チームとしても様々な種類のマシン/アーキテクチャ/OSを保守する代わりに、コンテナオーケストレータを実行するだけで良い
    • Kubernetesは、自動化、フェイルオーバ、集中ロギング、監視など、非常に優秀なシスアドが行うであろうことを実行する
    • クラウドネイティブの特性
    • 従来の運用の大半は他のチームに移譲するべき
      • そのためにはDevOpsエコシステムを構築およびサポートする中央チームが必要
      • チームの機能は運用ではなく開発者生産性工学(DPE)
    • 純粋なDevOpsモデルは中小でこそ機能するが、組織が成長するにつれ、インフラや信頼性に関する専門家が中央チームに引き寄せられていく
      • ソース:The human scalability of “DevOps” - Matt Klein - Medium
      • 75人を超える頃には確実に中央インフラチームが設置され製品チームに必要な共有の基盤を構築し始める
      • それでもいずれ回らなくなってくるので、そのときはSREを個々の開発チームに配属する
    • ソフトウェアエンジニアと運用エンジニアの明確な区別はなくなる

Kubernetes環境の選択

  • Kubernetes自体の運用については取り扱わないので、知りたい場合には別書籍読んでね
  • コントロールプレーン
    • kube-scheduler
      • 「全てのpodの情報を取得しNodeにassignされていなければする」という挙動を取っていると思われがちだが、実際にはControllerがactualとdesiredとの差分を見てqueueに追加、schedulerは継続的にそれをpopし配置する
      • How does the Kubernetes scheduler work? - Julia Evans
    • cloud-controller-manager
      • kubernetesのリリース周期とクラウドベンダー側の対応の周期が合わないため、kube-controller-managerから切り出した
  • セルフホスティングは特別な理由がない限りやめておけ!!辛いぞ!!!
  • 近年では、「納品後すぐに稼働できる状態で引き渡される」というターンキー方式が成長してきている
  • Run Less Software(=実行するソフトウェアを減らす)
  • ↑の哲学に従い、マネージドKubernetesを使用することを推奨
  • 基本はGKE、もし単一クラウドプロバイダーに縛られることなく24時間のテクニカルサポートを受ける場合にはHeptioがおすすめ

リソースの管理

  • CPUをlimit以上に使おうとするときにはスロットル処理の対象となる
  • gRPCのヘルスチェックには、標準のみだと難しいのでgrpc-health-probeツールを利用する
  • コンテナが起動していれば、podのステータスはRunningになるが、READY列はカウントされない
  • ファイルベースのReadiness Probeはトラブルシューティングに便利
    • 例えば、/tmp/healthyのファイルを削除することで、Serviceから除外する
    • (ただし、labelコマンド使った方が楽なんじゃないか説)
  • Readiness Probeが成功を返す状態がminReadySeconds続かないとServiceにつながらない(デフォルトはゼロになっている)
  • namespaceでネットワークトラフィックを管理する場合にはNetwork Policy使う
  • ResourceQuotaでnamespaceごとにリソース量を制限できる
  • LimitRangeリソースで、すべてのコンテナについてデフォルトのリソース要求と制限設定できる
  • 優れた経験則
    • ノードのサイズは典型的なpodを少なくても5つ実行するのに十分なサイズ
    • 取り残されたリソースの割合を10%程度以下に維持する
    • 大きいノードの方が費用対効果が高い(のでノードあたりのpod数は10個から100個目標にする)
  • Jobリソースを削除するためにTTLを設定できる
    • まだアルファなので代わりにThird Partyのkube-job-cleanerが使えそう
  • Kubernetesのannotationに所有者情報を入れるべし
    • 組織のドメイン名をprefixに付けるべき
    • example.com/owner: "Customer App Team"
  • 幾らかの割合でプリエンティブインスタンスを使うようにすることで費用削減
  • スケジューラはノード間でpodを移動しないため、アンバランスな配置になってしまうとが多い

クラスタの運用

  • ベンダやサービスがCNCFの期待するCertified Kubernetes標準を満たすかを示すマーク及びロゴがある
  • Sonobuoyを使って「クラスタが適切に設定され最新状態である」ことを確認できる
  • k8sguardはクラスタの問題をチェックしてくれるツールだが2年動きが無いらしいの代替のツール有れば欲しい
  • kubernetesのChaos Engineering
    • Chaoskube: ランダムにpodを落とす
    • kube-monkey: 対象となるDeploymentの何%までを落とす
    • powerfulseal: ほとんどのシナリオを網羅できる

Kubernetesの強力なツール

  • kubectlの自動補完はTABキーを使って行う
    • 出てこない場合には、有効になっていない可能性があるので、kubectl completion -hを打ってヘルプ見る
    • (…自動補完の設定してたものの、どうやって補完するんだ…?まあいいや、kube-prompt使おーとなってて完全に思考停止だった)
  • kubectlには--watchオプションがあり、ステータスが更新されると更新情報が表示れる
  • 基本、命令的モード(create,edit...)は使うべきでは無いが、マニフェストの生成役に立つ。--dry-runと-o yamlを使う
  • kubectl diffは良いぞー(分かる)
  • kubectl logsのオプション
    • tailで直近のみに絞り
    • --followでストリームとして随時表示
  • kubespyによってリソースの細かい時系列イベントの監視が出来る
  • busyboxと--rm、--restart=Neverを用いて調査できる
    • alias bb=$kubectl run busybox --image=busybox --rm -it --restart=Never$
  • バイナリのみ配置した軽量コンテナで普通に色々なコマンド叩きたいことがある。そのきはdockerビルド時にマルチステージビルドの要領でBusyboxコンテナから/bin/busyboxを持ってくる
    • COPY --from=busybox /bin/busybox /bin/busybox
  • squashというツールでライブデバッグできる
  • kubectl v1.14から、--selectorフラグを使ってラベルセレクタに一致する複数podのログを取得できるように

コンテナの実行

  • ダイジェストを使ったイメージ指定が確実
    • cloudnative/demo@sha256:${ダイジェスト}
  • 非rootとしてコンテナを実行
    • root実行はprinciple of least privilegeに反する
    • バグを利用して悪意のあるユーザがプログラムを乗っ取ることも考えられる
    • securityContext.runAsUserで実行ユーザのUIDを指定
      • Linuxシステムの多くでは、最初の非rootユーザにUID1000が割り当てられるめ、一般的にコンテナのUIDは1000以上の値を選択(ユーザが存在していなくても良い)
      • Dockerfile内で実行ユーザを指定できるが、runAsUserフィールドを設定するが良い
      • runAsUserで指定したUID → Dockerfileで指定したUID → root
      • セキュリティを最大限に高めるためにはコンテナごとに別のUIDを選択する必要がるが、同じデータボリュームにアクセスする場合は同じの方が良い
  • コンテナが自分自身のファイルシステムに書き込むことを防止すreadOnlyFilesystemを使うのもよい(ファイルの書き込みをしないコンテナのみ)
  • setuidバイナリが含まれる場合には、実行ユーザをroot以外にしていてもroot権限獲得する可能性がある。これを防ぐには、allowPrivilege Escalationをfalseにる。
  • コンテナのデフォルトのcapabilityはかなり寛容なので、最小権限の法則を鑑みると必要に応じてdrop, addする必要がある
    • セキュリティを最大限に高めるためには、全てのcapabilityを削除し、必要に応じ特定のcapabilityのみ追加する
  • podレベルでのsecurityContextの設定が可能なので、そこで権限昇格を無効にし、てのcapabilityを削除するのがベスト
  • クラスタレベルでpodのcapabilityを設定することも可能で、それにPodSecurityPolicyを使用する
  • プライベートレジストリからImageをpullする場合には、imagePullSecretsフィールドを使って設定する

Podの管理

  • kubectl get pods -l app!=demokubectl get pods -l app in(staging. production)のようなラベルクエリを書くことも可能(Serviceリソースは等価クエリのみ)
  • LabelとAnnotationの違い=リソースを識別するしないか(どちらもkeyvalueのメデータ)
    • Labelの目的は、関連するリソースのグループを選択すること
    • Annotationは識別には用いられず、Kubernetesの外部のツールやサービスが利用る
  • PodAffinityはスケジューラの自由を制約するため、アプリケーション間でトレードフが発生する -> 切り札として使うように
  • TaintとTolerationは特定のpodが特定の種類のノードの問題を許容できるようにすこともある
    • Nodeでネットワークが利用できなくなったとき、Kubernetesnode.kubernetesio/unreachableというTaintを自動的に追加する
    • 通常はNodeのkubeletがすべてのpodをnodeから退去させる
    • ネットワークが合理的な時間内に復旧することを期待して、一部のpodはそのまま実させておきたいケースがある
    • これを実現するためには、unreachableというTaintに一致するTolerationをpodに追加しておく
  • Podは通常コントローラによって管理される
    • StatefulSetはPodを特定の順序で起動及び終了させる
      • Redis, MongoDB, Cassandraなどの分散アプリケーションは独自のクラスタ作成することから、クラスタリーダを予測可能な名前で識別できる必要がある
      • 個々のPodは自身がredis-0で実行されているかを確認し、もしされているのでればクラスタリーダとして、そうでなければredis-0と連絡を取ることでクラスタ参加できる
      • 0から順に起動するため、クラスタリーダが起動した後に他のpodを起動させるこを保証できる
      • 終了時は逆に0を最後にシャットダウンさせる
      • Headless Serviceを使えば、redis-0.default.svc.cluster.localな各podが認識できるDNS名が与えられる
  • アルファの機能でPodPresetというのがあり、Mutating Admission Webhookを易に使用できるようなリソースのようで、定義した設定を個々のPodの設定とマージできる
  • オペレータの作り方

設定と機密情報

  • ConfigMapの設定で楽なのはkubectl create configmap demo--from-file=config.yaml --dry-run -o yaml
  • configMapの情報は環境変数として読み込むこともできれば、volumeとして扱うことできます
  • ConfigMapの設定変更はすぐに反映される
  • Secretの読み取りや編集はRBACによって制御される。etcdにアクセス権限のあるユザは見れるかというと、そうではなくデータは保存時に暗号化されているため見れない。
  • 機密データの運用については、一旦SOPS(secrets operation)を試してみて不足がるようならVaultのようなツールを試すべき
    • SOPSはファイル全体を暗号化するのではなく、機密情報の値だけ暗号化する -> コドレビューが容易に
    • リポジトリで管理するときに暗号化し、デプロイ時に復号します
    • Amazon KMSやGoogle Cloud KMSをSOPSのバックエンドにすることも可能

セキュリティとバックアップ

  • ClairはコンテナスキャナでCDパイプラインに統合してデプロイ前にテストすることもきる
  • 他にも、TrivyAchore Engineなどのコンテナスキャンツールが存在する
  • レプリケーションはバックアップではない
    • 例えば、クリックする場所を間違えてボリュームを削除してしまうといった事故からってくれない
    • なのでバックアップを取る必要性がある
  • Veleroクラスタの状態と永続データをバックアップ及び復元できるツール
    • クラスタの状態がいつどのように変化したかも追える
    • クラスタ間で移行するためにも利用できる
  • Kubernetes DashboardはConfigMapやSecretの内容も表示できるのでDashboardに対する制限は厳しくしなければならない
    • インターネットに公開してはならず、代わりにkubectl proxyを利用する
    • 要らないなら実行しなくても良い

Kubernetesアプリケーションのデプロイ

  • Helmについての記述が多かったが基本的には公開されているリポジトリ以外では使わいのでパラ見
  • kustomizeはv1.14からkubectlに入ったみたい
  • Conftestというツールでマニフェスト検証ができるので、CDパイプラインに追加しても良さそう
    • バージョンアップしたときに変更する必要性があるかをテストできる

開発ワークフロー

  • Skaffoldって変更を検知して、ファイルをユーザの代わりに自動的にデプロイするのすごい。コンテナレジストリへの明示的なPushが要らない。
  • Terepresenceはローカルマシンをリモートクラスタに参加させることができる、つまデプロイが要らない

Kubernetesにおける継続的デプロイ

  • コンテナベースのCDパイプラインツールを使う場合には、各ステップのコンテナをできるだけ小さくする

Observabilityと監視

  • ブラックボックス監視の限界
    • 「何が」壊れているのかは分かるが、「なぜ」壊れているのかはわからない
    • 事後対応であり、問題が発生した後にしか通知しない
    • ユーザがハッピーでなければ9の数に意味はない
      • 99,9%(スリーナインズ)だろうと99.99%(フォーナインズ)だろうと、ユーザにって機能してなければ意味がない
      • サイトの応答が遅すぎるのであれば、完全にダウンしているのと変わらない
  • メトリクスの導入
    • 「なぜか」という疑問の解決に役立つ
    • 問題の予測に役立つ
  • トレーシング
  • Obeservability Pipeline
    • kinesisとかのデータストリームにログデータやメトリクス全部突っ込んで、そっらLambdaとか使ってルーティングし、NewRelicやらStackdriverに流しましょうとうもの
    • Push型アーキテクチャのDatadogとかはそれが可能だけど、Pull型のPrometheusどうするのだろう
  • Kubernetesにおける監視
    • ブラックボックス型チェック
    • 内部ヘルスチェック
      • より高度なReadinessProbeとするなら、「このアプリケーションが仕事を遂行るには何が必要か」を問いかけるべき(ユーザがハッピーか?)
        • DBとのやり取りがあるなら、DBの接続が有効で応答するか?
        • 他のServiceに依存するなら、必要なServiceが利用できるかをチェック
      • Readinessのチェック失敗は「自分は問題ないが、今はユーザのリクエストを処できない」となる
        • つまりReadinessは、コンテナには何の問題もないが、依存関係にある要素のこかで障害が起こっているときに生きてくる
        • マイクロサービスの最終チェーンが障害を起こしたとして、初めは最後から2番のチェーンがReadinessProbeに失敗 -> それを受けて最後から3番目が失敗 ->... -> 一番最初のチェーンがReadinessに失敗しブラックボックステストでアート=これがサーキットブレイカーパターン
      • そもそもサービスを設計する際には、コンポーネントとなっているサービスの1つ上が利用できなくなったとして場合でもシステム障害となることは避けるべき
      • つまり優雅に劣化する

Kubernetesにおけるメトリクス

  • メトリクスはすべて数値であるため、ログと違い計算や統計を行うことができる
  • メトリクス値は大きく2つのタイプに分かれる
    • カウンタ:増えていく(または0にリセットする)ことしかできず、リクエストの数捕捉したエラーの数の測定に優れている
    • ゲージ:増減ともに可能で、CPU使用率のように継続的に変化する量や他の数量との率を表すのに便利
  • REDパターン
    • Requests-Errors-Duration
      • 1秒あたりの受信リクエスト数
      • エラーを返したリクエストの割合
      • リクエストの持続時間(レイテンシ)
    • サービスが示している性能と、それをユーザが体験している状況
    • 元記事はこれ
    • SRE本にあるThe Four Golden Signalsの派生
    • すべてのサービスで同じメトリクスを測定するメリットとは? -> 運用チームのスーラビリティが高まる。インシデントに対応する人は認知上の負担を軽くできる
  • USEパターン
    • Utilization-Saturation-Errors
      • リソースがリクエストの処理でビジーになっていた平均時間、またはリソースキパシティに対する現在の使用量
      • リソースが過負荷となっている程度、このリソースが利用できるのを待つリクエトが格納されているキューの長さ
      • リソースに対するオペレーションが失敗した回数
    • USEパターンの関心はサービスではなくリソース
    • CPUやディスクなどの物理的なコンポーネントやネットワークのインタフェイス
    • 元記事はこれ
    • ボトルネックを特定するときに用いられる
  • ビジネスメトリクス
    • 例えば…
      • ファネル分析(「ランディングページ閲覧人数」や「サインアップページへクリクスルーした人」)
      • 顧客あたりの収益
    • ログデータで集計するよりも、アプリケーションから時系列メトリクスを取得するこで可視化するほうが簡単
  • Kubernetesでのメトリクス収集
    • クラスタの健全性に関するメトリクス
      • ノードの数
      • ノードの健全性ステータス
      • ノードあたり、および全体のPodの数
      • ノードあたり、および全体のリソース使用量/割当て
    • Deploymentに関するメトリクス
      • Deploymentの数
      • Deploymentごとのレプリカの設定数
      • Deploymentごとの利用できないレプリカの数
    • コンテナに関するメトリクス
      • ノードあたり、および全体のコンテナ/Podの数
      • リソース要求/リソース制限に対する各コンテナのリソース使用率
      • コンテナのLiveness/Readinessの状況
      • コンテナ/Podの再起動回数
      • 各コンテナのネットワーク入出力トラフィックおよびエラー
    • ランタイムに関するメトリクス
      • ヒープとスタックの使用量
      • GC機能の実行時間など
      • 非ヒープメモリの使用率
  • 単純な平均の問題点
    • 単純平均はハズレ値の影響を受けやすい
    • 中央値の方が影響を受けにくく有用といえる
    • 最悪のケースの方に関心がある場合が多いので、90%タイルがより有用なケースも
  • どのサービスにおいてもダッシュボードのレイアウトは同じにしておく
    • サービスあたり1行
    • 左側にリクエスト数とエラー率
    • 右側にレイテンシ
    • 参考
  • マスタダッシュボードで情報ラジエータを使用するのが理想的
    • 重要なObservabilityデータを表示して関連するチームやオフィスの全員からみれようにする

感想

買ってよかった!
既知の内容少なかったし、監視周りはSRE本とそこから派生したブログ記事、入門監視あたりのエッセンスを集めてギュッとした感じがあって分かりやすかった。
仕事でも活かせやすい内容が多かったように思えます。

【AWS Re:Inventレポート】CON334-R1 - [REPEAT 1] Running high-security workloads on Amazon EKS

Running high-security workloads on Amazon EKS

5日目にして初めてセッションに参加しました。
セッション参加して思ったこととしては、まず前を取ることが重要だなと感じました。英語読むスピードはどうしても日本語より遅くなってしまうので、追いきれないと思ったスライドは写真に収めてしまうのが吉ですね。動画見ていても思うことではあるのですが、画面読んでると話聞けなくなるし、聞いてると画面読めないという板挟みにあいます。どうすれば…?
もちろん救いもあります。スライドと口頭の説明だけかと思いきや、デモとかもあって割と分かりやすいです。
あと、事前知識がまったくない分野の話になると、英語がまったくわからなくなります。今回のケースだと、SELinuxが全然分かりませんでした。「SELInuxわかんないのかよ。インフラエンジニアの癖に。」という石を投げるのはやめてください。

セッションの概要

Amazon EKSのセキュリティについてのお話です。Amazon EKSはKubernetes API層/Container Runttime Operating system層/AWS層というレイヤーで構築されていると思いますが、前者2つをセキュリティ性高くしていくにはどのようにすべきかという内容でした。

セッションの内容

(このパートは動画見た後に直すかもです)
最初にセキュリティを考える上での基礎に触れていたように思います。例えばCIAA modelだとかThe actor and capablities modelについてです。
EKSに対してどんな攻撃方法があるか?例えばL7での攻撃(XSS等)だとか、containerdやruncなどのオープンソースに潜んでいるバグを利用するもの、DDoS攻撃などが考えられます。

一つ例を挙げてみます。例えば、とあるブログの記事で見つけたyamlを適用したら、マルウェアが含まれていたケース。これはごく稀にあるかもしれないと思ってしまいますよね。curlで落としてきたマニフェストを確認せずにapplyしてしまったりとか。マルウェアは自分たちのシステムにアクセスできてしまうし、関連しているAWSのマネージド・サービス、例えばRDSなんかもそうです。

そういった驚異からの影響をどう小さくしていくか、緩和していくか
EKSの構造を Kubernetes API -> Container Runtime -> AWSの3層構造と考える

Kubernetes API

  • namespaces
  • ServiceAccounts
    • これはかなり効くと思っています。特に上であげた例のようなケースだと。
    • serviceAccountNameをマニフェストに追記することになるので、権限をかなり意識するようになる。
  • ResouseQuota/LimitRange
    • リソースをガンガン食いまくる系のマルウェアだった場合に制限がかけられる
  • NetWorkPolicy
  • RBAC
  • Dynamic Admission Webhooks
    • Mutableの方ではなく、おそらくvalidatingの方を使っての制限と思う
    • Admission Webhooksを使うとnamespaceとかも確認しつつ制限かけられるのは柔軟で良さそう
  • API AuditLogs
  • Pod Security Policies

PodSecurityPoliciesは下記のようなサンプルとともに深堀りされていました。確かにかなり効果的な制限が出来るようです。 これはClusterリソースでPodが作成・更新されるときにシステムに受け入れられるように必要な条件を定義できるものだそうです。

apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
  name: restricted
spec:
  privileged: false # 
  allowPrivilegeEscalation: false
  requireDropCapabilities: ['all']
  volumes: ['configMap', 'secret', 'projected']
  hostNetwork: false
  hostIPC: false
  hostPID: false

詳しくはこちら

kubernetes.io

Security Contextでシステムコールを制限できる。単純にprivillagedを使うんでなくSecurityContext使って極力権限を絞っていくと良い。

kubernetes.io

Container Runttime Operating system

SELInux勉強不足であまり理解出来なかったので、勉強しつつ動画みれるようになったら補足して書きます。

感想

セッションもかなり学びがあると思いました。もちろん動画公開もあるので、なんとも言えないのですが…。
これにてAWS Re:Inventは終わりでした!学習型カンファレンスと言われているように多くの学びがありました。反省点としては英語の能力が書けていたこと、同じ分野のワークショップを取り続けてしまったことでしょうか。
初めてのアメリカ、(ほぼ)はじめての海外旅行なので、色々戸惑った点などあったのですが、それはまた別で記事にしようと思います。