ようへいの日々精進XP

よかろうもん

AWS Lambda で Node.js v4.3 がサポート終了になるので, v8.10 にアップグレードする為にテストを書いて検証してみた

tl;dr

5 月の初めに AWS Lambda において, ランタイムとして Node.js v4.3 のサポートが終了になる旨のアナウンスがありました.

このアナウンスの最後に, 以下のように v6.10 又は v8.10 へのアップグレードを推奨されています.

We strongly encourage you to update all your functions to a newer available runtime version (v6.10 or v8.10) so that you continue to benefit from important security, performance, and functionality enhancements offered by more recent Node.js releases.

また, 本件については, クラスメソッドさんのブログでも取り上げられています.

dev.classmethod.jp

有難うございます.

さて

移行期間等の詳細については, 上記のクラスメソッドさんのブログが詳しいので, そちらを参考にしていただくとして, 手元の環境において Node.js v4.3 環境があるのかなと探してみたところ, CloudWatch Logs から Amazon Elasticsearch Service に転送する為に自動的に生成される Lambda ファンクションが Node.js v4.3 となっていました.

docs.aws.amazon.com

ちなみに, 2018/06/09 時点でサブスクリプションを作成した結果, Node.js のバージョンは v4.3 となりました.

$ aws lambda get-function-configuration --function-name=LogsToElasticsearch_oreno-es2
{
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "CodeSha256": "YGJUBkMfMXe6NKzVaH2yUNYy4E8TZZXI4kg/YmqnRoQ=",
    "FunctionName": "LogsToElasticsearch_oreno-es2",
    "CodeSize": 2557,
    "RevisionId": "a2e2835b-46ff-4656-9c3e-9f6e3c0646df",
    "MemorySize": 128,
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:01234567890:function:LogsToElasticsearch_oreno-es2",
    "Version": "$LATEST",
    "Role": "arn:aws:iam::01234567890:role/lambda_elasticsearch_execution",
    "Timeout": 60,
    "LastModified": "2018-06-09T11:24:28.204+0000",
    "Handler": "index.handler",
    "Runtime": "nodejs4.3",
    "Description": "CloudWatch Logs to Amazon ES streaming"
}

へー, そーなんやー.

ということで, 前述の CloudWatch Logs から Amazon Elasticsearch Service にログを転送する Lambda ファンクションを v8.10 にアップグレードすることを想定して, テストを書いてアップグレードする準備をしてみたので, その模様をハイライトでお送ります.

本編

自動的に生成された Lambda ファンクション

以下のようなコードになっています. (全体で 250 行程のですので, だいぶん端折って掲載させて頂きます.)

// v1.1.2
var https = require('https');
var zlib = require('zlib');
var crypto = require('crypto');

var endpoint = 'Amazon Elasticsearch Service のエンドポイント';

exports.handler = function(input, context) {
    // decode input from base64
    var zippedInput = new Buffer(input.awslogs.data, 'base64');

    // decompress the input
    zlib.gunzip(zippedInput, function(error, buffer) {
        if (error) { context.fail(error); return; }

        // parse the input from JSON
        var awslogsData = JSON.parse(buffer.toString('utf8'));

        // transform the input to Elasticsearch documents
        var elasticsearchBulkData = transform(awslogsData);

        // skip control messages
        if (!elasticsearchBulkData) {
            console.log('Received a control message');
            context.succeed('Control message handled successfully');
            return;
        }

        // post documents to the Amazon Elasticsearch Service
        post(elasticsearchBulkData, function(error, success, statusCode, failedItems) {
            console.log('Response: ' + JSON.stringify({ 
                "statusCode": statusCode 
            }));

            if (error) { 
                console.log('Error: ' + JSON.stringify(error, null, 2));

                if (failedItems && failedItems.length > 0) {
                    console.log("Failed Items: " +
                        JSON.stringify(failedItems, null, 2));
                }

                context.fail(JSON.stringify(error));
            } else {
                console.log('Success: ' + JSON.stringify(success));
                context.succeed('Success');
            }
        });
    });
};

