RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

Ansibleでバージョンアップ作業を自動化する

デベロッパーのkyosimotoです。

Ansibleをバージョンアップ作業の自動化ツールとして導入するための手順、おすすめ構成などについて紹介させていただきます。

目次

なぜAnsible

私は担当サービスのバージョンアップ作業を自動化するため、シェルベースのスクリプトを開発・運用してきました。 スクリプト導入により、ほとんどの作業は自動化され、運用コストは大幅に削減されました。

半面、バージョンアップスクリプトの属人化が課題となっており、リリース時のトラブルが起きた場合に、作業担当者による 原因調査と復旧作業が難しい状態となっています。

シェルスクリプトで作成したバージョンアップスクリプトには、フレームワークのような拘束力が無く、長い運用の中でイレギュラーケースに対応したコードやフラグが追加され、複雑化の道をすすみ続ける可能性があります。

さらには、今のところこの複雑化したスクリプトをポジティブに学習したいという人はいません。問題が起きたとしても、開発担当者に聞けばすぐに解決できるからです。

私は属人化を解消するためには、シェルスクリプトによる実装をやめ、学習コストが低く複雑なコードを生まない自動化ツールの導入が必要と考えており、この要件にマッチしたAnsibleを導入提案することになりました。

どんな感じ?

今のところメリットと感じているのは下記3点です。

  • 学習コストが低い
    設定ファイルはYAMLという形式で記述します。設定ファイルがシンプルで、初めての人でも内容をすぐに理解できると思います。
    運用作業用のモジュールが充実していますので、プログラミング書くことも読むこともほとんどありません。

  • 導入コストが低い
    管理対象サーバには余計なツールやデーモンをインストールする必要がありません。
    SSHPythonさえ使えれば、Ansibleからの操作が可能ですので、運用チームへも提案しやすいと思います。

  • 運用コストが低い
    少しの工夫で設定ファイルをそのまま手順書として扱うことができます。

Ansibleの基本

実行方法

Ansibleの実行コマンドは以下の通りです。

$ ansible-playbook -i {Inventory} {Playbook}   

Inventoryには、サーバ名やIPアドレスなどの管理対象ノードの情報、Playbookには管理対象ノードで実行するタスクを記述します。

実行イメージ

Ansibleは、上記コマンドを実行するとPlaybookの内容をPythonのプログラムに変換します。
変換されたプログラムファイルは、Inventory(インベントリ)に記述された管理対象ノードに転送後に実行されます。

f:id:kyoshimoto:20170926152405p:plain

マシン要件

対象 要件
コントロールマシン Python 2 (version 2.6 or 2.7)、またはPython3(version 3.5以上)がインストールされている。
Windowsはサポート対象外。  (詳細)
管理対象ノード SSH接続できる。
Python 2.6以上がインストールされている。  (詳細)

ファイル構成

私のお勧めするファイル構成サンプルを紹介します。

ディレクトリ構成(サンプル)
myapp_verup  
    product.ini            # inventory (本番環境のホスト名/IPを記述)
    staging.ini            # inventory (ステージングのホスト名/IPを記述)
    development.ini        # inventory (社内検証環境のホスト名/IPを記述)
    versionup.yml          # playbook (バージョンアップ手順を記述)
    roles/                 # 具体的バージョンアップ手順を実装するディレクトリ
        apacheを停止する/
        apacheを起動する/
        apacheをバージョンアップする/
        cronを停止する/
        cronを起動する/  
        postgresqlを停止する/
        postgresqlを起動する/
        postgresqlをバージョンアップする/
        アプリケーションをバージョンアップする/
playbook(サンプル)

以下、playbookファイルの内容です。
- myapp_verup/versionup.yml

---
- hosts: all
  roles:
    - cronを停止する

- hosts: webservers
  roles:
    - apacheを停止する
    - apacheをバージョンアップする
    - phpをバージョンアップする
    - アプリケーションをバージョンアップする

- hosts: dbservers
  roles:
    - postgresqlを停止する
    - postgresqlをバージョンアップする
    - postgresqlを起動する。

- hosts: webservers
  roles:
    - apacheを起動する

- hosts: all
  roles:
    - cronを起動する
ファイル構成のポイント

ファイル構成を考える上で、お勧めするポイントは下記2点です。

  • playbookを日本語で記述する playbookのタスクを日本語化することで、設定ファイルの可読性が上がる、playbookがそのまま手順書/ドキュメントになるという点でメリットが大きいと考えています。

  • バージョンアップ作業に集中する。
    バージョンアップ作業以外のタスクを含めないようにします。
    例えば、サーバ構成管理や冪等性のための実装を行うと、Ansibleの設定は複雑化します。 複雑化は属人化を進行させますし、(Serverspecなど)ツールを使ったテストなども検討する必要がでてくるでしょう。

検証環境の準備

ここからは、Ansibleの検証用環境の構築手順について記載します。

検証環境の説明

仮想マシンの構築に Vagrant + Virtualbox を利用します。 検証用に構築するサーバは以下の通りです。

ホスト名 IPアドレス OS MW/Tool
control 192.168.33.100 CentOS 6.9 Ansible
web 192.168.33.101 CentOS 6.9 Apache2.2 + PHP7.1
db 192.168.33.102 CentOS 6.9 PostgreSQL9.6

別のディストリビューションで検証したい場合は、Vagrant Cloudよりboxイメージを検索し、後述する「vagrant init」コマンドの引数に指定してください。

検証用仮想マシンの構築手順

# Vagrantの作業用ディレクトリを作成します。  
$ cd
$ mkdir -p vagrant_work/ansible_test
$ cd vagrant_work/ansible_test

# Vagrantの作業用ディレクトリを初期化します。    
$ vagrant init bento/centos-6.9


# 出力されたVagrantfileをエディタで修正します。
$ vi Vagrantfile
-----
# config.vm.box = "bento/centos-6.9"    # この行をコメントアウトし、以下の設定をコピペする。

config.vm.define "control" do |node|
  node.vm.box = "bento/centos-6.9"
  node.vm.hostname = "control"
  node.vm.network :private_network, ip: "192.168.33.100"
end

config.vm.define "web" do |node|
  node.vm.box = "bento/centos-6.9"
  node.vm.hostname = "web"
  node.vm.network :private_network, ip: "192.168.33.101"
end

config.vm.define "db" do |node|
  node.vm.box = "bento/centos-6.9"
  node.vm.hostname = "db"
  node.vm.network :private_network, ip: "192.168.33.102"
end
-----

# 仮想サーバを起動します
$ vagrant up

# 仮装サーバが起動するまでしばらく待ちます。

仮想サーバにSSH接続する

$ vagrant ssh control
# パスワードは「vagrant」

Windowsの場合は、ターミナルソフトで接続します。

ホスト 192.168.33.100
user vagrant
password vagrant

Ansible実行環境の構築

AnsibleのインストールとSSH設定の手順について記載します。

Ansibleのインストール

# Vagrantの作業ディレクトリより仮想サーバにSSH接続します。
$ vagrant ssh control
# パスワードは「vagrant」

# EPELリポジトリを追加
$ sudo yum install -y epel-release

# Ansibleのインストール 
$ sudo yum install -y ansible

# Ansibleのバージョン確認  
$ ansible --version
ansible 2.3.2.0 
  config file = /etc/ansible/ansible.cfg  
  configured module search path = Default w/o overrides   
  python version = 2.6.6 (r266:84292, Aug 18 2016, 15:13:37) [GCC 4.4.7 20120313 (Red Hat 4.4.7-17)]

SSH接続設定

接続先サーバへのSSH接続を簡単にするためSSH設定を記述します。

$ vi ~/.ssh/config
------------
Host *
  StrictHostKeyChecking no
  UserKnownHostsFile=/dev/null

Host web
  HostName 192.168.33.101
  User vagrant

Host db
  HostName 192.168.33.102
  User vagrant
------------

# 設定ファイルのパーミッションを変更します。
$ vi ~/.ssh/config
------------
Host *  
  StrictHostKeyChecking no  
  UserKnownHostsFile=/dev/null  
    
Host web    
  HostName 192.168.33.101   
  User vagrant  
    
Host db 
  HostName 192.168.33.102   
  User vagrant  
------------
    
# 設定ファイルのパーミッションを変更します。   
$ chmod 600 ~/.ssh/config
    
