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

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

セッション管理としてRedisを使用する

はじめに

みなさん こんにちは、Thuatと申します。今年ラクスに入社しました1年目です。 この記事ではセッション管理としてRedisを使用するケースを紹介します。

Redisとは?

Redis は簡単に言うと、メモリ上のKey-Valueストアです。 メモリ上にデータを格納しますので高速に動作します。
以下はインストールから簡単なデータの登録・取得までの手順になります。

Redisをインストールする

$ wget http://download.redis.io/releases/redis-4.0.1.tar.gz
$ tar xzf redis-4.0.1.tar.gz
$ cd redis-4.0.1
$ make

Redisサーバーを起動する

$ src/redis-server
33507:C 25 Sep 23:21:32.201 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
33507:C 25 Sep 23:21:32.202 # Redis version=4.0.1, bits=64, commit=00000000, modified=0, pid=33507, just started
33507:C 25 Sep 23:21:32.202 # Warning: no config file specified, using the default config. In order to specify a config file use src/redis-server /path/to/redis.conf
33507:M 25 Sep 23:21:32.203 * Increased maximum number of open files to 10032 (it was originally set to 256).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 4.0.1 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 33507
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

33507:M 25 Sep 23:21:32.206 # Server initialized
33507:M 25 Sep 23:21:32.206 * DB loaded from disk: 0.000 seconds
33507:M 25 Sep 23:21:32.206 * Ready to accept connections

Redisのポートはデフォルトで6379になります。

クライアントからGET/SETする

Redisにあらかじめ組込まれているクライアントツールからデータをGET/SETしてみましょう。
シンタックスは以下になります。
set key value
get key

$ src/redis-cli
127.0.0.1:6379> set key1 10000
OK
127.0.0.1:6379> get key1
"10000"
127.0.0.1:6379> 

ValueJSONでも保存できます。

127.0.0.1:6379> set user_info '{"username":"taro", "age":20}'
OK
127.0.0.1:6379> get user_info
"{\"username\":\"taro\", \"age\":20}"
127.0.0.1:6379>

Redisは メモリ上にデータを格納しますので、デフォルトはサーバーを停止、再起動した場合は、データが失われます。 Redisはどのような時に利用すればよいでしょうか。 最適なユースケースはキャッシュやセッション管理です。

セッション管理としてRedisを使用する

セッションとは?

セッションはクライアントとサーバの通信の状態をWebサーバー上に保持されます。クライアントはセッションIDをURLやクッキー経由でサーバに渡します。 Key-Value構造としてメモリ、ファイル又はDBに格納します。KeyはセッションID、Valueはセッションに保存したいデータです。
デフォルトではセッションは Webサーバーのメモリ上に保持されます。 Webサーバー単独でセッション管理を行う場合はWebサーバーのロードバランサを構成できません。 セッションをDBに格納する設定にすると、ロードバランサを構成することができますが、パフォーマンスに影響します。
セッションについては、
https://blog.takanabe.tokyo/2014/12/%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86%E3%81%AE%E5%91%A8%E8%BE%BA%E7%9F%A5%E8%AD%98%E3%81%BE%E3%81%A8%E3%82%81/
を参照ください。

Redisを使ってセッションを管理する

データをセッションではなくて Redisで管理する方法を紹介します。
Redisにはセッションのようにタイムアウト時間を設定できます。タイムアウト時間を超えた場合はRedisに保存したデータが自動的に削除されます。

以下の機能をJavaで実現してみます。

JavaでのRedisクライアントは多くありますが一番人気はJedisです。
https://github.com/xetorthio/jedis/wiki/Getting-started

プロジェクトのLibrariesにjedis-2.9.0.jarを追加してください。
http://search.maven.org/remotecontent?filepath=redis/clients/jedis/2.9.0/jedis-2.9.0.jar

jedis-2.9.0.jar を追加する


フォルダの構成は以下のようになります

Userクラスを作成します。

package jp.co.jedis;

public class User {
    private String username;
    private String address;

    public User(String username, String address) {
        this.username = username;
        this.address  = address;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "{'username':'"+this.username + "' , 'address':'"+ this.address + "'}";
    }

}


UserActionクラスを作成します。 ユーザーのデータをRedisに保存するためのクラスです。
3分後、自動にExpireされるように設定します。
KeyはユニークなUUID、ValueJSONでのユーザーのデータです。

package jp.co.jedis;

import java.util.UUID;

import redis.clients.jedis.Jedis;