function transform(payload) {
... 略 ...

このコードをローカルにコピーした上で作業を進めていきます.

テストの観点

テストには以下のような内容を期待したいと思います.

  • Lambda 関数から出ていくデータ (Elasticsearch に放り込まれるデータ) が正常に生成されること
  • Lambda 関数から出ていくデータ (Elasticsearch に放り込まれるデータ) について, バージョンによって差異が無いこと

上述の Lambda ファンクションでは, transform 関数が, その役割を担っているようですので, この関数を主にテストすることになると思います.

テストの準備

今回, テストを書くにあったって, nodenv で v4.3 と v8.10 環境を用意しました.

nodenv install 4.3.2
nodenv install 8.10.0

また, 各バージョン毎にディレクトリを作成し, カレントディレクトリに各バージョンのテスト環境を用意しました.

# Node.js v4.3 環境を用意
mkdir nodejs4
cd nodejs4
nodenv local 4.3.2
npm init -y && npm i -D mocha power-assert rewire
cd ../
# Node.js v8.10 環境を用意
mkdir nodejs8
cd nodejs8
nodenv local 8.10.0
npm init -y && npm i -D mocha power-assert rewire

尚, インストールする npm パッケージは以下の通りです.

パッケージ名 用途
mocha JavaScript のテストフレームワーク, https://mochajs.org/
power-assert アサーションライブラリ, https://github.com/power-assert-js/power-assert
rewire プライベートメソッドをテストする為に利用する, https://github.com/jhnns/rewire

一応, 動作を確認したいので, 簡単なテストを書いてみたいと思います.

以下のような sample.js を用意します.

'use strict';

exports.handler = (event, context, callback) => {
  const response = JSON.stringify({ message: 'hello lambda!', input: event });
  callback(null, response);
};

そして, テストコードは以下のようなテストを用意しました.

'use strict';
const assert = require('assert');
const lambda = require('./sample.js');

describe('Lambda Test', () => {
  let event;
  let context;

  it('event が指定されていない', () => {
      lambda.handler(event, context, (error, result) => {
        assert.equal(result, '{"message":"hello lambda!"}');
      });
  });

  it('event に sample という文字列が指定されている', () => {
      event = 'sample';
      lambda.handler(event, context, (error, result) => {
        assert.equal(result, '{"message":"hello lambda!","input":"sample"}');
      });
  });

  it('event に foo という文字列が指定されている', () => {
      event = 'foo';
      lambda.handler(event, context, (error, result) => {
        assert.notEqual(result, '{"message":"hello lambda!","input":"sample"}');
      });
  });
});

以下のようにテストを実行します.

$ node -v
v4.3.2
$ ./node_modules/mocha/bin/mocha sample.test.js


  Lambda Test
    ✓ event が指定されていない
    ✓ event に sample という文字列が指定されている
    ✓ event に foo という文字列が指定されている


  3 passing (14ms)
... 略 ...
$ node -v
v8.10.0
$ ./node_modules/mocha/bin/mocha sample.test.js


  Lambda Test
    ✓ event が指定されていない
    ✓ event に sample という文字列が指定されている
    ✓ event に foo という文字列が指定されている


  3 passing (8ms)

テスト用データの用意

手元の環境でテストを行うにあたって, 入力と出力のデータを用意する必要があります. 以下のように, 実際の Lambda ファンクションからデータをダンプして用意したいと思います.

// v1.1.2
var https = require('https');
var zlib = require('zlib');
var crypto = require('crypto');

var endpoint = 'Amazon Elasticsearch Service のエンドポイント';

exports.handler = function(input, context) {
    // 入力データをダンプする
    console.log(input);
    // decode input from base64
    var zippedInput = new Buffer(input.awslogs.data, 'base64');

    // decompress the input
    zlib.gunzip(zippedInput, function(error, buffer) {
        if (error) { context.fail(error); return; }

        // parse the input from JSON
        var awslogsData = JSON.parse(buffer.toString('utf8'));

        // transform the input to Elasticsearch documents
        var elasticsearchBulkData = transform(awslogsData);
        // 出力データ (Elasticsearch に書き込むデータ) をダンプする
        console.log(elasticsearchBulkData);
... 略 ...

実際に Apacheアクセスログを CloudWatch Logs に転送して, 上記の Lambda ファンクションには, 以下のような状態でデータが入ってきます.

{ awslogs: { data: 'H4sIAAAAAAAAAN1TUW+bMBD+K5afNq0xZwO2IU/tlmadlmpKkPbQVJUDTmKJQGbM2i7qf99BNamttP2AAQ/Wd98d9313PtGD7Tqzs8Xj0dKcfjovzu8Ws9XqfD6jZ7S9b6xHGJJEQSw1l6lEuG53c9/2R4yYoyn3doLIzjbPoVXw1hww5iZQKguVsUpuN1yIMbnrN13p3TG4trl0dbC+o/kNndWmC67srPHl/rnEXett005sR2/HwrOftgkD+URdhfXjBLTSqdJcA2QZF4mMZZLgUaUQZzF+UnKlM4ltAwhIuFKYg00Eh7qDOaAEngqdpiLhgM/ZHz+wPIeEcamZBMZ1TCb43kAWfembSADXuRB5GucxkA+QAdySNZ3PChK5YA9RaQ7Wmy6SMSefi+JbxBlfUyIAiJbIREKwu9Y/Ru1260qLsTVdtL9cXZsoYUDele3haILb1HZKFqurGdEMpuS7a6r2viPXBUkZn5LCuwpNGVKm5KJYrtBlnmZTMi8uFBNTwq6xp49flwQbYEksXkKCAUtBCfUCixGDJAXJYngDjyhWF6/wFIlK4B8vlleReL+m6K59CN6UwVaXztYVTuxEK9Q7LNJf/cM0b3/0OBWk/dvIkbq1flzNt05iEMfXDFX+Bz9RztgRypngefMY8DrkVA83ad+OZr3e00F/H/Z9N7oz5OCeh35Iwu2jT0+3T78B18fvmPUDAAA=' } }

ログ自体は JSON フォーマットになっており, 更に zip で圧縮して Base64 エンコードされています. これが入力データ (変数 input に入っているデータ) となります.

そして, Elasticsearch に書き込まれるデータは以下のようなデータとなります.

{"index":{"_index":"cwl-2018.06.09.13","_type":"apache-loggen","_id":"34087857818009912463644099750393393661789665600204177408"}}
{"date":"09/Jun/2018:22:53:30 +0900","request":"GET /item/cameras/631 HTTP/1.1","referer":"/category/office","agent":"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; BTRS122159; GTB7.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; BRI/2)","ident":"-","bytes":86,"host":"104.168.60.183","authuser":"-","status":200,"@id":"34087857818009912463644099750393393661789665600204177408","@timestamp":"2018-06-09T13:53:30.000Z","@message":"104.168.60.183 - - [09/Jun/2018:22:53:30 +0900] \"GET /item/cameras/631 HTTP/1.1\" 200 86 \"/category/office\" \"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; BTRS122159; GTB7.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; BRI/2)\"","@owner":"044703681656","@log_group":"apache-loggen","@log_stream":"i-0c7e0dae76fb12256"}

Elasticsearch の bulk API を利用する為, 上記のような 2 つの JSON が改行で連結されたフォーマットになっています. これが出力データ (transform 関数の返り値) となります.

尚, 上記で利用したログデータは apache-loggen という Gem を利用して生成したダミーデータを利用しています.

テストコード

データも用意出来たところで, 以下のようなテストコードを書きました. また, テストされる側のコード (Lambda ファンクションのコード) についても, 適当なファイル名を付けてローカルに保存しておきます. 今回は index.js とし, 以下のテストコードは index.test.js とします.

'use strict';
const assert = require('assert');
const rewire = require('rewire');
const zlib = require('zlib');
const crypto = require('crypto');

const input = { awslogs: { data: 'H4sIAAAAAAAAAN1TUW+bMBD+K5afNq0xZwO2IU/tlmadlmpKkPbQVJUDTmKJQGbM2i7qf99BNamttP2AAQ/Wd98d9313PtGD7Tqzs8Xj0dKcfjovzu8Ws9XqfD6jZ7S9b6xHGJJEQSw1l6lEuG53c9/2R4yYoyn3doLIzjbPoVXw1hww5iZQKguVsUpuN1yIMbnrN13p3TG4trl0dbC+o/kNndWmC67srPHl/rnEXett005sR2/HwrOftgkD+URdhfXjBLTSqdJcA2QZF4mMZZLgUaUQZzF+UnKlM4ltAwhIuFKYg00Eh7qDOaAEngqdpiLhgM/ZHz+wPIeEcamZBMZ1TCb43kAWfembSADXuRB5GucxkA+QAdySNZ3PChK5YA9RaQ7Wmy6SMSefi+JbxBlfUyIAiJbIREKwu9Y/Ru1260qLsTVdtL9cXZsoYUDele3haILb1HZKFqurGdEMpuS7a6r2viPXBUkZn5LCuwpNGVKm5KJYrtBlnmZTMi8uFBNTwq6xp49flwQbYEksXkKCAUtBCfUCixGDJAXJYngDjyhWF6/wFIlK4B8vlleReL+m6K59CN6UwVaXztYVTuxEK9Q7LNJf/cM0b3/0OBWk/dvIkbq1flzNt05iEMfXDFX+Bz9RztgRypngefMY8DrkVA83ad+OZr3e00F/H/Z9N7oz5OCeh35Iwu2jT0+3T78B18fvmPUDAAA=' } };
const elasticsearchBulkData = `{"index":{"_index":"cwl-2018.06.09.13","_type":"apache-loggen","_id":"34087857818009912463644099750393393661789665600204177408"}}\n{"date":"09/Jun/2018:22:53:30 +0900","request":"GET /item/cameras/631 HTTP/1.1","referer":"/category/office","agent":"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; BTRS122159; GTB7.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; BRI/2)","ident":"-","bytes":86,"host":"104.168.60.183","authuser":"-","status":200,"@id":"34087857818009912463644099750393393661789665600204177408","@timestamp":"2018-06-09T13:53:30.000Z","@message":"104.168.60.183 - - [09/Jun/2018:22:53:30 +0900] \\"GET /item/cameras/631 HTTP/1.1\\" 200 86 \\"/category/office\\" \\"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; BTRS122159; GTB7.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; BRI/2)\\"","@owner":"044703681656","@log_group":"apache-loggen","@log_stream":"i-0c7e0dae76fb12256"}\n`;

describe('Lambda function をテストする', () => {
  it('Elasticsearch に放り込むデータを生成する関数 (transform) をテストする', () => {
    let zippedInput = new Buffer(input.awslogs.data, 'base64');
    zlib.gunzip(zippedInput, function(error, buffer) {
      let awslogsData = JSON.parse(buffer.toString('utf8'));
      let lambda = rewire('./index.js'), transform = lambda.__get__('transform');
      let actual = transform(awslogsData);
      assert.deepStrictEqual(actual, elasticsearchBulkData);
    });
  });
});
  • 実際にテストしている部分というのは assert.deepStrictEqual(actual, elasticsearchBulkData); だけ
  • 他の部分は, 入力データを解凍してデコード, JSON をパースしている部分. これはテストされる側のコードから拝借している
  • assert.deepStrictEqual(actual, expected[, message]) はオブジェクトを再帰的に深い部分まで評価するアサーション
  • ちゃんと使いこなせていないけど, rewire 便利だな

テスト実行

ここまででディレクトリ構成は以下のようになっている状態です.

$ tree . -L 2
.
├── docker-compose.yml
├── dockerfiles
│   └── elasticsearch
├── nodejs4
│   ├── index.js
│   ├── index.test.js
│   ├── node_modules
│   ├── package.json
│   ├── sample.js
│   └── sample.test.js
└── nodejs8
    ├── index.js
    ├── index.test.js
    ├── node_modules
    ├── package-lock.json
    ├── package.json
    ├── sample.js
    └── sample.test.js

5 directories, 13 files

まず, Node.js v4.3 環境でテストをしてみます.

$ cd nodejs4
$ node -v
v4.3.2
$ ./node_modules/mocha/bin/mocha index.test.js


  Lambda function をテストする
    ✓ Elasticsearch に放り込むデータを生成する関数 (transform) をテストする


  1 passing (181ms)

つづいて, 同じコードを Node.js v8.10 環境で動かしてみます.

$ cd ../nodejs8
$ node -v
v8.10.0
$ ./node_modules/mocha/bin/mocha index.test.js


  Lambda function をテストする
    ✓ Elasticsearch に放り込むデータを生成する関数 (transform) をテストする


  1 passing (142ms)

いい感じです.

再掲となりますが, 冒頭に掲げた以下のようなテスト観点について網羅されているのではないかと思います.

  • Lambda 関数から出ていくデータ (Elasticsearch に放り込まれるデータ) が正常に生成されること
  • Lambda 関数から出ていくデータ (Elasticsearch に放り込まれるデータ) について, バージョンによって差異が無いこと

改めて, いい感じです.

以上

たった 1 行のテストでしたが, コードで検証出来たことでちょっとだけ自信を持ってアップグレード出来る気がしています. 引き続き, 異常系や Elasticsearch にデータを放り込む部分についてもテストが出来るようにしたいと考えていますが, 実際に Elasticsearch にデータを放り込むのか, モックを使うのか引き続き調査中です...

また, 今回, JavaScript のテストの書き方について勉強になりました (そもそも JavaScript をまともに書けないけど). テストフレームワークは Mocha を, アサーションライブラリとして power-assert を利用しました. これらのツールについては, 機能としてほんのちょっとしか使えていないと思うので, 引き続き勉強していきたいです.

ということで, 今後も精進したいと思います.

2018 年 06 月 08 日 (金)

ジョギング

日課

  • (腕立て x 50 + 腹筋 x 50) x 3

奥さん

  • 体調を崩した
  • 疲れがたまっていたのだろう, 週末はゆっくりとして欲しい

今日のるびぃ ~ REx - Ruby Examination にチャレンジ (22) ~

REx - Ruby Examination の問題を自分なりにアレンジした上で 1 〜 3 問くらいずつ解いていく. 正直言ってかなり難しい. 尚, irb に動作確認環境は以下の通り.

$ ruby --version
ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux]
$ irb --version
irb 0.9.6(09/06/30)

クラスの継承

次のコードを実行するとどうなるか.

class Cls1
  def initialize
    puts "Cls1#initialize"
  end
end

class Cls2 < Cls1
  def initialize(*args)
    super
    puts "Cls2#initialize"
  end
end

Cls2.new(1,2,3,4,5)

例外 (ArgumentError) が発生する

以下, irb にて確認.

...
irb(main):013:0> 
irb(main):014:0* Cls2.new(1,2,3,4,5)
ArgumentError: wrong number of arguments (5 for 0)
...

以下, 解説より抜粋.

  • super と呼び出した場合は, 現在のメソッドと同じ引数が引き継がれる
  • 引数を渡さずにオーバーライドしたメソッドを呼び出す際は super() と指定する
  • super() とするか, 親クラスの def initialize メソッドを def initialize(*) とすることで, 子クラスの同名メソッドの引数を無名引数として受け取ることが出来る

以下, コードの修正案.

# パターン 1
class Cls1
  def initialize
    puts "Cls1#initialize"
  end
end

class Cls2 < Cls1
  def initialize(*args)
    super()
    puts "Cls2#initialize"
  end
end

Cls2.new(1,2,3,4,5)

# パターン 2
class Cls1
  def initialize(*)
    puts "Cls1#initialize"
  end
end

class Cls2 < Cls1
  def initialize(*args)
    super
    puts "Cls2#initialize"
  end
end

Cls2.new(1,2,3,4,5)

修正案を irb で確認.

# パターン 1
irb(main):001:0> class Cls1
irb(main):002:1>   def initialize
irb(main):003:2>     puts "Cls1#initialize"
irb(main):004:2>   end
irb(main):005:1> end
=> :initialize
irb(main):006:0> 
irb(main):007:0* class Cls2 < Cls1
irb(main):008:1>   def initialize(*args)
irb(main):009:2>     super()
irb(main):010:2>     puts "Cls2#initialize"
irb(main):011:2>   end
irb(main):012:1> end
=> :initialize
irb(main):013:0> Cls2.new(1,2,3,4,5)
Cls1#initialize
Cls2#initialize
=> #<Cls2:0x005587ff14d750>

# パターン 2
irb(main):001:0> class Cls1
irb(main):002:1>   def initialize(*)
irb(main):003:2>     puts "Cls1#initialize"
irb(main):004:2>   end
irb(main):005:1> end
=> :initialize
irb(main):006:0> 
irb(main):007:0* class Cls2 < Cls1
irb(main):008:1>   def initialize(*args)
irb(main):009:2>     super
irb(main):010:2>     puts "Cls2#initialize"
irb(main):011:2>   end
irb(main):012:1> end
=> :initialize
irb(main):013:0> 
irb(main):014:0* Cls2.new(1,2,3,4,5)
Cls1#initialize
Cls2#initialize
=> #<Cls2:0x0055ef9b3d0e60>

定数参照

以下のコードを実行するとどうなるか.

class Cls1
  CONST = "aaa"
end

module Mod
  CONST = "bbb"
end

module Mod
  class Cls1
    CONST = "ccc"
  end
end

module Mod
  class ::Cls1
    p CONST
  end
end

aaa が表示される

以下, irb にて確認.

irb(main):001:0> class Cls1
irb(main):002:1>   CONST = "aaa"
irb(main):003:1> end
=> "aaa"
irb(main):004:0> 
irb(main):005:0* module Mod
irb(main):006:1>   CONST = "bbb"
irb(main):007:1> end
=> "bbb"
irb(main):008:0> 
irb(main):009:0* module Mod
irb(main):010:1>   class Cls1
irb(main):011:2>     CONST = "ccc"
irb(main):012:2>   end
irb(main):013:1> end
=> "ccc"
irb(main):014:0> 
irb(main):015:0* module Mod
irb(main):016:1>   class ::Cls1
irb(main):017:2>     p CONST
irb(main):018:2>   end
irb(main):019:1> end
"aaa"
=> "aaa"

以下, 解説より抜粋.

  • クラス名が修飾されている場合は同じ名前であっても別のクラスになるが, :: 演算子を使うことによりネストを指定することができる
  • モジュール Mod にあるクラス Cls1 でメソッドの呼び出しを行う為には, M::C と書く
  • 先頭に :: をつけるとトップレベルから探索を行う
  • 設問では ::Cls1 となっており, トップレベルの ::Cls1 を参照する為, aaa が表示される

フムフム.

2018 年 06 月 07 日 (木)

ジョギング

日課

  • (腕立て x 50 + 腹筋 x 50) x 3

体が

  • 色々痛い
  • 足もそうだし, 首筋とか痛みがある

今日のるびぃ ~ REx - Ruby Examination にチャレンジ (21) ~

REx - Ruby Examination の問題を自分なりにアレンジした上で 1 〜 3 問くらいずつ解いていく. 正直言ってかなり難しい. 尚, irb に動作確認環境は以下の通り.

$ ruby --version
ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux]
$ irb --version
irb 0.9.6(09/06/30)

例外, raise, RuntimeError

以下のコードを実行するとどうなるか.

begin
  raise
rescue => e
  puts e.class
end

RuntimeError と表示される

以下, irb による確認.

irb(main):001:0> begin
irb(main):002:1*   raise
irb(main):003:1> rescue => e
irb(main):004:1>   puts e.class
irb(main):005:1> end
RuntimeError
=> nil

以下, 解説より抜粋.

  • 引数無しで raise を呼び出すと RuntimeError が発生する
irb(main):006:0> raise
RuntimeError: 

確かに. 尚, raise に例外クラスを引数として渡すと, RuntimeError 以外の例外クラスで例外を発生させることが出来る.

irb(main):001:0> raise
RuntimeError: 
..
irb(main):002:0> raise StandardError
StandardError: StandardError
..
irb(main):004:0> raise NameError
NameError: NameError
..
irb(main):005:0> raise TypeError
TypeError: TypeError

更に例外

以下のコードを実行するとどうなるか.

begin
  raise "Error!"
rescue => e
  puts e.class
end

RuntimeError が表示される

以下, irb による確認.

irb(main):001:0> begin
irb(main):002:1*   raise "Error!"
irb(main):003:1> rescue => e
irb(main):004:1>   puts e.class
irb(main):005:1> end
RuntimeError
=> nil

以下, 解説より抜粋.

  • raise の例外クラスを省略した場合は, RuntimeError が発生する
  • rescue の例外クラスを省略した場合は, StandardError を捕捉する
  • RuntimeErrorStanderdError のサブクラスである

以下, StandardError クラスのドキュメントより引用.

通常のプログラムで発生する可能性の高い 例外クラスを束ねるためのクラスです。 StandardError とそのサブクラスは、 rescue 節でクラスを省略したときにも捕捉できます。

rescue 節で明示的に例外クラスを指定しなかった場合には, StandardError クラスに属する例外クラスを捕捉する.

# 以下のように書くと StandardError クラスの例外クラスであれば, rescue 節で捕捉する
begin
  raise
rescue => e
  e.class
end

以下, RuntimeError クラスの親クラスを確認するスクリプトirb で実行した例.

irb(main):001:0> subclass = RuntimeError
=> RuntimeError
irb(main):002:0> 
irb(main):003:0* loop do
irb(main):004:1*   superclass = subclass.superclass
irb(main):005:1>   p superclass
irb(main):006:1>   subclass = superclass
irb(main):007:1>   break if superclass == Object
irb(main):008:1> end
StandardError
Exception
Object
=> nil

上記のコードは, 8-1-2 例外処理と例外クラスを理解するプログラムを記述してみよう より引用させて頂いた.

フムフム.

2018 年 06 月 06 日 (水)

ジョギング

日課

  • お休み

SRE 本輪読会

  • つるべーさんの 10 章からの時系列データ, 時系列データベースの話, 流石, 専門に勉強されていて面白かった
  • 松浦さんの 15 章のポストモーテムの話, 実践的な内容の話が聞けてとても良かった

帰りに久米さんと一緒の電車になって色々と話をさせてもらって刺激になった. 人って, 環境によって大きく成長するんだなと痛感した.

今日のるびぃ ~ REx - Ruby Examination にチャレンジ (20) ~

REx - Ruby Examination の問題を自分なりにアレンジした上で 1 〜 3 問くらいずつ解いていく. 正直言ってかなり難しい. 尚, irb に動作確認環境は以下の通り.

$ ruby --version
ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux]
$ irb --version
irb 0.9.6(09/06/30)

クラスの継承関係

以下のコードを実行するとどうなるか.

val = 0

class Cls1
end

class Cls2 < Cls1
end

if Cls2 < BasicObject
  val += 100
else
  val += 15
end

if Cls2 < Cls1
  val += 100
else
  val += 15
end

p val

200

以下, irb にて確認.

irb(main):001:0> val = 0
=> 0
irb(main):002:0> 
irb(main):003:0* class Cls1
irb(main):004:1> end
=> nil
irb(main):005:0> 
irb(main):006:0* class Cls2 < Cls1
irb(main):007:1> end
=> nil
irb(main):008:0> 
irb(main):009:0* if Cls2 < BasicObject
irb(main):010:1>   val += 100
irb(main):011:1> else
irb(main):012:1*   val += 15
irb(main):013:1> end
=> 100
irb(main):014:0> 
irb(main):015:0* if Cls2 < Cls1
irb(main):016:1>   val += 100
irb(main):017:1> else
irb(main):018:1*   val += 15
irb(main):019:1> end
=> 200
irb(main):020:0> 
irb(main):021:0* p val
200
=> 200

以下, 解説より抜粋.

  • Module#<はクラスの継承関係を比較することが出来る
  • if 文においては継承関係を比較している
if Cls2 < BasicObject # Cls2 は BasicObject の子孫である為, true となる
  val += 100
else
  val += 15
end

if Cls2 < Cls1 # Cls2 は Cls1 の子孫でもある為, true となる
  val += 100
else
  val += 15
end

irb(main):027:0> if Object < Cls2 # Object クラスは Cls2 の先祖である為, false となる
irb(main):028:1>   val += 10
irb(main):029:1> else
irb(main):030:1*   val += 1000
irb(main):031:1> end
=> 1300

以下, Module#< 及び Module#<= について, ドキュメントより抜粋.

  • self が other の子孫であるか, self と other が 同一クラスである場合, true を返す
  • self が other の先祖である場合, false を返す
  • 継承関係にないクラス同士の比較では nil を返す
irb(main):001:0> module Foo
irb(main):002:1> end
=> nil
irb(main):003:0> class Bar
irb(main):004:1>   include Foo
irb(main):005:1> end
=> Bar
irb(main):006:0> class Baz < Bar
irb(main):007:1> end
=> nil
irb(main):008:0> class Qux
irb(main):009:1> end
=> nil
irb(main):010:0> p Bar < Foo
true
=> true
irb(main):011:0> p Baz < Bar
true
=> true
irb(main):012:0> p Baz < Foo
true
=> true
irb(main):013:0> p Baz < Qux
nil
=> nil
irb(main):014:0> p Baz > Qux
nil
=> nil
irb(main):015:0> p Foo < Bar
false
=> false
irb(main):016:0> p Foo < Object.new
TypeError: compared with non class/module

クラスの継承

以下のコードを実行するとどうなるか.

class Cls1
  def initialize(*)
    puts "Cls1#initialize"
  end
end

class Cls2 < Cls1
  def initialize(*args)
    super
    puts "Cls2#initialize"
  end
end

Cls2.new(1,2,3,4,5)

Cls1#initialize Cls2#initialize

以下, irb による実行例.

irb(main):001:0> class Cls1
irb(main):002:1>   def initialize(*)
irb(main):003:2>     puts "Cls1#initialize"
irb(main):004:2>   end
irb(main):005:1> end
=> :initialize
irb(main):006:0> 
irb(main):007:0* class Cls2 < Cls1
irb(main):008:1>   def initialize(*args)
irb(main):009:2>     super
irb(main):010:2>     puts "Cls2#initialize"
irb(main):011:2>   end
irb(main):012:1> end
=> :initialize
irb(main):013:0> 
irb(main):014:0* Cls2.new(1,2,3,4,5)
Cls1#initialize
Cls2#initialize
=> #<Cls2:0x00557d19ba4de8>

以下, 解説より抜粋.

  • def initialize(*) は無名の可変長引数を表す
  • superスーパークラスにある現在のメソッドと同じメソッドを呼びだす
  • super は引数指定なしで呼び出した場合は, 現在のメソッドと同じ引数が引き渡される
  • スーパークラスで引数を受け取る必要がない場合, initialize(*) とすることで, サブクラスで引数を意識する必要が無くなる
class Cls1
  def initialize
    puts "Cls1#initialize"
  end
end

class Cls2 < Cls1
  def initialize(*args)
    super
    puts "Cls2#initialize"
  end
end

Cls2.new(1,2,3,4,5)

Cls1def initialize(*)def initialize とすると, 以下のように ArgumentError 例外が発生する.

irb(main):001:0> class Cls1
irb(main):002:1>   def initialize
irb(main):003:2>     puts "Cls1#initialize"
irb(main):004:2>   end
irb(main):005:1> end
=> :initialize
irb(main):006:0> 
irb(main):007:0* class Cls2 < Cls1
irb(main):008:1>   def initialize(*args)
irb(main):009:2>     super
irb(main):010:2>     puts "Cls2#initialize"
irb(main):011:2>   end
irb(main):012:1> end
=> :initialize
irb(main):013:0> 
irb(main):014:0* Cls2.new(1,2,3,4,5)
ArgumentError: wrong number of arguments (5 for 0)

フムフム.

2018 年 06 月 05 日 (火)

ジョギング

  • 雨の為, お休み
  • 足の状態がイマイチだったので良い休みだった

日課

  • (腕立て x 50 + 腹筋 x 50) x 3

終日

  • 雨だった
  • 雨のせいか解らないけど, 肩から首筋に掛けて強い張りで終日辛かった

夕飯

  • パスタを作る

今日のるびぃ ~ REx - Ruby Examination にチャレンジ (19) ~

REx - Ruby Examination の問題を自分なりにアレンジした上で 1 〜 3 問くらいずつ解いていく. 正直言ってかなり難しい. 尚, irb に動作確認環境は以下の通り.

$ ruby --version
ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux]
$ irb --version
irb 0.9.6(09/06/30)