# SSHの公開鍵を登録します。  
$ ssh-keygen -t rsa
Generating public/private rsa key pair. 
Enter file in which to save the key (/home/vagrant/.ssh/id_rsa):  # Enterキー入力
Enter passphrase (empty for no passphrase):  # Enterキー入力
Enter same passphrase again:   # Enterキー入力
Your identification has been saved in /home/vagrant/.ssh/id_rsa.    
Your public key has been saved in /home/vagrant/.ssh/id_rsa.pub.    
The key fingerprint is: 
# 秘密鍵と公開鍵が作成されます。
~/.ssh/id_rsa
~/.ssh/id_rsa.pujb

# 作成したSSH公開鍵を接続先サーバにコピーします。 
$ ssh-copy-id web
# パスワードは「vagrant」   
$ ssh-copy-id db
# パスワードは「vagrant」   

# 接続先サーバにパスワードなしでアクセスできることを確認します。 
$ ssh web
$ hostname
web 
$ exit
    
$ ssh db
$ hostname
db  
$ exit

検証用仮想マシンミドルウェアセットアップ

バージョンアップ手順の検証用に、予めミドルウェアをセットアップします。

WEBサーバの構築 (Apache2.2 + PHP7.1)

# WEBサーバにSSH接続    
$ ssh web
    
# Apacheをインストール  
$ sudo yum install -y httpd
    
# PHPをインストール 
$ sudo yum install -y epel-release
$ sudo rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
$ sudo yum -y --enablerepo=remi-php71,epel install php php-cli php-common php-mbstring php-mcrypt php-pdo php-xml php-json php-devel php-pecl-zip php-pgsql
$ sudo service httpd restart
$ sudo chkconfig httpd on
    
# テストページを作成してApache+PHPの連携確認   
$ sudo vi /var/www/html/phpinfo.php
--------------
<?php
phpinfo();
--------------
# ブラウザから http://192.168.33.101/phpinfo.php にアクセスできることを確認 

# SSH接続を終了   
$ exit

DBサーバの構築(PostgreSQL9.6)

# DBサーバにSSH接続 
$ ssh db

# PostgreSQLのインストール  
$ sudo yum install -y https://yum.postgresql.org/9.6/redhat/rhel-6.9-x86_64/pgdg-redhat96-9.6-3.noarch.rpm
$ sudo yum -y install postgresql96-server
    
# PostgreSQLの初期設置    
$ sudo service postgresql-9.6 initdb
$ sudo vi /var/lib/pgsql/9.6/data/pg_hba.conf
--------------
# 末尾に追加  
host    all            all              192.168.33.101/32        trust 
--------------

$ sudo vi /var/lib/pgsql/9.6/data/postgresql.conf
# 接続設定追加(59行目あたり)  
--------------
#listen_addresses = 'localhost'    
listen_addresses = '*'   
--------------

# PostgreSQLの再起動   
$ sudo service postgresql-9.6 start
$ sudo chkconfig postgresql-9.6 on
    
# データベース&テーブル作成  
$ sudo su - postgres -c "psql"

# ココからはSQLモード  
> \c test
> create database test;
> create table t_staff (id int, name text);
> insert into t_staff values (1, 'あああああ'), (2, 'いいいいい');
> \q
# SSH接続を終了   
$ exit

WEB/DBサーバの連携チェック

# WEBサーバにSSH接続    
$ ssh web

# WEBサーバとDBサーバ間の疎通確認 
$ sudo vi /var/www/html/test.php
---------
<?php  
$connectString = "host=192.168.33.102 port=5432 dbname=test user=postgres";    
$conn = pg_connect($connectString);   
$result = pg_query($conn, "select * from t_staff");    

var_dump(pg_fetch_all($result));    
---------   

# ブラウザから下記URLにアクセスして、DBレコードが出力されることを確認   
http://192.168.33.101/test.php    

プロジェクトディレクトリ作成

ファイル構成の項目で紹介したAnsibleプロジェクトを作成していきましょう。

バージョンアッププロジェクト用のディレクトリ作成

# HOMEディレクトリにプロジェクトディレクトリを作成します。  
$ mkdir ~/myapp_verup

# フォルダを作成します   
$ cd ~/myapp_verup
$ mkdir -p roles/apacheを停止する/tasks
$ mkdir -p roles/apacheを起動する/tasks
$ mkdir -p roles/apacheをバージョンアップする/tasks
$ mkdir -p roles/cronを停止する/tasks
$ mkdir -p roles/cronを起動する/tasks
$ mkdir -p roles/postgresqlを停止する/tasks
$ mkdir -p roles/postgresqlを起動する/tasks
$ mkdir -p roles/postgresqlをバージョンアップする/tasks
$ mkdir -p roles/アプリケーションをバージョンアップする/tasks

Inventoryの作成

Inventoryでは、管理対象ノードのホスト名、またはIPアドレスとグループの定義を行います。 ファイルはINIファイル形式で記述します。

  • myapp_verup/development.ini
[webservers]
192.168.33.101

[dbservers]
192.168.33.102
Inventoryの作成(補足)

Inventoryを本番環境用、ステージング環境用、開発環境用に分けて管理することで バージョンアップ対象の切り替えできるようにします。

以下サンプルです。

  • myapp_verup/product.ini (本番環境用)
# 本番環境用のInventory
[webservers]
192.168.34.101
192.168.34.102
192.168.34.103

[dbservers]
192.168.34.104
192.168.34.105
  • myapp_verup/staging.ini (ステージング環境用)
# ステージング環境用のInventory
# セクション名(グループ名)は同じでIPアドレスのみ異なる。
[webservers]
192.168.35.101
192.168.35.102
192.168.35.103

[dbservers]
192.168.35.104
192.168.35.105

Playbookの作成

Playbookには、バージョンアップ手順を記述します。
(どのサーバでどんなタスクをどのような順番で実行するかを記述します)

  • myapp_verup/versionup.yml
---
- hosts: all
  roles:
    - cronを停止する

- hosts: webservers
  roles:
    - apacheを停止する
    - apacheをバージョンアップする
    - phpをバージョンアップする
    - アプリケーションをバージョンアップする

- hosts: dbservers
  roles:
    - postgresqlを停止する
    - postgresqlをバージョンアップする
    - postgresqlを起動する

- hosts: webservers
  roles:
    - apacheを起動する

- hosts: all
  roles:
    - cronを起動する

ファイルはYAML形式となりますので、拡張子は「yml」、1行目は「---」としてください。

2行目以降にバージョンアップ手順を記述します。書き方のルールは以下の通りです。

セクション 解説
hosts inventoryに定義したグループ名を記述します。
"all"の場合、inventoryに記述した全サーバを対象に処理を実行します。
roles hostsセクションに指定したサーバで実行するタスクを記載します。
タスクは「◯◯を停止する」「◯◯を起動する」「◯◯をバージョンアップする」くらいの粒度で記述しておき、具体的な処理内容をroles配下のタスク名と同名のディレクトリ配下に実装します。

ansible.cfgの作成

Ansibleの動作設定を記述するファイルで、INI形式で記述します。
- myapp_verup/ansible.cfg

[defaults]
# 実行時のログを出力するファイルを指定します。
log_path=/tmp/ansible.log

[privilege_escalation]
# タスクの実行ユーザをrootに設定します
become = true
become_user = root

Roleの作成

Playbookのrolesディレクティブに記述したタスクの実態をrolesディレクトリの配下作成します。 例えば、「Cronを起動する」というタスクは、roles/Cronを起動する/tasks/main.yml に、処理内容を記述します。

ミドルウェアの起動

service コマンドが準備されているミドルウェア(デーモン)であれば、「service」モジュールを使って以下のように記述します。

  • myapp_verup/roles/cronを起動する/tasks/main.yml
---
- name: crondを起動する
  service:
    name: crond 
    state: started
  • myapp_verup/roles/apacheを起動する/tasks/main.yml
---
- name: Apacheを起動する
  service:
    name: httpd
    state: started
  • myapp_verup/roles/postgresqlを起動する/tasks/main.yml
---
- name: PostgreSQLを起動する
  service:
    name: postgresql-9.6
    state: started

※「service」モジュールの使い方はこちら

(補足)ミドルウェアの起動

service コマンドが提供されていないミドルウェアの場合は、「shell」モジュールと「wait_for」モジュールで実装することもできます。

---            
- name: 起動コマンドを実行する          
  shell: /usr/local/myapp/apache/bin/apachectl start

- name: 80番ポートの疎通確認が終わるまで待機する
  wait_for:  
    host: localhost
    port: 80    
    state: started
    delay: 1
    timeout: 60

shell モジュールは終了ステータスコードが0以外は、すべてエラーとして処理を中断しますので注意が必要です。 ステータスコードが0以外でも処理を継続する場合には、下記サンプルを参考にしてください。