public class UserAction {
    private static final int TIMEOUT = 3*60; //3分
    public static void main(String[] args) {
        String userToken = UUID.randomUUID().toString();
        System.out.println("user token: " + userToken);
        Jedis jedis = new Jedis("localhost");
        User user = new  User("tanaka", "Tokyo-shibuya");
        //3分後、無効になる
        jedis.setex(userToken, TIMEOUT, user.toString());
        jedis.close();

    }

}


Eclipseで実行してみましょう。


Redisのクライアントで確認しましょう。

127.0.0.1:6379> get a62a05d0-1efe-4d09-99f1-4289ff1511a6   -> 3分以内の場合
"{'username':'tanaka' , 'address':'Tokyo-shibuya'}"

127.0.0.1:6379> get a62a05d0-1efe-4d09-99f1-4289ff1511a6  -> 3分以上の場合
(nil)
127.0.0.1:6379> 

a62a05d0-1efe-4d09-99f1-4289ff1511a6 は生成されたKey、ユニークなUUIDです。

セッション管理としてRedisを使用するメリットは以下になります。

  • メモリ上に格納するのでパフォーマンスがよい

  • Webサーバーの負荷が減る

  • 別サーバーでセッション管理するので、Webサーバーのロードバランサを問題なく構成することができる
    ロードバランサを構成するには以下のような構成となります。

最後に

セッション管理としてRedisを使用する方法やJavaでRedisにアクセスする方法を紹介しました。 Redisはまだたくさんオプションがあります。
例えば接続数ハンドル、トランザクションクラスターなどをもっと知りたい方は https://github.com/xetorthio/jedis/wiki を参照ください。

参考資料

https://blog.takanabe.tokyo/2014/12/%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86%E3%81%AE%E5%91%A8%E8%BE%BA%E7%9F%A5%E8%AD%98%E3%81%BE%E3%81%A8%E3%82%81/

Redis

Home · redis/jedis Wiki · GitHub


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

今更聞けない『クラウド』の仕組み

はじめに

皆さんこんにちは、rs_tukkiです。今年新卒でラクスに入社しましたピッカピカの一年生です。

普段仕事や授業に取り組んでいると、「あれ、この単語どういう意味だろう?」と思っても、さも知ってて当然かのように話が進むせいでなかなか聞けなかったりすることありますよね。大丈夫です。私も割と多いです。
そんな「今更聞けない」言葉ですが、皆さん、こんな単語をご存知でしょうか。

クラウド

…はい、おそらく、知らない方はいないはずです。ですが、
クラウドサービスとか、最近よく聞くけどどんなサービスなの?」
「なんかデータを預けるとか聞いたことがあるけど、いざ質問されるとわからないな」

という方も多いでしょう。

そこで今回は、ラクスのシステムの根幹を担う「クラウド」が、どんなサービスなのかということについて、一から解説していこうと思います。

目次


クラウドとは?

概要

さて、クラウドについてネットで調べてみると、たまにこういう記述が見つかります。
クラウドの定義はとても曖昧です」
「定義や意味は曖昧である」

…さすがにこれでは元も子もないですね。
ですが、なんとか一言で「クラウド」を説明するのであれば、おそらく
「ユーザ一人ひとりがソフトやサーバを用意しなくても、ネットワークを通じてそれらを使うことが出来る、という考え方」
とするのが正しいと思います。

私たちが今までサービスを利用するには、専用のソフトウェアをインストールしたり、サーバを一から作る必要がありました。それが、クラウドによって「どこにあるか分からないけど、ネットワークの中から、必要な時に必要な量を取り出して利用する」ようになったのです。

クラウドという名称ですが、どこにあるかは分からない、けどどこかにある、というイメージが雲(=cloud)の中を手探りで操作しているように見えた、とか、ソフトやサーバを一極集中させていることから、ユーザがそこに群がっている(=crowd)ように見えた、など単語の意味自体には諸説あるようです。

クラウドという名前が生まれた背景

クラウド」という言葉を最初に使用し始めたのは、当時の米GoogleSEOエリック・シュミット氏だと言われています。これは2006年8月のことですが、実は有名なクラウドサービスの一つであるGmailは、2004年には既に稼働が始まっていました。 つまりクラウドという言葉は、技術の登場と同時に誕生したのではなく「元々存在していた技術に名前を付けたもの」ということになります。

なぜ、わざわざ名前を付けるようなことをしたのでしょうか。