String#scan と 正規表現

以下のコードを実行するとどうなるか.

p "Matz is my tEacher".scan(/[is|my]/).size

4 と表示される

以下, irb による確認.

irb(main):001:0> p "Matz is my tEacher".scan(/[is|my]/).size
4
=> 4

以下, 解説より抜粋.

  • String#scan はマッチした部分文字列を配列で返す
  • 正規表現[] は囲まれた文字 1 つ 1 つにマッチする
  • |正規表現では OR のメタ文字である
  • 設問では |が[]に囲まれているため, これもマッチ対象となる

以下, 改めて, irb にて確認.

# String#scan でマッチした文字列を配列で返している
irb(main):002:0> p "Matz is my tEacher".scan(/[is|my]/)
["i", "s", "m", "y"]
=> ["i", "s", "m", "y"]
# Array#size でマッチした結果の size (length) で要素数を返す
irb(main):004:0> p "Matz is my tEacher".scan(/[is|my]/).class
Array
=> Array
irb(main):005:0> p "Matz is my tEacher".scan(/[is|my]/).size
4
=> 4
irb(main):006:0> p "Matz is my tEacher".scan(/[is|my]/).length
4
=> 4

以下, String#scan の実行例.

irb(main):007:0> p "foobar".scan(/../)     
["fo", "ob", "ar"]
=> ["fo", "ob", "ar"]
irb(main):008:0> p "foobar".scan("o")
["o", "o"]
=> ["o", "o"]
irb(main):009:0> p "foobarbazfoobarbaz".scan(/ba./)
["bar", "baz", "bar", "baz"]
=> ["bar", "baz", "bar", "baz"]
irb(main):010:0> p "foobar".scan(/./)
["f", "o", "o", "b", "a", "r"]
=> ["f", "o", "o", "b", "a", "r"]
irb(main):011:0> p "foobar".scan(/(.)/)
[["f"], ["o"], ["o"], ["b"], ["a"], ["r"]]
=> [["f"], ["o"], ["o"], ["b"], ["a"], ["r"]]
irb(main):012:0> p "foobarbazfoobarbaz".scan(/(ba)(.)/)
[["ba", "r"], ["ba", "z"], ["ba", "r"], ["ba", "z"]]
=> [["ba", "r"], ["ba", "z"], ["ba", "r"], ["ba", "z"]]