---
- name: スクリプトを実行する
  shell: /usr/local/myapp/bin/hoge.sh
  register: exitStatus
  failed_when: exitStatus.rc not in [0, 100]   # 終了ステータスが0 or 100の場合はエラーにしない        

※「shell」モジュールの使い方はこちら
※「wait_for」モジュールの使い方はこちら

ミドルウェアの停止

service コマンドが準備されているミドルウェア(デーモン)であれば、「service」モジュールを使って以下のように記述します。

  • myapp_verup/roles/cronを停止する/tasks/main.yml
---
- name: crondを停止する
  service:
    name: crond 
    state: stopped
  • myapp_verup/roles/apacheを停止する/tasks/main.yml
---
- name: Apacheを停止する
  service:
    name: httpd
    state: stopped
  • myapp_verup/roles/postgresqlを停止する/tasks/main.yml
---
- name: PostgreSQLを停止する
  service:
    name: postgresql-9.6
    state: stopped

ミドルウェアのバージョンアップ

RPMyumコマンドでバージョンアップできる場合は、「yum」モジュールを利用して「latest」の状態に更新します。

  • myapp_verup/roles/apacheをバージョンアップする/tasks/main.yml
---
- name: RPMを更新する
  yum:
    name: httpd
    state: latest
  • myapp_verup/roles/phpをバージョンアップする/tasks/main.yml
---
- name: RPMを更新する
  yum: 
    name={{ item }}
    state=latest
    enablerepo=remi,epel
  with_items:
    - php
    - php-cli
    - php-common
    - php-mbstring
    - php-mcrypt
    - php-pdo
    - php-xml
    - php-json
    - php-devel
    - php-pecl-zip
    - php-pgsql
  • myapp_verup/roles/postgresqlをバージョンアップする/tasks/main.yml
---
- name: RPMを更新する
  yum: 
    name=postgresql96-server
    state=latest

リポジトリ管理されていない(カスタムRPMをつかっている)場合は、以下設定を参考にしてください。

myapp_verup
  └ roles
       └ Apacheをバージョンアップする
             ├ tasks
             │  └ main.yml
             ├ files
             │  └ myapp_apache2.2.99.rpm # カスタムPRMを格納
             ├ templates
             │  └ httpd.conf.j2          # テンプレートファイルを格納。拡張子は「.j2」にする。(テンプレートエンジン「Jinja2」を利用)
             └ vars
               └ main.yml               # RPMのファイル名やチェックサム値などを記述する

ディレクトリ構成については公式ページのBest Practices の「Directory Layout」を参考にしています。

  • myapp_verup/roles/Apacheをバージョンアップする/tasks/main.yml
---
- name: RPMファイルを転送する
  copy: src=files/{{ rpm_file_name }} dest=/tmp 

- name: RPMファイルの状態を取得する
  stat:
    path: /tmp/{{ rpm_file_name }}
  register: file_status

- name: チェックサム値を確認する
  fail: msg='MD5 value did not match'
  when: file_status.stat.md5 != rpm_file_md5

- name: "RPMを更新する ({{ rpm_file_name }})"
  shell: rpm -Uvh --force --nodeps /tmp/{{ rpm_file_name }}
  args:
    chdir: "/tmp"
  register: verup_result

- name: ステータスコードを確認する
  fail: msg="Failed upgrade rpm."
  when: verup_result.rc != 0

- name: httpd.confを差し替える
  template:
    src=httpd.conf.j2
    dest=/usr/local/vanguard/apache/conf/httpd.conf
    owner=root
    group=root
    mode=644

※「copy」モジュールの使い方はこちら
※「stat」モジュールの使い方はこちら
※「fail」モジュールの使い方はこちら
※「template」モジュールの使い方はこちら

  • myapp_verup/roles/Apacheをバージョンアップする/templates/httpd.conf.j2
...中略...
# サーバ毎に異なる設定は 2重波括弧と変数名を記述しておきます。
ServerName {{ apache_server_name }}:80
...中略...

※ 上記変数部分は、例えばInventoryファイルに記述した変数の値に自動で 置き換えることができます。

...中略...
[web:vars]
apache_server_name = myapp.example.com
...中略...
  • myapp_verup/roles/Apacheをバージョンアップする/vars/main.yml
---
# RPMファイル名を記述します。
# この値はtask/main.ymlで参照されます。
rpm_file_name: "vg_httpd-2.4.25-centos6.x86_64.rpm"

# RPMファイルのチェックサム値を記述します。
# この値はtask/main.ymlで参照されます。
rpm_file_md5: "4f8009b1cbcf5dbc7f082773d2f0d661"

playbookの実行

$ cd ~/myapp_verup
$ ansible-playbook -i development.ini versionup.yml

実行結果は以下の通りです。
日本語で書いたタスクがそのままターミナル上のログに出力されていることが確認できます。
(/tmp/ansible.log にも出力されます。)

f:id:kyoshimoto:20170926174322p:plain

最後に

本記事は、運用チーム向けにAnsibleを使ったバージョンアップ自動化提案後に書いた記事です。
本番運用が開始されれば、いろいろと課題はでてくると思いますので、知見がたまりましたら
改めて情報を共有させていただこうと思います。

以上、ありがとうございました。


  • エンジニア中途採用サイト
    ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
    ご興味ありましたら是非ご確認をお願いします。
    20210916153018
    https://career-recruit.rakus.co.jp/career_engineer/

  • カジュアル面談お申込みフォーム
    どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
    以下フォームよりお申込みください。
    forms.gle

  • イベント情報
    会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! rakus.connpass.com

意図しない処理が実行されるCSRFとは?概要と対策

はじめに

 こんにちは、mickey-STRANGEです。昨年に新卒でラクスに入社しました2年目です。

 新卒に毛が生えた程度の新米エンジニアですが、今回はその数少ない毛の中から学生時代に意識したことのなかったものという観点で脆弱性のお話を選び、記事にしました。新しくWeb開発企業に入社した新卒の方の学習の手助けになればと思います。

目次

脆弱性とは?

 脆弱性とは、セキュリティの面からみたシステムの欠陥のことです。 脆弱性をついて攻撃されてしまうと、ユーザには本来許されていない操作ができてしまったり、サーバにウィルスを仕込まれてしまったりという被害が出ます。

 この記事では脆弱性の中でも、私が実際に理解に時間のかかった

という脆弱性の概要と対策をご紹介したいと思います。

CSRF(クロスサイトリクエストフォージェリ)

概要

 サービスの利用者に意図しないHTTPリクエストを送信させ、利用者の意図しない処理をサービスに実行させる攻撃です。と、1文で書いてはみるものの、これだけで理解は出来ないと思います(私が勉強したときは理解出来ませんでした)ので、shop.example.comという架空の通販サイトで簡単な例を挙げてみましょう。

 shop.example.comではユーザはIDとパスワードでログインします。利用ユーザがパスワードを変更しようとしたとき(ユーザが新しいパスワードを入力して確定ボタンを押したとき)に送信されるリクエストが下のようなものだったとします。

URL
 http://shop.example.com/password/change
パラメータ
 new_pass:【新しいパスワード】
 new_pass_conf:【新しいパスワード(確認入力用)】

 このリクエストと同じものを作成して送信させる悪意のあるWebページを攻撃者が用意します。 偽装リクエストを送信さえできればよいのでWebページの用意は難しいことではありません。具体的なコードは示しませんが、javascriptを使って数行で実現出来ます。GETでよければ他の手段でもっと単純に、コードとしては1行でも十分に実現できてしまいます。(確定処理でGETを使用すること自体が論外ですが。)

 さて、shop.example.comのユーザAさんが、攻撃者の用意した悪意のあるWebページを開いてしまったとしましょう。

 するとAさんのブラウザから上記リクエストと同じものが勝手に送信されてしまいます。正規にパスワード変更画面で確定ボタンを押したときのリクエストと違うところがあるとすれば、パラメータのパスワード部分は攻撃者しか知らないものである、ということだけです。 このときAさんのブラウザに「shop.example.comにログインしている状態のセッション情報」が残っていた場合、その情報もブラウザが同時に送信してしまいます。 そのリクエストを受け取ったサーバ側のプログラムはAさんが操作を再開して、パスワードの変更確定ボタンを押したと誤認し、パスワード変更の処理を行ってしまいます。パスワード変更の処理が完了するとAさんはもうログイン出来なくなってしまいます。

 CSRFログイン済みのユーザに、意図しない操作を強制的に実行させてしまう攻撃であるといえます。

 今回の例ではパスワード変更ですが、これが購入や決済の確定処理で起きてしまうと取り返しがつかないということは簡単に想像出来ると思います。ではCSRFがどのような脆弱性か分かったところで、CSRF攻撃を防ぐためにどのような仕組みを入れればよいかを考えてみようと思います。