それは、当時多くのサービスが「クラウドではなかった」からなのです。例えば、ワードやエクセル、パワーポイントなどのアプリケーション。これらは、当然自分のパソコンの中にインストールしなければ使えませんし、作成したデータも(USBなどの外部領域を使わないのであれば)一般的には、そのパソコンの中に保存しています。

このような非クラウドなサービスでも問題はないのですが、Gmailなどの誕生を機に「この形式でサービスを作れば、より便利になるんじゃないか」という流れが生まれました。そのため、「ユーザがそれぞれの領域にソフトやデータを確保する」という今までの使い方に対して、これからは「ネットワークにソフトやデータを配置して、場所は分からないけど簡単に使えるようにしよう」、ということを簡単に表す言葉として、「クラウド」が使われ始めたのです。


クラウドの使用例

さて、実際のクラウドの使用例についてですが、先ほど挙げたGmailなどのメールサービスをもとに説明します。
私たちがメールを送る際は、パソコン上でメールを作成や送受信を行うための「メールソフト」と、実際に送受信を制御するための「メールサーバ」が必要です。
メールソフトはOutlookBecky!Thunderbirdなどのソフトをインストールすればいいのですが、メールサーバの構築をクラウドなしで実現しようとすると結構大変です。企業であれば社内のメールサーバを持っているところもあるものの、個人で構築するのは手間ですし、あまり意味はありません。

一方、GmailHotmailなどの、クラウドサービスとして提供されているメールサービスでは、私たちユーザが用意するものは「インターネットに接続できるブラウザ」だけで十分です。ソフトのインストールも、サーバの構築も必要なく、
「どこにあるかはわからないけど、どこかにあるソフトとサーバを使わせてもらう」
ことで、メールの送受信を実現しているのです。


クラウド使用によるメリット・デメリット

ただ、クラウドサービスは便利なだけではなく、当然デメリットも存在します。個人向け、企業向けのクラウドサービスごとに、主な例を挙げて説明します。

個人向けクラウドサービスのメリット
  1. どこからでも、どんな端末からでもアクセスできる
    企業、個人問わず、クラウドサービスにおいて最もメリットを体感できるのはこれだと思います。わざわざSDカードやUSBメモリを使ってデータを移し替えなくても、データはインターネット上のサーバに保存されていますから、他のデバイスでもログインするだけでデータの共有が可能になります。買い物に行くとき、自宅のパソコンで欲しい商品を調べてメモしておいて、それをスマートフォンで確認する、といったことが可能になります。

  2. 更新作業が不要
    パソコンでもスマートフォンでも、インストールして使用するタイプのソフトは、定期的なアップデートが必要でした。ですが、クラウドサービスはソフトの更新をサーバ側で行ってくれるため、煩雑な更新作業や、それに伴うシステムの不具合に悩まされる必要もなくなるのです。

個人向けクラウドサービスのデメリット
  1. データ漏洩・紛失の可能性
    クラウドはインターネット上にデータを保管するわけなので当然と言えば当然なのですが、ログイン情報の流出や、その他サーバ側の不手際によって、簡単に個人情報が流出してしまう可能性があるわけです。前者はともかく、後者は使用者にとってはどうしようもないので、信頼のおけるサービスを使うようにしましょう。
    また、サーバ障害、サービス自体の廃止など、クラウド上に保存しておいたデータがいつのまにかなくなっていることも考えられます。大切なデータは、自分の手元にバックアップを取るようにした方が無難です。
企業向けクラウドサービスのメリット
  1. どこからでも、どんな端末からでもアクセスできる
    先ほど個人向けでも説明した内容と同じです。ネット環境さえあれば、たとえ出張先でも使えます。

  2. コスト削減
    ここでいう「コスト」とは、「初期コスト」と「保守・管理コスト」の2種類に分けられます。クラウドを用いず、自社でサーバやシステムを用意しようとすると、どうしても最初の環境整備や、稼働開始後の保守・管理に費用や人員を割かなければいけません。しかし、クラウドサービスにおいては、特に小規模のシステムで初期費用が抜群に軽減されるほか、煩雑なアップデート作業も管理会社が請け負ってくれるため、そのためのコストも削減が見込めるのです。

企業向けクラウドサービスのデメリット
  1. データ漏洩の可能性
    個人向けでも説明した内容ですが、企業向けのクラウドサービスでは扱うデータの性質上、よりハッキングの攻撃対象になりやすいというリスクがあります。当然企業向けの有料サービスであれば管理会社が何らかの対策はしてあるでしょうが、その面もきちんと確認しておくのが堅実かと思います。


クラウドサービスの分類