アクセサメソッド

以下のコードで指定した行を書き換えた時,同じ結果になるものを選ぶ.

class C
  attr_accessor :v # ここを書き換える
end

c = C.new
c.v = 100
p c.v

以下, 解答.

# パターン 1
attr_reader :v
attr_writer :v

# パターン 2
def v=(other)
  @v = other
end
def v
  @v
end

以下, irb による確認.

# 設問コード
irb(main):001:0> class C
irb(main):002:1>   attr_accessor :v
irb(main):003:1> end
=> nil
irb(main):004:0> 
irb(main):005:0* c = C.new
=> #<C:0x00561953649a48>
irb(main):006:0> c.v = 100
=> 100
irb(main):007:0> p c.v
100
=> 100
# 解答 (1)
irb(main):008:0> class C
irb(main):009:1>   attr_reader :v
irb(main):010:1>   attr_writer :v  
irb(main):011:1> end
=> nil
irb(main):012:0> 
irb(main):013:0* c = C.new
=> #<C:0x005619535fcf18>
irb(main):014:0> c.v = 100
=> 100
irb(main):015:0> p c.v
100
=> 100
# 解答 (2)
irb(main):016:0> class C
irb(main):017:1>   def v=(other)
irb(main):018:2>     @v = other
irb(main):019:2>   end
irb(main):020:1>   def v
irb(main):021:2>     @v
irb(main):022:2>   end
irb(main):023:1> end
=> :v
irb(main):024:0> 
irb(main):025:0* c = C.new
=> #<C:0x005619535b1888>
irb(main):026:0> c.v = 100
=> 100
irb(main):027:0> p c.v
100
=> 100