対策

 ではCSRFの対策を考えてみましょう。 上記の通り、CSRFにおいてサーバはリクエスト通りに1つの機能を正しく完了しているという特徴があります。つまり対策として考えられるのは受け取ったリクエストに対して処理を実行するかしないかを確認することになります。

確定処理の前に認証を行う

 一番分かりやすい方法はユーザに確認してもらうことです。重要な処理の前にはユーザにもう一度認証を行う、つまりログインIDとパスワードの入力をしてもらうことになります。今回のパスワード変更の例ですと、以下のようになります。(数字がユーザ操作、→がサーバ処理です。)

1.新しいパスワードを入力して確定ボタン
2.認証画面でログインIDとパスワードを入力
 →変更処理を行う

 この順番で操作してもらうようになっていれば、今回の例のように1番のリクエストを偽装されても処理を行ってしまうことはありません。

 しかし、この方法で対策するとユーザの操作が増えてしまいます。大事な処理の前だけとしても、ユーザの操作量が増えてしまったり、直感的に進めない画面が表示されると「このサイトは使いにくい、面倒だ」と感じてしまうかもしれません。ユーザに負担をお願いしたくない、ということで次にプログラム側だけで出来る対策を考えてみます。

リファラを確認する

 ユーザ操作を増やさずに出来るCSRF対策は「送られてきたリクエストが正しいものか確認する」ことです。ここで正しいリクエストとは、サイト内にある確定ボタンを押すことで送信されたリクエストということになります。それを確認するために、リファラというHTTPヘッダの1つを利用します。

 リファラとは、リクエスト元のページのURLを示すHTTPヘッダです。処理の前にリファラの値がhttp://shop.example.comから始まっているかを確認すれば、偽装リクエストかどうか判定することが可能です。

1.新しいパスワードを入力して確定ボタン
 →リファラのチェックを行う
 →変更処理を行う

 しかし、この方法ではまだ完璧な対策ではありません。リファラはその特性上、プライバシー面に問題を抱えており、ブラウザのでリファラを送信しない設定が可能です。また、前述のとおり、リファラはHTTPヘッダの1つです。改竄されてしまっては元も子もありません。

ワンタイムトークンを利用する

 ワンタイムトークンとは、リクエストが正しいものか判断するための文字列をリクエストの中に仕込む手法です。

 今回のパスワード変更の例ですと、以下のような流れになります。

0.パスワード変更画面を開く
 →乱数文字列(トークン)を生成
1.新しいパスワードを入力して確定ボタン(トークンを付与したリクエストを送信)
 →サーバ側で保持しておいたトークンとリクエストで送信されたトークンの比較を行う
 →変更処理を行う

 パスワード変更画面を開く際にトークン(乱数文字列)を作成し、セッションなどのサーバ側の領域で一時保存しておきます。また、確定のリクエストの時にそのトークンをパラメータに追加して送信させ、サーバ側でトークンの照合を行います。サーバ側で保存していたトークンと一致すれば処理を行う、一致しなければ不正なリクエストとして処理を行わない、といった判断が可能となります。

 偽装リクエストがもしトークンを勝手に付与したリクエストを送信してもサーバ側で同じトークンを保持していないので判別できますし、リファラとは違いヘッダではなくパラメータなのでユーザ依存で失敗したりすることがありません。今回紹介した中ではこの手法が最も適切だといえるでしょう。

おわりに

 CSRFの概要と対策についてご紹介いたしました。いかがでしたでしょうか。 XSS(クロスサイトスクリプティング)と名前が似ていることで勘違いしやすい脆弱性で、自分が理解しづらかったCSRFについて記事にしてみました。

 Web開発に関する勉強を始めたばかりの方の手助けになれば幸いです。


  • エンジニア中途採用サイト
    ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
    ご興味ありましたら是非ご確認をお願いします。
    20210916153018
    https://career-recruit.rakus.co.jp/career_engineer/

  • カジュアル面談お申込みフォーム
    どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
    以下フォームよりお申込みください。
    forms.gle

  • イベント情報
    会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! rakus.connpass.com

PhpStormで始めるGit生活

みなさんこんにちは。ぺちぱー歴5年のフジサワでございます。

今回は、PHP開発にはかかせない、PhpStormとGitを連携させて使用する方法をご紹介します。
有償ツールということもあってか、意外とPhpStormを使用したGitの利用方法について詳しくまとめられている記事が少ないので、いっそまとめてしまおうと思い至りました。

本記事では以下の環境をベースに解説しています。

OS:Windows 10 Home Edition
PhpStorm: 2017.2.3

なお、今回の記事では、「開発する上でよく使う機能」のうち、基本的な一部の機能しかご紹介していません。
他の機能については、また改めて記事にまとめ、投稿させて頂きます。

目次

PhpStormのGit関連のUIについて

まず初めに、PhpStorm上でGitを利用するためのUIについて簡単に解説します。

f:id:miracle-fjsw:20170912000334p:plain

VCSメニュー:[VCS] - [Git]配下にcommitやpushといったGitの操作メニューが表示されます。
②VersionContorolタブ:Git関連の情報を表示するペインの表示切替タブ
③各種Git情報タブ
 LocalChanges: プロジェクト中の未コミットファイルや非Git管理下のファイルを表示する。
 Log:コミットツリーを表示する。
 Console:PhpStorm上から実行した諸々の操作履歴
  ※GUI上からの操作はすべてGitクライアントにコマンドとして実行されており、ここにコマンド履歴が表示されます。
  トラブルシューティングの際はこのログを見ると、エラーや何が実行されているのか分かるので便利です。
④Git Branches:[Git:[現在のブランチ]]ブランチの切り替えや新規作成など、ブランチ関連の操作はここから実行します。

まずはここから [git clone]

何はともあれ、リポジトリをcloneしなければ何も始まりません。
まずは、リポジトリをcloneする方法について見てみましょう。

①PhpStorm起動時のウィザードから[Check out from Version Control] - [Git]を選択する。

f:id:miracle-fjsw:20170911235812p:plain:w500

リポジトリのURLやディレクトリの情報を入力して[clone]ボタンを押す。

f:id:miracle-fjsw:20170912000048p:plain

 Git Repository URL: clone対象のリポジトリのURL
 Parent Directory: clone先の親ディレクト
 Directory Name: clone先のディレクトリ名

なお、すでに何かしらのプロジェクトを開いている場合は、以下の手順でもcloneを実行することが可能です。

[VCS] - [Check out from Version Control] - [Git]を選択する。

f:id:miracle-fjsw:20170912013421p:plain

新しいブランチを作成する [git branch]

プロジェクトに対して何か変更を加えるため、新しいブランチを作成しましょう。

①画面右下の[Git Branches]を押下し、表示されるポップアップから[New Branch]を選択する。

f:id:miracle-fjsw:20170912001207p:plain

②作成したいブランチの名前を入力し[OK]ボタンを押下する。

f:id:miracle-fjsw:20170912001304p:plain

ブランチの作成に成功すると、完了メッセージが表示され、現在作業中のブランチが切り替わります。

f:id:miracle-fjsw:20170912014706p:plain

変更内容をコミットする [git commit]

ソースコードに加えた変更をコミットする方法を見てみましょう。

今回はサンプルとして、プロジェクト中のfile-A.phpとfile-C.phpに変更を加えました。

①[VCS] - [Git] - [Commit FIle...] またはツールバーから[Commit]ボタンを選択する。

f:id:miracle-fjsw:20170912001655p:plain f:id:miracle-fjsw:20170912001710p:plain

②コミット対象のファイルにチェックを入れ、コミットメッセージを入力する。

f:id:miracle-fjsw:20170912001814p:plain

③ダイアログ右下の[commit]を押下し、コミットを実行する。

f:id:miracle-fjsw:20170912001925p:plain

ここで[commit and push..]を選択することで、コミットとプッシュを一度に実行できますが、ここではコミットのみ実行します。

④[Verstion Control]から[Log]タブを選択し、ブランチツリーを確認する。

先ほどコミットした内容がブランチツリーに反映されました。

f:id:miracle-fjsw:20170912002030p:plain

なお、作業ディレクトリ内の未コミットのファイルは以下の手順で確認することができます。

①[Verstion Control]から[Local Changes]タブを選択し、作業ディレクトリの状態を表示する。

f:id:miracle-fjsw:20170912001523p:plain

ファイル名を右クリックし、[Show Diff]を選択することで、最新のコミットとの差分を表示することができます。