ここまで説明してきたクラウドサービスの提供形態ですが、実は更に細かく分類することができます。

SaaS(Software as a Service)

SaaSは、クラウドによってソフトウェアを提供するサービスのことです。先ほど例として挙げたGmailや、Google Driveなどのサービスは代表的なSaaSと言えるでしょう。
その他、個人向けではDropBox、企業向けではSalesforceなど、普段から皆さんがよく使っている「クラウドサービス」は、ほとんどがこのSaaSだと思います。

PaaS(Platform as a Service)

PaaSとは、クラウドによって開発環境を提供するサービスのことです。有名なところだとGoogle App EngineMicrosoft Azureなどがあります。
SaaSとは違い、PaaSとIaaSはエンジニア以外が使うことはあまりないと思います。利用者は提供された開発環境の上でウェブサービスを開発し、更に提供しているのです。

IaaS(Infrastructure as a Service)

IaaSとは、クラウドによってサーバを提供するサービスのことです。各種レンタルサーバなどはIaaSに含まれます。
PaaSより更に範囲が狭まって、サーバのみの提供となっていますが、開発環境から自分で用意できるため、自分の好きなプラットフォームで開発したい、という人はこちらを使うことになります。


おわりに

最近よく聞くけど、改めて考えてみるとイメージのつかめない「クラウド」という言葉について解説しました。
私自身大学でクラウドシステムを用いたゲームを開発していたことがあるのですが、意外と知らないことが多く苦戦しました改めて勉強し直すいい機会になったと思います。
今回の記事で、少しでも皆さんの「?」が解消出来たら幸いです!


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

Git初心者のためのローカル作業時の備忘録

はじめに

皆さんはじめまして。新人エンジニアのsts-250rrです。
本年度新卒として入社し、1年目の私ですがよろしくお願いいたします。

Git初心者である方の中には、Gitの大枠がどんなものかは知っているし、使ったこともある。
でも機械的git status,git add -Aとしていたり、git commitをしていた。
というような方もおられるのではないでしょうか?(私はその口でした・・・)

各コマンドの正しい知識を持たずに機械的にコマンドを実行するのでは、 業務を行う上で正しい操作ができないことも考えられます。チームの開発ではなおさら御法度といえるでしょう。
私自身含め、そのような方が今後Gitで失敗をしてしまわないように手助けができればと思います。

本記事では、ローカル環境で作業をする際のGitの構成であるリポジトリやワークツリー・インデックスに関してまとめ、利用するコマンドがどのような操作であるのかを再確認していきます。 それらを把握したうえで、よく利用するGitコマンドをシーン別に分けて紹介し、コマンドによる操作のイメージを持っていただきたいと思います。
加えて、コミットの流れやコミットで失敗してしまったときの対処法も載せておきましたので、困ったときの参考になると幸いです。

リポジトリ・ワークツリー・インデックスって?

Gitではローカルで作業する際に以下のような構成でプロジェクトを管理しています。 この部分の構成を理解していると、addやcommitによってどのような操作がなされるのかがわかってくるかと思います。

  • リモートリポジトリ[Remote repository]
    サーバに配置されており、複数人でプロジェクトを共有する場所。
    *本記事での話題の対象ではありません。

  • ローカルリポジトリ[local repository]
    ローカル環境内でのプロジェクトのファイルやディレクトリの変更履歴を記録する場所。

    • ヘッド[HEAD]:最新のコミット状態を示す。
  • インデックス[index]
    ローカルリポジトリにコミットを行う準備をする場所。ワークツリーから登録された内容をgit commitでコミットする。
    インデックスが存在することで、ワークツリー内の必要のないファイルはコミットしないといったことができる。

  • ワークツリー[work tree]
    実際に作業を行っているディレクトリであり、編集を行った最新のファイルが存在する。
    ここでの作業内容をgit addすることでインデックスに変更を登録する。

これらローカル内の3つの構成により、Gitではプロジェクトの管理が行われています。 この部分が理解できればaddcommit、それらの操作を取り消すresetといった操作が理解しやすくなったのではないでしょうか。

シーン別:よく使うGitコマンド

前節まででローカル環境でのGitの構成の大枠は掴んでいただけたでしょうか?
本節では、多様なgitコマンドの中からよく使うコマンドを3つのシーンに分けてまとめています。
逆引きのように利用していただけると幸いです。