以下, 解説より抜粋.

  • attr_accessorattr_readerattr_writer を同時に定義する
  • attr_readerattr_writer は, 以下のようにも定義出来る
def v=(other)
  @v = other
end
def v
  @v
end

フムフム.

2018 年 06 月 04 日 (月)

ジョギング

日課

  • (腕立て x 50 + 腹筋 x 50) x 3

なんだか

  • 頭の中がガチャガチャして集中出来ていない
  • 一旦, 色々とリセットしたい気分

今日のるびぃ ~ REx - Ruby Examination にチャレンジ (18) ~

REx - Ruby Examination の問題を自分なりにアレンジした上で 1 〜 3 問くらいずつ解いていく. 正直言ってかなり難しい. 尚, irb に動作確認環境は以下の通り.

$ ruby --version
ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux]
$ irb --version
irb 0.9.6(09/06/30)

Enumerator オブジェクト

以下のコードの実行結果を得るために __(1)__ に適切なメソッドをすべて選ぶ.

# コード
module Enumerable
  def with_prefix(prefix)
    return to_enum(__(1)__, prefix) { size } unless block_given?

    each do |char|
      yield "#{prefix} #{char}"
    end
  end
end

[1, 2, 3, 4, 5].with_prefix('foobarbaz').reverse_each {|char|
  puts char
}

# 実行結果
foobarbaz 5
foobarbaz 4
foobarbaz 3
foobarbaz 2
foobarbaz 1

以下, irb による確認.

# :with_prefix
irb(main):001:0> module Enumerable
irb(main):002:1>   def with_prefix(prefix)
irb(main):003:2>     return to_enum(:with_prefix, prefix) { size } unless block_given?
irb(main):004:2> 
irb(main):005:2*     each do |char|
irb(main):006:3*       yield "#{prefix} #{char}"
irb(main):007:3>     end
irb(main):008:2>   end
irb(main):009:1> end
=> :with_prefix
irb(main):010:0> 
irb(main):011:0* [1, 2, 3, 4, 5].with_prefix('foobarbaz').reverse_each {|char|
irb(main):012:1*   puts char
irb(main):013:1> }
foobarbaz 5
foobarbaz 4
foobarbaz 3
foobarbaz 2
foobarbaz 1
=> #<Enumerator: [1, 2, 3, 4, 5]:with_prefix("foobarbaz")>
# __method__
irb(main):001:0> module Enumerable
irb(main):002:1>   def with_prefix(prefix)
irb(main):003:2>     return to_enum(__method__, prefix) { size } unless block_given?
irb(main):004:2> 
irb(main):005:2*     each do |char|
irb(main):006:3*       yield "#{prefix} #{char}"
irb(main):007:3>     end
irb(main):008:2>   end
irb(main):009:1> end
=> :with_prefix
irb(main):010:0> 
irb(main):011:0* [1, 2, 3, 4, 5].with_prefix('foobarbaz').reverse_each {|char|
irb(main):012:1*   puts char
irb(main):013:1> }
foobarbaz 5
foobarbaz 4
foobarbaz 3
foobarbaz 2
foobarbaz 1
=> #<Enumerator: [1, 2, 3, 4, 5]:with_prefix("foobarbaz")>

以下, 解説より抜粋.

  • ブロックを渡さない場合は, Enumerator オブジェクトを作成してメソッドをチェーン出来るようにする
  • Enumerator オブジェクトを作成するためには, to_enum または, enum_for を呼ぶ
  • これら (to_enum 又は enum_for) の引数にメソッド名をシンボルで指定して, チェーン先でブロックを渡されたときにどのメソッドを評価すればよいかが分かる
  • 設問では, with_prefix を再評価する必要があるので, __method__ または :with_prefix を引数に指定する
  • __method__ はメソッドの中で呼び出すと, そのメソッド名となる
irb(main):014:0> def foobarbaz
irb(main):015:1>   __method__
irb(main):016:1> end
=> :foobarbaz
irb(main):017:0> 
irb(main):018:0* p foobarbaz
:foobarbaz
=> :foobarbaz

protected

以下のコードを実行するとどうなるか.

class C
  protected
  def initialize
  end
end

p C.new.methods.include?(:initialize)

true が表示される

以下, irb にて確認.

irb(main):001:0> class C
irb(main):002:1>   protected
irb(main):003:1>   def initialize
irb(main):004:2>   end
irb(main):005:1> end
=> :initialize
irb(main):006:0> 
irb(main):007:0* p C.new.methods.include?(:initialize)
true
=> true

以下, メモ.

  • initialize メソッドは private メソッドである
  • protected に設定されたメソッドは, そのメソッドを持つオブジェクトが self であるコンテキスト(メソッド定義式やinstance_eval)でのみ呼び出せる
  • Object#methods は, そのオブジェクトに対して呼び出せるメソッド名の一覧を返す, このメソッドは public メソッドおよび protected メソッドの名前を返す

以下, initialize メソッドが private メソッドである確認.

irb(main):001:0> class C
irb(main):002:1>   def initialize
irb(main):003:2>   end
irb(main):004:1> end
=> :initialize
irb(main):005:0> 
irb(main):006:0* p C.new.methods.include?(:initialize)
false
=> false

ちなみに, initialize メソッドは, 可視性を public にすることは出来ないが, protected にすることは出来る.