ローカルの変更をリモートにプッシュする [git push]

それでは、ローカルの変更をリモートに反映するため、プッシュを実行しましょう。

①[VCS] - [Git] - [Push...]を選択する。

f:id:miracle-fjsw:20170912002207p:plain

ダイアログが表示され、これからプッシュする差分の情報などが表示されます。

f:id:miracle-fjsw:20170912002248p:plain

②[Push]ボタン - [Push]を選択し、プッシュを実行する。

f:id:miracle-fjsw:20170912002336p:plain

作業中のブランチを切り替える [git checkout]

次に、作業中のブランチを切り替える方法について確認します。

①画面右下の[Git Branches]を押下し、表示されるポップアップから切り替えたいブランチ名を選択し、[checkout]を選択する。

f:id:miracle-fjsw:20170912002650p:plain

上の例は、既にローカルブランチとして存在するブランチに切り替える場合を説明したものです。 まだローカルブランチとして存在しないブランチに切り替える場合は、次の通りです。

①画面右下の[Git Branches]を押下し、表示されるポップアップから切り替えたいブランチ名を選択し、[Checkout as new branch]を選択する。

f:id:miracle-fjsw:20170912002827p:plain

②作成するブランチ名を入力する。

f:id:miracle-fjsw:20170912002904p:plain

ローカルブランチとして展開する際に、任意の名前を付けることが可能ですが、特別な理由がない限り、基本的にはデフォルトで入力されている名前をそのまま使用すれば良いでしょう。
[OK]ボタンを押下すると、ブランチの切り替えが完了します。

さて、PhpStormでGitを利用するうえで、混乱が生じやすいLocalBrancesとRemoteBranchesについて簡単に補足しておきます。

f:id:miracle-fjsw:20170912003436p:plain

LocalBranches … ローカル上に存在するブランチ。コミットはこのブランチに対して実行される。
RemoteBranches … リモート追跡ブランチ。リモートリポジトリとの同期に使用するブランチで、fetchを実行するとリモートリポジトリの最新の状態が反映される。

ブランチを最新化する [git fetch/merge]

別の開発者によって、リモートリポジトリ側でブランチに加えられた変更内容を取り込む方法を見てみましょう。
ここではmasterブランチに変更が加えられた場合を例に、最新化する手順を確認します。

①[VCS] - [Git] - [fetch]を選択し、RemoteBranchesにリモートリポジトリの変更を取り込む。

f:id:miracle-fjsw:20170912003525p:plain

②作業ディレクトリを、最新化したいブランチに切り替える。

③[Git Branches]から、最新化したいブランチに対応したリモートブランチを選択し、[Merge]を選択する。

f:id:miracle-fjsw:20170912003756p:plain

上記の操作は、指定したブランチの変更内容を、現在checkoutしているブランチに取り込む操作です。
これでリモート側の変更をローカルに反映することができました。

少々、fetchとmergeの仕組みはわかりにくいので、簡単に解説します。
fetch前とfetch後で、ブランチツリーがどのように変化しているかを確認してみましょう。

f:id:miracle-fjsw:20170912003608p:plain

ブランチツリー上でコミット履歴にマウスオーバーすると、そのコミットを指しているブランチの一覧を表示することができます。
fetch前の状態を見ると、ローカルブランチとmasterとリモート追跡ブランチのorgin/masterが同じコミットを指していることがわかります。
fetch後の状態をみると、ローカルブランチのmasterが指すコミットは変わりませんが、origin/masterが別の開発者によって行われたコミットを指している状態になっていることが分かります。
この状態では、まだ作業ディレクトリのソースコードに変化はありません。

f:id:miracle-fjsw:20170912003709p:plain

では、fetchの後、mergeを実行するとどうなるでしょうか。
ブランチツリーの変化とソースコードの変化を見てみましょう。

f:id:miracle-fjsw:20170912003831p:plain

mergeを実行すると、ローカルブランチmasterの指すコミットが、origin/masterと同じコミットを指すようになりました。
この時、ソースコードの方はどう変化しているでしょうか。

f:id:miracle-fjsw:20170912003911p:plain

ご覧の通り、リモート側の変更がソースコードに反映されています。

なお、一連の作業は[VCS] - [Git] - [Pull...]で一括実行することも可能です。

別のブランチの変更を取り込む [git merge]

それでは、別のブランチに行われた変更を取り込む方法を見てみましょう。

例では、[master]から派生した別のブランチ[merge-test]に行われた変更を取り込む流れを確認します。

f:id:miracle-fjsw:20170912004813p:plain

①[VCS] - [Git] - [fetch]を選択し、RemoteBranchesにリモートリポジトリの変更を取り込む。

②作業ディレクトリを、最新化したいブランチに切り替える。

③[Git Branches]から、マージしたいブランチを選択し、[Merge]を選択する。

f:id:miracle-fjsw:20170912004823p:plain

マージが実行されると、デフォルトの設定では以下のようにマージログが追加されます。(fast-forwardについては割愛)
ブランチツリー上も、[master]ブランチと[merge-test]ブランチが統合されていることが分かります。

f:id:miracle-fjsw:20170912005017p:plain

ここでお気づきの方もいると思いますが、「あるブランチを最新化する」という操作と、「別のブランチの変更を取り込む」という操作は、Git上では本質的には同じです。
同じブランチ同士のマージか、異なるブランチ同士のマージかという違いだけで、いずれもブランチ間のマージが行われています。

不要になったブランチを削除する [git branch -D]

マージを実行したので、[merge-test]ブランチは不要になりました。 ここでは、ブランチの削除方法を見てみましょう。

①[Git Branches]から、削除したいブランチを選択し、[Delete]を選択する。

f:id:miracle-fjsw:20170912005744p:plain

なお、この操作はローカルブランチ、リモート追跡ブランチのいずれにも実行することが可能です。 リモート追跡ブランチを削除すると、自動的にリモートリポジトリ上のブランチを削除することができます。

ブランチ間の差分を見る [git diff]

続いて、ブランチ間の差分を確認する方法を見てみましょう。

f:id:miracle-fjsw:20170912011722p:plain

ここでは、[master]ブランチと[new-branch]の差分を比較してみます。

①[Git Branches]から、比較したいブランチを選択し、[Compare]を選択する。

f:id:miracle-fjsw:20170912011755p:plain

②ダイアログから[Files]タブを選択し、差分ファイル一覧を表示する。

f:id:miracle-fjsw:20170912011833p:plain

③確認したいファイルの名前をダブルクリックし、差分を表示する。

f:id:miracle-fjsw:20170912011921p:plain

コミット前のソースを元に戻す

ソースコードの修正をリセットしたい。そんな時はRevertを使用しましょう。

①[Verstion Control]から[Local Changes]タブを選択し、作業ディレクトリの状態を表示する。

②修正を元に戻したいファイルを右クリックする。

f:id:miracle-fjsw:20170912084018p:plain

コンテキストメニューから[revert]を選択する。

f:id:miracle-fjsw:20170912084036p:plain

おわりに

さて、いかがでしたでしょうか。
冒頭にも記載している通り、今回はPhpStormでGitを利用するための一部の基本的な機能しかご紹介していません。
別の機会に、rebaseやstash、conflictの解消方法などの重要な機能についても投稿させていただく予定です。

ではまた。


  • エンジニア中途採用サイト
    ラクスでは、エンジニア・デザイナーの中途採用を積極的に行っております!
    ご興味ありましたら是非ご確認をお願いします。
    20210916153018
    https://career-recruit.rakus.co.jp/career_engineer/

  • カジュアル面談お申込みフォーム
    どの職種に応募すれば良いかわからないという方は、カジュアル面談も随時行っております。
    以下フォームよりお申込みください。
    forms.gle

  • イベント情報
    会社の雰囲気を知りたい方は、毎週開催しているイベントにご参加ください! rakus.connpass.com

知ってる?nodemailerを使ってメールを送る方法

はじめに

はじめまして。新卒入社で3年目のNIR-AMAUQAです。
今回は少し前に触ったnodemailerについて記事を書こうと思います。
具体的にはnode.jsからnodemailerというモジュールを使って、メールを送信してみようと思います。

以前nodemailerを触る機会があったんですが、調査している時に日本語の資料が少なかったり、
見つけても古いバージョンのものが多かったので、記事にしてみようと思いました。

node.jsについて分からない人はこちらをどうぞ

Node.js を5分で大雑把に理解する - Qiita

いまアツいJavaScript!ゼロから始めるNode.js入門〜5分で環境構築編〜

nodemailerとは