編集を確認したいとき
  • log
    • git log :最新までのコミットログを確認。
    • git log --oneline :コミットログを簡潔に確認。
  • status
    • git status:ローカルで編集されたファイルの一覧を表示。
  • diff
    • git diff:インデックスとワークツリーの全ての差分を表示。
    • git diff ファイル名:インデックスとワークツリーのしてファイルの差分を表示。
編集したものをリポジトリにコミットしたいとき
  • add
    • git add -A:ワークツリーの内容をすべてインデックスに登録。
    • git add <ファイル名>:編集した1ファイルのみをインデックスに登録。
    • git add <ファイル名1> <ファイル名2>:複数の編集をインデックスに登録。
  • commit
    • git commit:インデックスの内容を全てコミットする。
    • git commit -m 'message':コミットと同時に1行メッセージを書く。
addやコミットを取り消したいとき
  • reset
    • git reset:git addの内容を取り消す。
    • git reset --soft HEAD^:直前のコミットのみを取り消す。
    • git reset --hard HEAD^:現時点のインデックスやワークツリーごと直前のコミットを取り消す。

コミットするときの流れ

本節では実際コミットをするまでの流れを以下のTest.javaファイルと共に確認していきます。

public class Test {
    public static void main(String[] args) {

    }
}

Test.javaにある編集を加えました。編集は正しく反映されているでしょうか?
自身の編集が完了したらgit statusで現在の状態を確認します。

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   src/test/Test.java

no changes added to commit (use "git add" and/or "git commit -a")

Test.javaに変更があったようです。ソース内ではどのような変更があったのかgit diffで確認します。

$ git diff
diff --git a/src/test/Test.java b/src/test/Test.java
index 8e11c08..b625295 100644
--- a/src/test/Test.java
+++ b/src/test/Test.java
@@ -2,5 +2,6 @@ package test;

 public class Test {
        public static void main(String[] args) {
+               System.out.println("Test");^M
        }
 }

出力部分が追加されたようですね。ではこの編集をインデックスに登録しましょう。
確実にインデックスに追加したい自身の編集ファイルのみ git add src/test/Test.java で登録しましょう。

再度、現在の状態を確認します。

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   src/test/Test.java

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

インデックスに登録されている様子が確認できました。 それではコミットしていきましょう。

$ git commit -m 'Test出力の追加'
[master 2eb4313] Test出力の追加
 1 file changed, 1 insertion(+)

問題なくコミットできたようです。
最後にコミットの履歴のログを確認しておきましょう。

$ git log --oneline
2eb4313 (HEAD -> master) Test出力の追加
8823cc3 Test.javaの作成
97fde21 first commit

ここまでの作業が完了すればローカルでの編集内容が履歴として記録されることになります。
以降は同様に編集→コミットしていけばよいわけですね。
個人的にコミットまでの注意点は

  • git diffで変更差分を確認すること。
  • git addで必要のないファイルまでインデックスに登録しないこと

この2点だと思っています。
この点を意識できているかどうかだけでも、Gitに対する理解が深まるのではないでしょうか。

コミットで失敗したとき

次に、今までやってきた「addやcommitをやり直したい」そんなときの流れを簡単に確認します。
例として、前節で行ったコミットが間違えていたのでなかったことにしましょう。
git resetのオプションを指定して直前のコミットをなかったことにします。

$ git reset --soft HEAD^

ログやステータスを確認してコミットが取り消されていることと現在の状態を確認します。

$ git log --oneline
8823cc3 (HEAD -> master) Test.javaの作成
97fde21 first commit

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   src/test/Test.java

直前のコミットTest出力の追加が取り消されています。 ここまでで、直前のコミットを取り消し、編集内容がインデックスに登録されているという状態が確認できました。 次にこのadd自体も取り消してみましょう。
この場合はオプション無しのgit resetです。

$ git reset
Unstaged changes after reset:
M       src/test/Test.java

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        modified:   src/test/Test.java

no changes added to commit (use "git add" and/or "git commit -a")

addされた内容がインデックスの登録から外れた様子も確認できました。
ここまでで直前のコミットを取り消し、編集内容も取り消しました。つまり、2つ前のコミットの状態まで戻ったことになります。ここからは引き続き作業を行っていきましょう。

おわりに

最初にも述べたように機械的に行うcommitaddはGitを利用していく上で良いことではありません。
自身が利用するツールに関してはどのような利用方法があるのか、どのような操作であるのかを確認して利用していくと良いかと思います。

自身の学習を踏まえた記事ではありますが、読んで頂いた方々の参考になれば幸いです。


◆TECH PLAY
techplay.jp

◆connpass
rakus.connpass.com

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

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