irb(main):001:0> class C
irb(main):002:1>   public
irb(main):003:1>   def initialize
irb(main):004:2>   end
irb(main):005:1> end
=> :initialize
irb(main):006:0> p C.new.methods.include?(:initialize)
false
=> false
irb(main):007:0> class C
irb(main):008:1>   protected
irb(main):009:1>   def initialize
irb(main):010:2>   end
irb(main):011:1> end
=> :initialize
irb(main):012:0> p C.new.methods.include?(:initialize)
true
=> true

フムフム.

2018 年 06 月 03 日 (日)

ジョギング, 日課 

  • お休み

久留米デート

  • 芸事, 若返りにご利益があると言われている神社にお参り
  • 久留米ラーメンを初めて食べた後に観音様を拝みに
  • 観音様の後はジェラートで〆る

やりたいことを短時間で済ませることが出来た, とても効率の良いデートだった (笑. 奥さんも喜んでくれたようで良かった.

今日のるびぃ ~ REx - Ruby Examination にチャレンジ (17) ~

REx - Ruby Examination の問題を自分なりにアレンジした上で 1 〜 3 問くらいずつ解いていく. 正直言ってかなり難しい. 尚, irb に動作確認環境は以下の通り.

$ ruby --version
ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux]
$ irb --version
irb 0.9.6(09/06/30)

キーワード引数

以下のコードを実行するとどうなるか.

def foo(arg1:100, arg2:200)
  puts arg1
  puts arg2
end

option = {arg2: 900}

foo arg1: 200, *option

例外が発生する

以下, irb にて確認.

irb(main):001:0> def foo(arg1:100, arg2:200)
irb(main):002:1>   puts arg1
irb(main):003:1>   puts arg2
irb(main):004:1> end
=> :foo
irb(main):005:0> 
irb(main):006:0* option = {arg2: 900}
=> {:arg2=>900}
irb(main):007:0> 
irb(main):008:0* foo arg1: 200, *option
SyntaxError: (irb):8: syntax error, unexpected *
foo arg1: 200, *option

以下, 解説より抜粋.

  • キーワード引数へ Hash オブジェクトを渡すことが出来る
  • Hash の中身を渡す必要があるので, 変数の前に ** を付加する

以下, 正しく動くコード.

def foo(arg1:100, arg2:200)
  puts arg1
  puts arg2
end

option = {arg2: 900}

foo arg1: 200, **option

以下, irb にて確認.

irb(main):001:0> def foo(arg1:100, arg2:200)
irb(main):002:1>   puts arg1
irb(main):003:1>   puts arg2
irb(main):004:1> end
=> :foo
irb(main):005:0> 
irb(main):006:0* option = {arg2: 900}
=> {:arg2=>900}
irb(main):007:0> 
irb(main):008:0* foo arg1: 200, **option
200
900
=> nil
irb(main):009:0> foo(arg1: 200, **option)
200
900
=> nil

Complex, Fixnum, Float 等

以下のコードを実行するとどうなるか.

val = 10i * 2i
puts val.class

Complex クラスと出力される

以下, irb にて確認.

irb(main):010:0> val = 10i * 2i
=> (-20+0i)
irb(main):011:0> puts val.class
Complex
=> nil

以下, 解説より抜粋.

  • 10i複素数 (Complex) のオブジェクトを表す
  • Complex 同士の演算は Complex を返す

その他の演算について確認.

# Fixnum と Complex の演算結果は Complex
irb(main):014:0> v1 = 10 * 2i
=> (0+20i)
irb(main):015:0> v1.class
=> Complex

# Fixnum と Rational の演算結果は Rational
irb(main):016:0> v2 = 10 * 2r
=> (20/1)
irb(main):017:0> v2.class
=> Rational

# Fixnum と Float の演算結果は Float
irb(main):018:0> v3 = 10 * 2.0
=> 20.0
irb(main):019:0> v3.class
=> Float

# Complex と Rational の演算結果は Complex
irb(main):020:0> v4 = 10i * 2r
=> ((0/1)+(20/1)*i)
irb(main):021:0> v4.class
=> Complex

フムフム.

2018 年 06 月 02 日 (土)

ジョギング

日課

  • (腕立て x 50 + 腹筋 x 50) x 3

のんびりした休日

  • お昼ごはん食べて, 気付いたら昼寝してしまっていた
  • 夕飯食べて, ダラダラしてたら探偵ナイトスクープが始まって, 夫婦ふたりでお腹を抱えて笑った

今日のるびぃ ~ REx - Ruby Examination にチャレンジ (16) ~

REx - Ruby Examination の問題を自分なりにアレンジした上で 1 〜 3 問くらいずつ解いていく. 正直言ってかなり難しい. 尚, irb に動作確認環境は以下の通り.

$ ruby --version
ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux]
$ irb --version
irb 0.9.6(09/06/30)

Enumerable, Enumerator

次のプログラムを実行するために __(1)__ に適切なメソッドをすべて選ぶ.

class IPAddr
  include Enumerable

  def initialize(ip_addr)
    @ip_addr = ip_addr
  end

  def each
    return __(1)__ unless block_given?

    @ip_addr.split('.').each do |octet|
      yield octet
    end
  end
end

addr = IPAddr.new("192.10.20.30")
enum = addr.each

p enum.next # 192と表示される
p enum.next # 10と表示される
p enum.next # 20と表示される
p enum.next # 30と表示される

to_enum enum_for

以下, irb による確認.

irb(main):001:0> class IPAddr
irb(main):002:1>   include Enumerable
irb(main):003:1> 
irb(main):004:1*   def initialize(ip_addr)
irb(main):005:2>     @ip_addr = ip_addr
irb(main):006:2>   end
irb(main):007:1> 
irb(main):008:1*   def each
irb(main):009:2>     return to_enum unless block_given?
irb(main):010:2> 
irb(main):011:2*     @ip_addr.split('.').each do |octet|
irb(main):012:3*       yield octet
irb(main):013:3>     end
irb(main):014:2>   end
irb(main):015:1> end
=> :each
irb(main):016:0> 
irb(main):017:0* addr = IPAddr.new("192.10.20.30")
=> #<IPAddr:0x00556232b19970 @ip_addr="192.10.20.30">
irb(main):018:0> enum = addr.each
=> #<Enumerator: #<IPAddr:0x00556232b19970 @ip_addr="192.10.20.30">:each>
irb(main):019:0> p enum.next
"192"
=> "192"
irb(main):020:0> p enum.next
"10"
=> "10"
irb(main):021:0> p enum.next
"20"
=> "20"
irb(main):022:0> p enum.next
"30"
=> "30"

irb(main):001:0> class IPAddr
irb(main):002:1>   include Enumerable
irb(main):003:1> 
irb(main):004:1*   def initialize(ip_addr)
irb(main):005:2>     @ip_addr = ip_addr
irb(main):006:2>   end
irb(main):007:1> 
irb(main):008:1*   def each
irb(main):009:2>     return enum_for unless block_given?
irb(main):010:2> 
irb(main):011:2*     @ip_addr.split('.').each do |octet|
irb(main):012:3*       yield octet
irb(main):013:3>     end
irb(main):014:2>   end
irb(main):015:1> end
=> :each
irb(main):016:0> 
irb(main):017:0* addr = IPAddr.new("192.10.20.30")
=> #<IPAddr:0x0055a4d08957e0 @ip_addr="192.10.20.30">
irb(main):018:0> enum = addr.each
=> #<Enumerator: #<IPAddr:0x0055a4d08957e0 @ip_addr="192.10.20.30">:each>
irb(main):019:0> p enum.next
"192"
=> "192"
irb(main):020:0> p enum.next
"10"
=> "10"
irb(main):021:0> p enum.next
"20"
=> "20"
irb(main):022:0> p enum.next
"30"
=> "30"

以下, 解説より抜粋.

  • Enumerable をインクルードした場合は, each メソッドを実装する必要がある
  • ブロックが渡されない場合でも, Enumerator オブジェクトを返すようにして外部イテレータとしても使えるようにする
  • Enumerator オブジェクトを作成するメソッドは enum_for, または to_enum を利用する

Integer#times

以下のコードを実行するとどうなるか.

10.times{|d| print d < 2...d > 5 ? "O" : "X" }

以下, irb で確認.

irb(main):021:0> 10.times{|d| print d < 2...d > 5 ? "O" : "X" }
OOOOOOOXXX=> 10

ホー.