2010年から作られているnode.jsからメール送信を可能にするモジュールで、多くのユーザに利用されています。
ソフトウェアのライセンスはMITです。

Nodemailer :: Nodemailer

目次

nodemailerをインストール

では早速、nodemailerをインストールしたいと思います。
npmでインストールする際にモジュール名のみだと最新版をインストールします。

$ npm install nodemailer

グローバルの場合は

$ npm install nodemailer -g

インストールの確認

$ npm list --depth=0

以下のように表示されていれば成功(2017/09/06 時点での最新が4.1.0です。)

nodemailer@4.1.0

グローバルの場合は上記のコマンドと同じく末尾に-gを入れて下さい。

もし過去バージョンをインストールしたい場合は以下の書き方でバージョンを指定できます。
@の後ろにバージョン番号を書いてください。

$ npm install nodemailer@4.0.1

サンプルプログラムを作成

//モジュールの読み込み
var nodemailer = require("nodemailer");

//SMTPサーバの設定
var smtp = nodemailer.createTransport({
    host: 'localhost',
    port: 25
});

//メール情報の作成
var message = {
    from: 'Fromアドレス',
    to: 'Toアドレス',
    subject: 'nodemailer test mail',
    text: 'テストメールです。'
};

// メール送信
try{
    smtp.sendMail(message, function(error, info){
        // エラー発生時
        if(error){
            console.log("send failed");
            console.log(error.message);
            return;
        }
        
        // 送信成功
        console.log("send successful");
        console.log(info.messageId);
    });
}catch(e) {
    console.log("Error",e);
}

詳細説明

SMTPサーバ設定
//SMTPサーバの設定
var smtp = nodemailer.createTransport({
    host: 'localhost',
    port: 25
});

ここでSMTPサーバの設定を行います。
今回はローカルのメールサーバを利用した最も簡単な例です。

nodemailerの記事で探すとGmailなどのSMTPサーバを利用しているものが多いので、
外部のSMTPサーバを利用したい方はそちらを参考にしてください。

nodemailerでGmailから送信するための方法 - Qiita

メールヘッダ作成
//メール情報の作成
var message = {
    from: 'Fromアドレス',
    to: 'Toアドレス',
    subject: 'nodemailer test mail',
    text: 'テストメールです。'
};

こちらも最もシンプルなメールヘッダの例になります。

私の調べた限りではReturn-Pathの設定はできないようです。
ただし、envelopeの設定はできるのでバウンスメールなどをコントロールしたい場合は以下のようにすれば可能です。

//メール情報の作成
var message = {
    from: 'Fromアドレス', // 表示名つきにする場合は'表示名<Fromアドレス>'
    to: 'Toアドレス',
  envelope: {
        from: 'envelopeFromアドレス',  // バウンスメールの戻り先アドレス
        to: 'envelopeToアドレス'    // 実際の送信先
    },
    subject: 'nodemailer test mail',
    text: 'テストメールです。'
};

envelopeを付与した場合はto: 'Toアドレス'はなくても送信可能ですが、メールヘッダのToが無くなってしまうので書いておくのが無難かと思います。

メール送信処理
// メール送信
try{
    smtp.sendMail(message, function(error, info){
        // エラー発生時
        if(error){
            console.log("send failed");
            console.log(error.message);
            return;
        }

        // 送信成功
        console.log("send successful");
        console.log(info.messageId);
    });
}catch(e) {
    console.log("Error",e);
}

メッセージの送信に失敗するとerrorオブジェクトが返ってきます。
送信先を消してメール送信に失敗した場合の例

$ node サンプルファイル名.js  
send failed
No recipients defined

メール送信に成功すると様々な情報が返されます。
公式のリファレンスによれば以下の情報が返されているそうです。

  • info.messageId
  • info.envelope
  • info.accepted
  • info.rejected
  • info.pending
  • response

英語が苦手な私が訳すと誤解させる恐れがありますので詳しくは公式のリファレンス*1で確認お願いします。

実行してみる

以下のようになればメール送信成功です。

$ node サンプルファイル名.js  
send successful
<Message-IDが出力されている>

まとめ

今回はnode.jsにnodemailerを入れてメールを送信してみました。
想定していたよりも手軽にメール送信できました。

node.jsのモジュールに関して日本語の記事が増えてくれると嬉しいので、
私のために記事を書いてくれると嬉しいです。


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

ソフトウェアテストについて簡単にまとめてみた

はじめに

はじめまして。開発エンジニアのamdaba_sk(ペンネーム未定)です。 ラクスに新卒で入社し、今年で2年目になります。

先日ラクスオフィス内にあります共用本棚に「知識ゼロから学ぶソフトウェアテスト 【改訂版】」を見つけました。

ちょうどテストコードの書き方に悩んでいたところで勝手に自主的にこれを読みましたので、少し内容をまとめてみようと思います。

少しネットで「ソフトウェアテスト」検索するとたくさんの記事がヒットしますが、めげずに書きます。

目次

ソフトウェアのテストを分類してみる

○○テストという言葉はたくさんありますが、まずは大きく工程・品質の観点・実行方法・技法に区別されます。

工程による分類

開発の工程に対応させた分類です。

開発者側の工程に対応するテスト
  • 単体テスト
    • ソフトウェアを構成する最小の要素に対するテスト
    • 言語や開発プロセスによって「単体」の定義が異なる
  • 統合(結合)テスト
    • 単体同士を組み合わせた全体に対するテスト
  • システムテスト
    • ソフトウェアの全機能に対するテスト
    • 運用時と同じインフラ、ハードウェア、ミドルウェアを用いて行う
顧客側の工程に対応するテスト
  • 受入テスト
    • 顧客が納品されたソフトウェアに対して行うテスト
    • 自社開発など、行われない場合も多い

品質の観点による分類

どういった品質を確かめる目的で行われるのかという視点に基づく分類です。

  • 機能テスト
    • 機能が正しく実装されているかどうかのテスト
  • 性能テスト・負荷テスト
    • ストレスなく使用できる程度の実行速度がでるかどうかのテスト
    • 負荷テストでは必要な性能を満たせる限界を見極める
  • ユーザビリティテスト
    • 「使いやすい」かどうかを確認するテスト
  • セキュリティテスト
    • 外部からの攻撃に耐えられるかどうかのテスト
  • etc...

実行方法による分類

その名の通り、テストの実行方法による分類です。

  • 動的テスト
    • テストのためにソフトウェアを実行するテスト方法
  • 静的テスト
    • ソフトウェアを実行せずに行うテスト
    • コードレビュー、静的解析、etc...

技法による分類

テストのためにはどのような操作をして何を確認するかを定めた「テストケース」を作成します。テストケース作成に用いる技法による分類です。

テストケース作成技法をまとめてみる

ホワイトボックステスト(制御パステスト)

ホワイトボックステストはプログラムの論理構造が正しいかどうかのテストです。デバッガでステップ実行などしながら、それぞれの行、それぞれのブロックで実行される文は正しく書かれているか、if分やswitch文の条件は適切か、きちんと終了まで実行されるかを確認します。

このテストの実行によってカバレッジが算出され、プログラムの品質を計る一つの指標となります。

ホワイトボックステストで焦点となるのはあくまでプログラムの論理構造なので、以下のような不具合は見つけられません。

  • 要求仕様自体の誤りや不備
  • データに関するバグ
  • マルチタスクや割込みに関するバグ

ブラックボックステスト

ブラックボックステストは名前の通りプログラムを一種のブラックボックスとして扱うテストで、様々な入力に対して妥当な出力が返されるかどうかを確認します。

ですが多くのプログラムでは可能な入力の組み合わせは膨大で、それらをすべて試すことは不可能です。そこで効果的な入力をもれなく選び取る方法が考案されています。

同値分割と境界値分析

同値分割と境界値分析は、ブラックボックステスト手法の中でも基本的な手法です。

同値分割では入力全体の集合を「同値クラス」という部分集合に分割します。

同値クラスは、同じ同値クラスの入力であればプログラムの動きに本質的な違いが出ないような入力の集合です。多くはプログラムが期待する入力値である「有効同値」、そしてそれ以外のあらゆる入力値である「無効同値」に分けられます。

それぞれの入力項目ですべての同値クラスの入力を行えば、あらゆる入力に対してテストされたことになります。

境界値分析では同値クラス同士の境界に注目します。

同値クラスの境界は条件文によって分けられることが多く、これを書き間違えることでバグになります。

そこで境界をまたぐもっとも近い入力の組を入力とすることで処理の切り替えがきちんとなされていることを確かめます。

例えば…