以下, 解説より抜粋.

  • Integer#times0 から self - 1 までの数値を順番にブロックに渡すメソッド
  • 設問では, ブロックに 0 から 9 が渡される

以下は Integer#times の利用例.

irb(main):001:0> 3.times { puts "Hello, World!" }
Hello, World!
Hello, World!
Hello, World!
=> 3
irb(main):002:0> 0.times { puts "Hello, World!" }
=> 0
irb(main):003:0> 5.times {|n| print n }
01234=> 5
  • d < 2...d > 5 は, 条件式に範囲式の合わせ技

これは何だ...ということで, ドキュメントより引用.

条件式として範囲式が用いられた場合には、状態を持つ sed や awk 由来の 特殊な条件式として振る舞います。

「..」の場合:

1. 初期状態では式1だけを評価し、式1が真を返すまでは false を返します。
2. 式1が真を返すと true を返します。式2が真なら初期状態に戻ります。
3. この後は式2だけを評価し、式2が真を返すまで true を返します。
4. 式2が真を返すと true を返したあと、初期状態に戻ります。

「...」の場合:

1. 初期状態では式1だけを評価し、式1が真を返すまでは false を返します。
2. 式1が真を返すと true を返します。
3. この後は式2だけを評価し、式2が真を返すまで true を返します。
4. 式2が真を返すと true を返したあと、初期状態に戻ります。

以下, サンプル.

irb(main):001:0> 5.times{|n|
irb(main):002:1*   if (n==2)..(n==3)
irb(main):003:2>     p n
irb(main):004:2>   end
irb(main):005:1> }
2
3
=> 5
irb(main):006:0> 5.times{|n|
irb(main):007:1*   if (n==2)...(n==3)
irb(main):008:2>     p n
irb(main):009:2>   end
irb(main):010:1> }
2
3
=> 5
irb(main):011:0> 5.times{|n|
irb(main):012:1*   if (n==2)..(n==2)
irb(main):013:2>     p n
irb(main):014:2>   end
irb(main):015:1> }
2
=> 5
irb(main):016:0> 5.times{|n|
irb(main):017:1*   if (n==2)...(n==2)
irb(main):018:2>     p n
irb(main):019:2>   end
irb(main):020:1> }
2
3
4
=> 5

ムムム.

CloudFormation で作った S3 バケットにおいて, オブジェクトが入っている状態でスタックを削除しようとすると軒並みエラーになるので, その対処方法について検討した #ただそれだけ

どうも

#ただそれだけ の CloudFormation 初心者, 川原です.

EC2 や ELB, そして, ELB のログを記録する S3 バケットを含むスタックを削除しようとすると, 軒並み以下のようなエラーとなってスタックの削除が失敗に終わる.

Target group 'arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/oreno-api-debug-alb-target/d429742cee94a2c2' is currently in use by a listener or a rule (Service: AmazonElasticLoadBalancingV2; Status Code: 400; Error Code: ResourceInUse; Request ID: 66f8369a-6636-11e8-9cc4-23b879baae28)

えええーっとなって, 慌てて手動でバケットを削除してから, 改めてスタックを削除すると削除は成功で終わる.

これを回避する方法って無いのかなーと思ったのでメモ.

どういうことなの?

DeletionPolicy 属性というドキュメントを読むと, 以下のように記述されている.

削除

AWS CloudFormation はスタックの削除時にリソースと (該当する場合) そのすべてのコンテンツを削除します。この削除ポリシーは、あらゆるリソースタイプに追加することができます。デフォルトでは、DeletionPolicy を指定しない場合、リソースは削除されます。 ただし、以下の点を考慮する必要があります。

* AWS::RDS::DBCluster リソースの場合、デフォルトポリシーは Snapshot です。
* DBClusterIdentifier プロパティを指定しない AWS::RDS::DBInstance リソースの場合、デフォルトポリシーは Snapshot です。
* Amazon S3 バケットでは、削除を成功させるためにはバケットのすべてのオブジェクトを削除する必要があります。

なるほど, S3 バケットの中身は削除しなければ, スタックを削除する際に一緒に S3 バケットを削除することは出来ないとのこと.

じゃあ, どうすれば良いのか

DeletionPolicy で Retain を設定して, スタックの削除とは切り離す

docs.aws.amazon.com

以下のように, DeletionPolicyRetain を定義する.

    "ALBLOGBUCKET": {
      "Type": "AWS::S3::Bucket",
      "DeletionPolicy" : "Retain",
      "Properties": {
        "BucketName": { "Fn::Join" : [ "", [{ "Ref": "Project" }, "-", { "Ref": "Env" }, "-alb-log"]]}
      }
    },

Retain を設定することで, スタックを削除する際にも S3 バケット自体は削除せずに, 個別に手動 (AWS CLI 等) で S3 バケットを削除する.

カスタムリソースを利用して, Lambda ファンクションでオブジェクトを削除してからバケットを削除する

CloudFormation テンプレートでは, AWS::CloudFormation::CustomResource または Custom::String リソースタイプを使用して、カスタムリソースを指定することが出来る. また, スタックイベントに応じて Lambda ファンクションを実行することが出来る.

docs.aws.amazon.com

これを利用してスタックを削除する際に Lambda ファンクションを呼び出してバケット内のオブジェクトを削除してからバケットを削除するという方法を検討する. この方法は, 以下の記事を参考にさせて頂いた.

stackoverflow.com

カスタムリソースについて

改めて, カスタムリソースについて, 以下のドキュメントを参考に整理してみる.

docs.aws.amazon.com

カスタムリソースとは, スタックを作成, 更新, 削除する度に CloudFormation がカスタムリソースで定義されたロジック (Lambda ファンクション等) を実行することが出来る機能で, 以下の三者が関連して動作している. (ドキュメントより引用しているが, 内容を理解し辛かったので意訳も入っている)

登場人物 役割
template developer 要するに CloudFormation のテンプレートで, カスタムリソースを定義する
cusom resource privider CloudFormation から要求される処理と応答を行う (例えば, Lambda ファンクションとか), テンプレート内では ServiceToken の値として指定する
CloudFormation スタックオペレーション中にテンプレートで指定されたリクエストを ServiceToken に送信し, スタックオペレーションを進める前に応答を待機する

テンプレート内ではカスタムリソースを以下のように定義する.

MyCustomResource: 
  Type: "Custom::TestLambdaCrossStackRef"
  Properties: 
    ServiceToken:
      !Sub |
        arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${LambdaFunctionName}
    StackName: 
      Ref: "NetworkStackName"

オブジェクトをまるっと削除する Lambda ファンクション (1)

Lambda ファンクションを実装するにあたって, CloudFormation 側から送信されるイベントと, Lambda ファンクションが実行された後に CloudFormation に返す情報が必要となる. これらの情報についても, ドキュメントに明記されており, 例えば, スタック削除の場合には, 以下のようなイベント (ドキュメントより引用) が CloudFormation から送信される.

{
   "RequestType" : "Delete",
   "RequestId" : "unique id for this delete request",
   "ResponseURL" : "pre-signed-url-for-delete-response",
   "ResourceType" : "Custom::MyCustomResourceType",
   "LogicalResourceId" : "name of resource in template",
   "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid",
   "PhysicalResourceId" : "custom resource provider-defined physical id",
   "ResourceProperties" : {
      "key1" : "string",
      "key2" : [ "list" ],
      "key3" : { "key4" : "map" }
   }
}

また, Lambda ファンクションが実行された際に CloudFormation に返す情報は以下のような内容となっている. こちらも, スタック削除時のメッセージとなる.

成功した場合.

{
   "Status" : "SUCCESS",
   "RequestId" : "unique id for this delete request (copied from request)",
   "LogicalResourceId" : "name of resource in template (copied from request)",
   "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
   "PhysicalResourceId" : "custom resource provider-defined physical id"
}

失敗した場合.

{
  "Status" : "FAILED",
  "Reason" : "Required failure reason string",
  "RequestId" : "unique id for this delete request (copied from request)",
  "LogicalResourceId" : "name of resource in template (copied from request)",
  "StackId" : "arn:aws:cloudformation:us-east-2:namespace:stack/stack-name/guid (copied from request)",
  "PhysicalResourceId" : "custom resource provider-defined physical id"
}  

これらのイベントメッセージをやり取り出来るように Lambda ファンクションを実装する必要がある.

オブジェクトをまるっと削除する Lambda ファンクション (2)