1から10までの自然数を受け付ける入力項目に対して

  1. 0を入力1(無効同値①、境界値)⇒ 入力エラーになる

  2. 1を入力(有効同値、境界値)⇒ 正常に処理される2

  3. 5を入力(有効同値)⇒ 正常に処理される

  4. 10を入力(有効同値、境界値)⇒ 正常に処理される

  5. 11を入力(無効同値②、境界値)⇒ 入力エラーになる

デシジョンテーブル

同値分割や境界値分析は、入力項目が複数ありさらにそれらが相関している時には大変ややこしくなります。デシジョンテーブルではそれらの入力の組み合わせを表にして、各組み合わせに対して期待される出力をまとめていく方法です。

複雑な状態が絡み合う機能のテストで有効です。

例えば…

入力項目が2つあって、両方に1から10までの自然数が入力された場合のみ処理がされる場合

条件 組み合わせ
入力項目Aが1-10の範囲内 Yes Yes No No
入力項目Bが1-10の範囲内 Yes No Yes No
動作 処理実行 エラー エラー エラー
状態遷移テスト

ソフトウェアによっては「状態」が複数存在し、操作することでそれらの状態間を行き来する場合があります。状態によって受け付ける入力や出力を変化させています。

状態遷移テストはそういった場合に、入力に対して正しく状態遷移(出力)するかどうかをテストします。

ランダムテスト

ランダムテストはこれまでのブラックボックステスト手法とは毛色が違っていて、事前にテストケースなどを作成せずにやみくもに入力や操作を行うテスト手法です。

結構なバグがこれで見つかるようですが、機能要求に対するテストではあまり有効でないとのことです。ただし、セキュリティに対するファジングテストという形でランダムテストが活用されています。

おわりに

参考図書、参考ページをもとに、ソフトウェアテストの分類と、その中でテストケース作成技法についてまとめてみました。簡単ではありましたが、この記事を読んでくれた方がソフトウェアテストの勉強をするきっかけになれたなら幸いです。

参考

  1. 高橋 寿一 (著) 知識ゼロから学ぶソフトウェアテスト 【改訂版】
  2. http://gihyo.jp/dev/serial/01/tech_station/0001?page=1
  3. http://b.hatena.ne.jp/entry/qiita.com/ktarow/items/8c3d94d6c21a0c86b799

◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com


  1. 0はいろいろな意味で特別なので、仕様的に境界値でなくともテストした方が良いです。

  2. 実際はここは仕様に合わせて具体的に書きます。

【図解】はじめてでもわかるJMeterの使い方

先日、仕事でJMeterを使わせていただく機会がありました。Y-Kanohと申します。 とはいえ、新卒2年目の私には何のことかさっぱりで...先輩に教えていただきながらの作業でした。

せっかくブログを書く機会があるので、同じ境遇の人が、「え、LatencyとSample Timeってどう違うの?」「実際にテストしたらコンピュータがフリーズした!!」「2時間たっても終わらないけど、どうすれば...(焦)」とならないように、簡単な例を用いてJMeterの使い方を紹介します。

そもそもJMeterって?

JMeterApacheソフトウェア財団が開発しているオープンソースの負荷検証ツールです。 サーバに対して指定した量のリクエストを送り、そのレスポンスを受けることで、パフォーマンス計測することができます。

JMeterApacheの公式サイトからダウンロードできます。 (Javaを入れていない方は、インストールしてからご使用ください。)

JMeterGUIモードとNon-GUIモードがありますが、ここではGUIモードの説明をします。 今回は例として、サーバ192.168.99.100のtest.htmlに2つのパラメータ「param1」と「param2」を送信する例を考えます。

スレッドグループの追加

以下がJMeterの起動画面です。

ダウンロードしたファイルのbinディレクトリにあるjmeter.bat(Windows) もしくはjmeter(Unix)を実行することで開きます。

まず、テスト計画を作成するため、スレッドグループを作成します。 左上の「テスト計画」を右クリックし、 追加 > Threads(Users) > スレッドグループ を選択してください。 新しいスレッドグループが作成されます。

スレッドグループの設定画面でも設定することがあるのですが、今はひとまず置いておきましょう。

送信するHTTPリクエストの追加と設定

次にスレッドグループで送信するリクエストを設定します。

今回は一番基本的なHTTPリクエストを送るため、 左画面のスレッドグループ名を右クリックし、追加 > サンプラー > HTTPリクエスト を選択し、 作成された「HTTPリクエスト」にてWebサーバとリクエストの設定を行います。

まず、リクエストを送信するWebサーバの設定。 「プロトコル」にはhttpを入力し、「サーバ名またはIP」には負荷検証を行うサーバを指定します。

次に、HTTPリクエストの設定です。

「メソッド」には、送信メソッドを指定します。とりあえず、今回は「GET」を指定。

「パス」には、リクエス送信先のリソースを指定します。

送信するリクエストパラメータを設定します。 画面下の「追加」ボタンから複数追加することができ、パラメータの名前と値をそれぞれ指定することができます。

これで、送信するHTTPリクエストを設定できました。

スレッドグループの設定

さて、HTTPリクエストが設定出来たら、スレッドグループの設定に戻りましょう。

スレッドグループでは、先ほど設定したHTTPリクエストをどのように、どれぐらいの量、どれぐらいの期間で送信するかを設定できます。*1

これらを決定するのが、スレッド数、Ramp-Up期間、ループ回数 です。

Ramp-Up期間

Ramp-Up期間が一番わかりやすいでしょうか。

Ramp-Up期間は、全リクエストの作成時間です。

「スレッド数」で設定したリクエスト群を、何秒間で作成するかを決めるのがRamp-Up期間です。

例えば、Ramp-Up期間を100(秒)、スレッド数を10とすると、 JMaterは100秒かけて10スレッド分のリクエストを送信しようとします。

ただし、Ramp-Up期間はあくまで「リクエストの作成時間」であり、テストの実行時間ではありません。 負荷検証するサーバの処理速度がテストに追いつかない場合は、この時間を大幅にオーバーしてしまいます。

もし、一定時間でテストを中断したい場合は、同画面の「スケジューラ」にチェックを入れて、終了時間を設定してください。

スレッド数とループ回数

スレッド数は、「リクエスト群を送信する回数」のことです。

ここで重要なのことは、スレッドは「リクエスト」ではなく「リクエスト群」であること。

1スレッドでは複数のリクエストを送信することができます。

では、この「リクエスト群」内のリクエスト数はどうやって設定するかというと、「ループ回数」で指定します。

要するに、ループ回数は「1スレッドで送信するリクエストの量」を決める値です。

テスト実行時に送られる総リクエスト数は、この「スレッド数」と「ループ回数」の積によって決まります。

総リクエスト数 = スレッド数 × ループ回数

スレッド数 = 総リクエスト数ではありませんよ。

この3パラメータの関係を時系列で図にまとめると、以下のような感じですね。

また、スレッドグループでは、スケジューラを用いることでテストの開始時間や終了時間を設定できます。

テスト実行中はリクエストを送信する端末にも負荷がかかるため、これで夜間などにテストを実行させておけば楽(?)できますよ。

テスト計画作成の注意点

スレッド数、Ramp-Up期間、ループ回数 を用いてテスト計画を作ることができますが、ここであまりに大量のリクエストを投げようとすると、 JMeterを実行する端末のCPUが食い散らかされてしまします。

デフォルト設定のJMeterは、たとえRamp-Up期間が長時間だったとしても、テスト開始と同時に、スレッド数で指定したスレッドを作ってから、テストを実行するそうで、スレッド数の量によっては、一発でフリーズしてしまいます。*2

この場合、スレッドグループの設定画面にあるDelay Thread creation until neededにチェックを入れることで、スレッドの作成をずらすことができます。*3

JMeter has an option to delay thread creation until the thread starts sampling, i.e. after any thread group delay and the ramp-up time for the thread itself. This allows for a very large total number of threads, provided that not too many are active concurrently.

Apache JMeter - User's Manual: Best Practices

リスナーの追加

テストの実行結果を表示するリスナーを追加します。

左画面のスレッドグループを右クリックし、 追加 > リスナー > 結果を表で表示 を選択します。

続けて 追加 > リスナー > 統計レポート も追加しておきます。

他にも下図のように様々な結果の表示方法があるので、用途によって使い分けてください。

ただし、あまりリスナーを増やしてしまうと、メモリをたくさん使ってしまいますので、必要最低限にする必要があります。

(特に、今回用いる「結果を表で表示」は、全サンプルの結果を表示するのでたくさんメモリを使ってしまうそうです。)

また、各リスナーは、テスト実施前にファイル名を指定することで、全てのデータをファイルに出力することができるので、 テスト結果に対して実行後に何らかの処理を加えたい場合は指定してください。

ここまでで作ったテスト計画は、ファイルとして保存できます。

テスト計画を選択したうえで保存ボタンを押下し、保存しておきましょう。

テスト計画の実行と結果の見方

いよいよテスト実施です。

まずはあまり負荷が高くない条件で動かしてみましょう。

画面上部の「開始」ボタンでテストを開始でき、テストを開始すると、画面右上のアイコンが緑色になり、終了すると灰色に戻ります。

以下の画面では、スレッド数:5、Ramp-Up期間:10秒、ループ回数:20 でテストを実施した場合の「結果を表で出力」画面です。

表項目それぞれの意味は次の通り。

項目名 意味
Sample リクエストの番号
StartTime リクエストの送信を始めた時間
Sample Time(ms) リクエストの送信からレスポンスを受け終わるまでにかかった時間
Status レスポンスのステータスを示す
Bytes 受信データのバイト数
SendByte 送信したバイト数
Latency リクエストを送ってからレスポンスが届いた時間
Connect Time(ms) JMeterがサーバとの接続確立にかかった時間

ちょっと時間にかかわる用語がややこしいので、図で説明します。 また、結果をファイルへ出力する際に出力できる「Elapsed time」についても併せて説明します。

JMeterが1リクエストを送信してレスポンスを受信するまでの処理は、上の図のように行われます。

Start Timeはその名の通り、処理を開始した時刻です。(結果をファイル出力した場合はマシンタイムで表されます。)

JMeterは、処理開始後、サーバとのHTTP接続の確立を行います。この接続の確立にかかる時間がConnect Timeです。 もし、このConnect Timeに時間がかかる場合、JMeterからの接続要求が待たされている可能性があるので、サーバの同時接続条件などを見直したほうがいいかもしれません。

JMeterは、接続が確立されると、リクエストを送信し始めます。

全てのリクエストが送信されると、サーバの処理が終わるまで待機し、サーバからレスポンスが返ってくるとレスポンスを受信し始め、すべてのレスポンス情報を受け取ると処理終了となります。

余談ですが、JMeterはブラウザと違い、受信したレスポンスに対して処理を行いません。 したがって、(当たり前ですが)JavaScriptのようなブラウザ側での処理は、JMeterの結果に影響することはありません。

Elapsed Timeはリクエストを送信し始める直前から、すべてのレスポンスを受信した直後までの時間です。

Latencyは、日本語に訳すと「潜時」と言って、「刺激が与えられてから反応するまでの時間」のことです。

JMeterでは、「処理開始時間」から、「最初のレスポンスが返ってきた直後」の時間を指します。

(少しややこしいのですが、JMeterの公式リファレンスの「Connect Time」の項によると、LatencyはConnect Timeを含んだ時間になるそうです。そのため、接続エラーが起きたリクエストでは、Connect Time = Latency になります。)*4

Sample Timeは、これらの処理すべてにかかった時間です。

ちなみに、全リクエストの通し番号である「Sample」は、この終了時間の昇順で割り当てられているらしいので、Start Timeが前後していても気にしなくていいそうですよ。*5

「統計レポート」リスナーには、全サンプルの統計情報が記載されます。

たくさんのリクエストを送信するテストの場合、どうしてもすべてを見ることは不可能なので、統計情報を参考にしてください。

おわりに

さあ、これでざっくりですが、JMeterの基本的な機能は使えるようになりました。

リクエスト数を増やして、いざ、テスト! ...と考えた方、ちょっと待ってください!

これだけ説明しておいてですが、ApacheGUIモードでの本番テストを推奨していません!! *6

Don't run load test using GUI mode !

 

GUI mode should only be used for creating the test script, NON GUI mode must be used for load testing

Apache JMeter - User's Manual: Getting Started

Non-GUIモードのほうがより正しい結果を得られるため、GUIモードはテスト計画の作成のみに使い、実際のテストはNon-GUIモードを使ってほしいそうです。

斯く言う私も、この記事を書くため公式ドキュメントを読んで気づきました(^^;)

より正確な結果が求められるテストを行う場合は、GUIモードでテスト計画を作成、上部メニューの保存ボタンから保存したのち、 Non-GUIモードから読み込んでお使いください。

以上、基本的な内容ですが、私が業務で使ったJMeterの使い方について、説明しました。

もし、はじめての負荷検証であたふたしている人の助けになれば、幸いです。


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

WIP制限の大切さ

はじめに

はじめまして。開発エンジニアのstrongWhiteです。

ラクスに入社して今年で2年目になります。

今回は、仕事を進めていく上でWIPを制限する大切さをお伝えしようと思います。
堅苦しい話にならないように、あるゲームの内容をもとにお伝えできればなと思います。

WIP

WIPとは「work in progress」の略で、仕事が進行中である あるいは 仕事が完了状態ではないことを意味します。
簡単にいうと、やりかけの作業 ということになります。

突然ですが、みなさんは仕事ができる人 や 仕事が早い人と聞くと、どんな人をイメージされるでしょうか。
いろいろな仕事を同時並行的にこなしている人をイメージされますか?

実は仕事を同時並行的に行うと、かえって完了が遅くなります。
仕事ができる人 や 仕事が早い人は、ひとつひとつの仕事を確実に終わらせる人です。
ここが今回の記事の肝になりますので、念頭に置いておいてください。

コインゲーム

先日、社内で開催された勉強会でコインゲームを開催しました。
このゲームはWIPを制限する大切さを実感できるゲームです。

ゲームに必要なもの

  • コインをひっくり返す「作業者」
  • 作業者の作業時間を計測する「管理者」
  • 1人の「顧客」
  • テーブル
  • 同じサイズのコイン20枚
  • ストップウォッチ(あるいはストップウォッチ機能付き携帯電話)
  • 結果を書き出す紙

遊び方

作業者と顧客はテーブルを囲んで着席します。
1人の作業者の後ろに1人の管理者が付き、管理者はストップウォッチを持ちます。
また、顧客もストップウォッチを持ってスタンバイします。

作業者・管理者・顧客の役割はそれぞれ以下の通りです。

  • 作業者…すべてのコインをひっくり返して、次の作業者に渡す。(最後の人は顧客に渡す。)
  • 管理者…作業者がコインをひっくり返すのにかかった実質作業時間を計測する。
  • 顧客…最初のコインが届くまでの時間とすべてのコインが届くまでの時間(最後のコインが届くまでの時間)の2つを計測する。

ゲームは3回繰り返してプレイします。
最初は20枚のコインを一括で行い、2回目は5枚ずつに分けて行います。そして最後は1枚ずつに分けて行います。
勘のいい方はこの時点で気付いたかもしれませんが、このゲームでは、コインがひとつのWIPを示しています。
3回とも全く同じようにプレイしますが、違うのはWIPが徐々に小さくなっていくことだけです。

今回、勉強会では作業者3名、管理者3名、顧客1名の計7名でゲームを行いました。
結果は以下の通りです。

20枚 5枚 1枚
Aさん 20秒 28秒 28秒
Bさん 23秒 26秒 28秒
Cさん 21秒 28秒 29秒
最初のコイン 67秒 23秒 4秒
合計時間 67秒 42秒 31秒

ゲームから学べること

WIPが小さくなるにつれて作業者の作業時間は長くなりますが、最初のコインがゴールするまでの時間と合計時間は短くなります。 つまり、同時に作業する作業項目を減らすと、リードタイムが短くなるということになります。
ひとつひとつのコインを皆さんが仕掛っている仕事に当てはめると、同時並行的に仕事をするとすべての仕事が完了するのにとても時間がかかってしまうということになります。 この点から、WIPを減らすことによる重要性が理解できるかと思います。興味がある方はぜひ周りの人とゲームをプレイしてみてください。

まとめ

今回はWIPを制限する大切さをお伝えしました。効率的に仕事を進めるには、ひとつひとつの仕事を確実に終わらせるように立ち回ったほうがいいということが少しでも伝われば幸いです。
抱えている仕事が多いと、つい気が焦って何でもかんでも手をつけてしまいがちです。ですがそのまま進めても何もかもが中途半端に終わってしまい、何ひとつ完了している仕事がないという状況に陥ります。
そんなときは今回の記事を思い出していただき、WIPを制限していま取りかかっている作業を終わらせることに集中してみてください。

参考

Marcus Hammarberg、Joakim Sunden 著 『カンバン仕事術-チームではじめる見える化と改善』

Copyright © RAKUS Co., Ltd. All rights reserved.