以下のリポジトリにアップした. serverless framework でデプロイ出来るようにしてある.

github.com

テンプレートは以下のように書くことで, バケットを削除する前にカスタムリソースが呼ばれて, バケットの中身を削除した後にバケットが削除されるようになる.

AWSTemplateFormatVersion: "2010-09-09"
Description: "Clean up Bucket on CloudFormation stack delete demo."
Parameters:
  S3BucketName:
    Type: String
  CleanUpBucketFunction:
    Type: String
Resources:
   BucketResource:
     Type: AWS::S3::Bucket
     Properties:
       BucketName: !Ref S3BucketName
   CleanupBucketOnDelete:
     Type: Custom::CleanupBucket
     Properties:
       ServiceToken:
         Fn::Join:
           - ""
           - - "arn:aws:lambda:"
             - Ref: AWS::Region
             - ":"
             - Ref: AWS::AccountId
             - ":function:"
             - !Ref CleanUpBucketFunction
       BucketName: !Ref S3BucketName
     DependsOn: BucketResource

スタックを削除する

上記に掲載した CloudFormation テンプレートを利用して, オブジェクトが入っている場合と入っていない場合でバケットの削除を試してみる. このテンプレートを利用することで, oreno-sample-bucket という名前のバケットを作成する.

$ ./deploy.sh aws-profile oreno-sample-bucket demo create
{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/demo-oreno-sample-bucket/1387e400-6680-11e8-9468-50fa13f2a811"
}
Create Stack Success.

一応, バケットが作成出来たかを確認してみる.

$ aws s3api list-buckets --query=Buckets[].Name | grep "oreno-sample-bucket"
    "oreno-sample-bucket",

バケットにオブジェクトを放り込んでみる.

$ aws s3 cp test.txt s3://oreno-sample-bucket/
upload: ./test.txt to s3://oreno-sample-bucket/test.txt
$ aws s3 ls s3://oreno-sample-bucket/
2018-06-03 01:20:40          0 test.txt

AWS CLIバケットの削除を試みてみる.

$ aws s3 rb s3://oreno-sample-bucket
remove_bucket failed: s3://oreno-sample-bucket An error occurred (BucketNotEmpty) when calling the DeleteBucket operation: The bucket you tried to delete is not empty

上記のようにエラーとなる. これまで書いたように, バケットを空にする必要がある.

では, この状態でスタックを削除してみる.

$ ./deploy.sh aws-profile oreno-sample-bucket demo delete
Delete Stack Success.

正常にスタックの削除が完了した. Lambda から CloudFormation には, 以下のようなイベントが送信されている (CloudWatch Logs にダンプした).

{
    "Status": "SUCCESS",
    "Reason": "Log stream name: 2018/06/02/[$LATEST]018724fd1cc242deade18d90a525daf0",
    "PhysicalResourceId": "2018/06/02/[$LATEST]018724fd1cc242deade18d90a525daf0",
    "StackId": "arn:aws:cloudformation:ap-northeast-1:123456789012:stack/demo-oreno-sample-bucket/0580ea80-667d-11e8-80c3-50a68a175a82",
    "RequestId": "53fdeee6-feca-46b2-8073-94751f752386",
    "LogicalResourceId": "CleanupBucketOnDelete",
    "Data": {}
}

懸念点

Lambda ファンクションを用いて, オブジェクトを削除するこの方法だが, 一点だけ気になることがある. それは, オブジェクトの数がとてつもなく多い場合に削除に時間が掛かってしまい, スタックの削除自体が正常に終了しない可能性がある. これはどうしても回避することは出来ないので, やはり, テンプレートに DeletionPolicyRetain に設定して, 手動でバケットを削除する方法が良いと思われる.

以上

CloudFormation 作った S3 バケットにおいて, オブジェクトが入っている状態でスタックを削除する方法について考察した. シンプルなのは DeletionPolicyRetain を定義しておいて, 後から手動で S3 バケットを削除する, もしくは, Lambda ファンクションを実装する必要があるけど, カスタムリソースを利用することで, 一気通貫で削除することが出来ることが解った. いずれにせよひと手間かかるのは変わらないし, オブジェクト数次第では, カスタムリソースでは対応しきれない可能性がある為, 適材適所で使い分けるようにしたい.

また, CloudFormation について, カスタムリソースの存在を知れたのはとても良かった. うまく活用していきたいと思う.

2018 年 06 月 01 日 (金)

ジョギング

日課

  • (腕立て x 50 + 腹筋 x 50) x 3

もりす

  • 久しぶりに串かつもりす
  • カレーが美味しかった

今日のるびぃ ~ REx - Ruby Examination にチャレンジ (15) ~

REx - Ruby Examination の問題を自分なりにアレンジした上で 1 〜 3 問くらいずつ解いていく. 正直言ってかなり難しい. 尚, irb に動作確認環境は以下の通り.

$ ruby --version
ruby 2.1.10p492 (2016-04-01 revision 54464) [x86_64-linux]
$ irb --version
irb 0.9.6(09/06/30)

alias

期待した出力結果になるように __(1)__ に適切なコードを選ぶ.

# コード
class String
  __(1)__ 
end

p "abcde".foo

# 出力結果
edcba

alias foo reverse alias :foo :reverse

以下, irb にて確認.

irb(main):001:0> class String
irb(main):002:1>   alias foo reverse
irb(main):003:1> end
=> nil
irb(main):004:0> 
irb(main):005:0* p "abcde".foo
"edcba"
=> "edcba"
irb(main):001:0> class String
irb(main):002:1>   alias :foo :reverse
irb(main):003:1> end
=> nil
irb(main):004:0> 
irb(main):005:0* p "abcde".foo
"edcba"
=> "edcba"

以下, 解説より抜粋.

  • alias 式はメソッドやグローバル変数に別名を付けることが出来る
  • 定義は以下のように, alias 新しい名前, 元になる名前 で, メソッドの場合にはリテラルかシンボルで指定する
alias new_method old_method
alias :new_method :old_method
alias $new_global_variable $old_global_variable

alias と同様の機能を持つ alias_method について, alias とは以下のような違いがある.

  • メソッド名は String または Symbol で指定する
  • グローバル変数の別名をつけることは出来ない
  • alias_method 自身はメソッドである為, オーバーライトが出来る
irb(main):001:0> class String
irb(main):002:1>   alias foo reverse
irb(main):003:1> end
=> nil
irb(main):004:0> 
irb(main):005:0* p "abcde".foo
"edcba"
=> "edcba"
irb(main):006:0> class String
irb(main):007:1>   alias_method "bar", "reverse"
irb(main):008:1> end
=> String
irb(main):009:0> 
irb(main):010:0* p "abcde".bar
"edcba"
=> "edcba"

alias キーワードと, alias_method メソッドの違いについては, 以下の記事が参考になった.

以下の部分.

This is because alias is a keyword and it is lexically scoped. It means it treats self as the value of self at the time the source code was read . In contrast alias_method treats self as the value determined at the run time.

フムー.

文法 (演算子)

以下のコードを実行するとどうなるか.

v1 = 1 / 2 == 0
v2 = !!v1 or raise RuntimeError
puts v2 and false

true

以下, irb にて確認.

irb(main):011:0> v1 = 1 / 2 == 0
=> true
irb(main):012:0> v2 = !!v1 or raise RuntimeError
=> true
irb(main):013:0> puts v2 and false
true
=> nil

ホエー.

以下, 解説より抜粋.

  • 1 行目において, Fixnum クラス同士の除算は Fixnum クラスとなり, 0 == 0 が評価され, v1 は true となる
  • 2 行目では, or は左辺が真であればその結果を返す為, 左辺は評価されない (左辺が偽であれば, 右辺を評価する)
  • !!v1 は true となる, ちなみに !v1 は false となり, !!!v1 も false になる
  • また, or は評価する優先順位の低い演算子なので, v2 には !!v1 の結果である true が入る
  • 3 行目では, and は左辺が真であれば, 右辺の結果を返し, 左辺が偽であれば, 左辺の結果を返すが, and は評価の優先順位が低い為, puts v2 が評価される

以下, メモ.

irb(main):026:0> 1 / 2
=> 0
irb(main):027:0> 1 / 2 == 0
=> true
irb(main):028:0> !!true
=> true
irb(main):029:0> !!!true or raise RuntimeError
RuntimeError: RuntimeError
irb(main):030:0> true and false
=> false
irb(main):031:0> p true and false
true
=> false

フムフム.