RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

今更わかるPostman〜便利なAPI開発補助ツール〜

みなさん、今日も元気にAPI開発していますか?
最近社内勉強会で改めてPostmanについて説明したら
意外と機能を知らない人が多かったので
API開発で色々便利な機能満載のPostmanを
API Clientとしての機能をメインに紹介しようと思います。

Postman is 何?

Postman, Inc.が開発しているAPI開発コラボレーションツールです。
API Client機能やチーム開発の共有、MockServerの作成機能、自動テスト機能などがあります。
弊社ではローカルでの開発の振る舞いテストで主に使用しています。

インストール方法

mac(Homebrew-cask)

Homebrew-caskでインストール可能です。

$ brew cask install postman

mac & windows

www.getpostman.com

こちらのリンク先のDownload the Appをクリックしてダウンロードしてインストールしてください。

新規設定

f:id:dedekopon:20191213160428p:plain 画面左上の[New]をクリックして新規の設定を行う

f:id:dedekopon:20191213161311p:plain BUILDING BLOCKS

Request:新規でリクエストを作成する。
Collection:リクエストのフォルダを作成する。
Environment:Postman上の環境変数の設定を行う。

ADVANCED(ログイン後の機能)

API Documentation:ドキュメントを作成する。ログイン後のページで共有される。 Mock Server:リクエストのモックを作成する。
Monitor:自動テストの設定、パフォーマンスチェックを行う。

メイン画面

f:id:dedekopon:20191213163133p:plainAPI
APIの説明
APIのメソッド・URL
④リクエストの設定
⑤リクエストの設定詳細項目
環境変数設定
APIのリクエスト例
⑧クッキーの設定・別媒体でのリクエストサンプル

リクエストの設定

Params

APIのqueryパラメータとpathパラメータを設定する。
f:id:dedekopon:20191213164104p:plain pathパラメータはURL中に:param_nameで設定、queryパラメータは通常のGETパラメータなので?param_name=valueの形で定義する。
例えばhttp://example.com/test/:test_id/?param=testの場合は図のようになる。

Authorization

APIを使用する際に認証が必要な場合などに設定する。

Headers

リクエストのヘッダーの設定を行う。Authorizationにauthorizeヘッダーを設定している場合はこちらには不要です。

Body

リクエストのボディの設定を行う。

pre-request script & tests

f:id:dedekopon:20191213164914p:plain Intro to scripts | Postman Learning Center
リクエスト送信時に前処理、後処理を簡易的ですが実装できます。
以下にちょっとした一例を書いておきます。

  • 環境変数から保持しているトークンの有効期限を確認して、エラーを出すpre-request script
// 環境変数から値を取得する
var ttl = pm.environment.get("TOKEN_TTL")
var now = Date.now()
if (now > ttl) {
    // throw new Errorされた場合、処理はそこで止まります
    throw new Error("No available token set. Please set environment TOKEN and TOKEN_TTL")
}
var refresh = false
// レスポンスのステータスコードが200かチェックする
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
    refresh = true
});

if (refresh) {
    // 環境変数のトークンを更新する
    console.log('token refresh')
    pm.environment.set("TOKEN", pm.response.json().token);
    pm.environment.set("TOKEN_TTL", Date.now() + (3600 * 1000));    
}

環境変数設定

f:id:dedekopon:20191213170830p:plain 環境変数の切り替えを行うことができます。
こちらで設定した値はpre-request script,tests

pm.environment.get("VALUE_NAME") // 取得
pm.environment.set("VALUE_NAME", "value") // 更新

のような形で操作できる他
{{URL}}/test/のようにURLやパラメータ中に{{VALUE_NAME}}の形で埋め込むことができます。

APIのリクエスト例

f:id:dedekopon:20191213171745p:plain APIのリクエスト例を保存することができ、簡単に呼び出すことができます。

別媒体でのリクエストサンプル

f:id:dedekopon:20191213172118p:plain CodeをクリックすることでPHP,Ruby,Goでの実装サンプルが確認することができます。
別の環境で確認するときなどにとても重宝します。

エクスポート

これらの設定は全てインポート・エクスポートすることが可能です。
弊社ではAPIのサンプルの共有用リポジトリを用意し、Git上で管理しています。

まとめ

  • Postmanは優秀なAPI開発補助ツール
  • ただリクエストを送るだけだと勿体無いので色々設定して使おう
  • ログインして使う場合はさらに便利機能が色々使えるよ

RDS Proxyでサーバレス捗りそうだと思いました

f:id:shino8383:20191205183647j:plain

おはようございます。レアジョブのHUNTER×HUNTER芸人こと、DevOpsチームのshinoです。 (前回の記事: EC2 Auto Scalingを導入する際のポイント - RareJob Tech Blog)

HUNTER×HUNTERAWSの知識をインプットするのが日課になっています。

今まさにAWS re:Inventが開催中です。来年は行きます。

この時期はAWSに関する大量の新サービス・アップデート情報が流れてくるのでキャッチアップが大変です。

今回はAWS re:Inventの中で発表があった情報の中で、個人的に熱いと思ったRDS Proxyについて触れたいと思います。

RDS Proxyとは

f:id:shino8383:20191205184022p:plain

RDS Proxyとは名前の通りRDSへの接続をプロキシするお方です。

RDS Proxyの主なメリットは以下のようになります。

  • RDSへの接続にコネクションプールを利用できる
  • RDS Proxyの利用にはSecrets ManagerかIAM認証を用いて接続するので、よりセキュアな認証を強制できる(推奨はIAM認証)

また、RDS Proxy自体は可用性を持ったマネージドなプロキシサービスなのでプロキシサーバの管理コストはこちらが持つ必要がありません。 移行コストについては、従来利用していたRDSエンドポイントをRDS Proxyが提供するエンドポイントに差し替えるだけで利用可能です。

クライアント側でコネクションプールを実装しなくていいってだけでも嬉しいです。

このようなサービスのRDS Proxyですが、私が初見で感じた感想は

「サーバレス捗りそう(小並感)」

でした。

LambdaとRDS(RDB)の話

話は変わりますが、LambdaとRDS(RDB)は相性が悪く、アンチパターンと言えます。

なぜ相性が悪いのでしょうか?

一般的にRDBは大量の接続要求をさばくのが苦手だからです

RDBがクエリを処理する際にかかるコストに比べ、TCP接続を確立するコストやDB側でコネクションを生成するコストが比較的高いため、 高負荷なワークロードでは"接続"にかかるコストでCPUやメモリを食いつぶしてしまうことが多いです。

そのため、アプリケーション規模が大きくなってきてRDBが悲鳴を上げ始めたらコネクションプーリングなどを用いて、DBへの接続にかかるコストを低くするなどの対処が取られるケースが多いかと思います。 ただし、コネクションをプールして利用するのにもメモリやCPUを使いますし、レスポンスタイムも遅くなることが多いのでRDBへの負荷が小さいうちは割に合わないと思います。

(RDBじゃないですがRedisでもコネクションプールを使わずにCPUリソースを食いつぶしてしまったことがあります。)

これがどうLambdaと関係あるかと言うと、

簡単にスケールしてしまうLambdaはコネクションプールを使うことができず、 Lambda->RDSを利用しようと思うと大量の接続要求を行ってしまいRDSが悲鳴をあげる、

という図です。 夜間のバッチ処理とかなら使えるとは思いますが、用法用量を守る覚悟が必要です。

また、RDS側がリソース的に耐えられたとしてもDBへの最大接続数の制限に引っかかってエラーとかも全然あります。

なので、サーバレスやろうぜってなってもRDS(RDB)をデータソースの候補にあげることができず、DynamoさんやElastiCache For Redisさんなどが使われる事例が多いと思います。

今回、RDS Proxyさんが降臨なさったのでLambdaからRDS使えるぞ!というのが

「サーバレス捗りそう(小並感)」

に繋がります。

終わりに

RDS Proxyの登場で、RDBMSの種類問わずにRDSをデータソースとしたアーキテクチャをサーバレスで構築することが可能になります。 控えめに言って熱いです。

現在はまだPreview版なのでMySQLとAuroraしか使えないですが、東京リージョンでもお試し頂けます。

隙あらば利用していきたいと思います。

そういう訳で我々は一緒に働く仲間を大募集しております。

少しでも興味のある方は以下からどうぞ!

👏採用/求人情報 | アピール | 未来の教育を作る人のマガジン インフラエンジニア(AWS)

Alexaスキル開発してます

プラットフォームチーム所属の南です
弊社では有志で勉強会のような場を設けることができます。分科会という呼び方をしているのですが、その中で自分はAlexaスキル開発に参加していました。今回はその活動を通じて学んだことなどを徒然なるままに書きたいと思います。
以前、勉強会でLT枠としても発表したのでその資料も下にリンクしておきます

なぜやるのか?

  • 新たな技術分野に乗り遅れないようにするため
    • 広まりつつある技術に対し、いつ事業として取り組むことになったとしても乗り遅れないように知見を貯める
  • 気楽な気持ちで利用してもらい「学習体験の向上」を目指す
    • 教材やPCを準備することなく学習を始めることができる

大きくはこの2点を目的として始動しました。

どのように作るのか?

AlexaのようなVUIを設計するのは初めての経験でした。 そのため以下をまず決定しました。

  • なにを作るのか
    • リスニングからTrue/Falseを答えるシンプルなスキルを作る
  • スタートから終わりまでの道筋
    • 目に見えないものを設計する

スタートから終わりまでの道筋

今回の開発ではここに一番時間がかかりました

f:id:nannannanan:20191129174909p:plain
ユーザにどのような遷移をして欲しいのかを考える
上記の図のように、どのタイミングでどの処理が実行されユーザが遷移するのかを話し合いました。しかし、図で表した内容を実際にデバッグすると間延びしたつまらないスキルになることも多く、問いかける順番や反応に対する処理はGUIと同じかそれ以上に気を配る必要がありました。

わかったこと

  • 気をつけないといけないことが多い
    • 間の取り方や問いかけの回数
    • VUIに慣れている人はまだ少ない
    • 想定外の返答に対する制御
  • シンプルな受け答えで学習を提供できるのは純粋におもしろい
  • 手ぶらで受けられるなど、VUIの特性をうまく利用すれば学習体験は向上できる

VUI自体がこれからさらに広まり、声を使った操作に慣れた人が増えてくれば、もっと面白いスキルが作れるように感じました。

最後に

今後、弊社がAlexaスキルをリリースした際は利用して頂けると幸いです。

こんな感じで新しい技術に対しても積極的に取り組むことができるので、興味がある方はとりあえず下のリンクを踏んでくれると嬉しいです。 https://appeal.rarejob.co.jp/recruit/

勉強会 LT資料 -> Alexaスキル開発をして分かった VUIの難しさ - Speaker Deck

AWS Device Farm でブラウザから実機でアプリを動かそう

AWS Device Farm

AWS Device Farmクラウドでモバイル端末をエミュレートできる仕組みです。 CodepipelineなどのCI/CDとも連携可能で、E2Eやエミュレート環境としてAWSから提供されています。

できること

  1. iOS/Android端末がクラウド上で動かせる
  2. 2019/11/05時点で利用可能なデバイス266種類
  3. 「AutomatedTest」と「RemoteAccess」が可能(後述
  4. テスト後に動画やログを自動で保存してくれているのでDLできる

最新のデバイス・OSサポート

現状ではあまり期待できないようです。色々な会社の抱えている課題の一つに 十全に検証機を用意できない というのがあると思います。特に最新のデバイスなどは価格やそもそもの入手困難さからサポートが後手になることが多いと思います。 またOSに関しても社内の検証端末のOSを上げるのはちょっと骨が折れる・・・

iOSだと特にまだ最新のXcodeでビルドできるように対応できてないのに最新のOSで問題がッ!とか起きることもあると思うのでそういうケースで使えてほしい・・・アップデート頻度等を見てもそこのスピード感はあまりないように思います。

AutomatedTest

f:id:jumbos5:20191122130444p:plain

ios/android共にE2Eテストやモンキーテストを実行できます。

テストケース

様々なテストケースの準備ができます。

f:id:jumbos5:20191122130604p:plain

中でも準備がいらないものとしてFuzzTestがあります。モンキーテストに相当するものなので、かなりシンプルですが使えるユースケースは多いと思います。

Testing mobile apps with Cucumber and Appium through TestNG on AWS Device Farmとかを見ると、上記の写真にもある通りAppium/Calabashでもテストケースが欠けるので細かい手順も実行できます。


Remote Access

f:id:jumbos5:20191122130553p:plain

こちらは本当にブラウザ上でapk/ipaのアップロードをして端末に入れてそれをポチポチ動かせるような仕組みですね。

WebRTCは動くの?

結論動く。 特に準備もいりません、うごくapk上げるだけ。 エミュレーターだとダミー映像が出るかと思いますが、DeviceFarmでは真っ黒の映像が出ます。 カメラはアクティブにはなっているようですが、設置環境によって映像が黒くなっているようです。キャリアやSMSは動かないので、ある程度基本的な動作を確認するに限定されるかと思います。

そのためカメラアプリなどでも一応動かすことはできるかもです。 ただ画面自体は黒くなるので、E2Eで成功結果として撮影物のデータ量や流れるストリームで見てしまうとコケるかもしれません。

f:id:jumbos5:20191122130628p:plain

コスト

0.17USD/デバイス

です。1分を1デバイスで = 1デバイス分なので、1つのリモートアクセスで5分使ったとすると85円くらいですかね?例えばAutmatedTestを複数デバイスで動かした場合はそのデバイス分のコストがかかります。

最初の1000分までは無料のため、ぜひ試してください。10分もあれば動きを確認できるかと思います。

プライベートデバイス

こういう概念があり、理由としてはCircleCIのようにインスタンスリザーブされ続けているわけじゃないので混んでいる時間には待ち時間が発生します。私が検証している時に長時間待つようなことはなかったのですが、将来的には利用を検討すべきかと思います。 コストは 200 USD/月 とまぁまぁ高いので、それなら端末買ったほうがよくない???とも思いますが、現在はCodePipelineと連携してCIのプロセスとも連携できるので、本格的なE2Eをデプロイメントパイプラインの中に内包したいのであればかなり有効かなと感じます。

IPなどが固定されるわけではないと思うので例えばネットワーク的にクローズとなSTG環境を持っていて、それのアプリをE2Eしたい場合はどうすれば?とか考えましたが

docs.aws.amazon.com

上記を読んでいる感じはできそうだと感じました。設定はめんどくさそうだが・・・

エミュレーション

などがエミュレートできる。GPSとか地味に地図系のアプリとかだと重要だと思うし、他アプリのインストールも複数アプリで連携するようなケースでは有用ですね。

まとめ

  • バイスごとに挙動の違うWebRTCのE2Eをデバイス*OSを並列・包括的に実行できる
  • skywayもちゃんと動くことを確認できた
  • 相手の映像は見れないのでp2psfuの接続のチェックなどのみ、カメラは真っ黒だが生きておりマイクはない
  • プラグインがあるので既存のデプロイメントプロセスに載せてE2Eが実施可能

Client VPNで分析環境を用意してみた

こんにちは。@hayata-yamamotoです。

普段は、EdTech Labという研究開発のチームで機械学習とデータ分析をしています。 AllenNLPmypyが好きです。

さて今回は、AWSのClientVPNを用いて手軽にVPN環境を用意し、Jupyterを動かしてみたいと思います。 ClientVPNの使い方はAWSの公式にありますが、Terraformと組み合わせたものは見当たらなかったので誰かの参考になれば嬉しいです。

今回出てくる技術

なお今回の内容はGitHubにも置いてあります。

github.com

Client VPN

AWSが用意するVPNサービスの1つです。2018年12月にリリースされました。東京リージョンは、2019年5月から利用可能になっています。

aws.amazon.com

そもそもVPNは、あるネットワークやデバイスからある別のネットワークへの安全でプライベートに接続するために設定します。 公式によるとClient VPNは、ユーザー需要に合わせて利用可能な Client VPN 接続の数を自動的にスケールアップまたはスケールダウンする、完全マネージド型で伸縮自在な VPN サービスです。使ってみた感じ、クラウド側でそれぞれのクライアントのIPをClient VPNに割り振っているCIDR範囲と紐づけてくれています。それゆえ、クライアント側はVPN用のソフトウェアをダウンロードするだけで済みます。

詳細な説明やユースケースについては、AWSの公式も合わせて確認されると良いと思います。

aws.amazon.com

どのようなものか

少し触れてしまいましたが、このサービスは以下のような仕組みで動いています。

f:id:hayata-yamamoto:20190727133707p:plain
Client VPN 仕組み(AWSより)

この通り設定すると、環境が用意できるようになります。

  1. Client VPN Endpointを設定します
  2. VPNをサブネットに関連づけます
  3. ユーザーがリソースにアクセスできるように認証ルールを追加します
  4. 他のネットワークにアクセスできるようにルールやルートを追加します(インターネットとか)

設定方法

クライアント認証用の証明書を発行する

acmに登録するサーバーとクライアントの証明書を発行します。

$ git clone https://github.com/OpenVPN/easy-rsa.git
$ cd easy-rsa/easyrsa3

新しいPKI, CA環境を作って、サーバーとクライアントの証明書を発行します。

$ ./easyrsa init-pki
$ ./easyrsa build-ca nopass
$ ./easyrsa build-server-full server nopass
$ ./easyrsa build-client-full client1.domain.tld nopass

適当な場所にファイルを移動して、awsコマンドでACMにインポートします。

$ cp pki/ca.crt /custom_folder/
$ cp pki/issued/server.crt /custom_folder/
$ cp pki/private/server.key /custom_folder/
$ cp pki/issued/client1.domain.tld.crt /custom_folder
$ cp pki/private/client1.domain.tld.key /custom_folder/
$ cd /custom_folder/

# サーバー証明書のインポート
$ aws acm import-certificate --certificate file://server.crt --private-key file://server.key --certificate-chain file://ca.crt --region region

# クライアント証明書のインポート
$ aws acm import-certificate --certificate file://client1.domain.tld.crt --private-key file://client1.domain.tld.key --certificate-chain file://ca.crt --region region

環境を用意

今回は、Terraformを使って設定します。ただ、Terraformだと執筆時点でAPIへのサポートが微妙に足りませんので、そこだけはコンソールから操作をします。具体的には、aws_client_vpn_route_table, aws_client_vpn_authorization_ruleに相当するものがありません。

github.com

# variables.tf

variable "region" {
  default = "ap-northeast-1"
}

variable "availability_zone" {
  default = "ap-northeast-1a"
}

variable "instance_info" {
  type = "map"
  default = {
    ami           = "ami-0c6dcc2b75586bc5d"
    instance_type = "t2.small"
  }

}

variable "cidr_blocks" {
  type = "map"
  default = {
    vpc        = "10.0.0.0/16"
    subnet     = "10.0.0.0/24"
    global     = "0.0.0.0/0"
    client_vpn = "100.0.0.0/16"
  }
}

variable "my_cidr_block" {}

variable "ssh_keyname" {}
# data.tf

data "aws_acm_certificate" "server_certificate" {
  domain = "server"
}

data "aws_acm_certificate" "client_certificate" {
  domain = "client1.domain.tld"
}
# resources.tf

provider "aws" {
  region = var.region
}

resource "aws_vpc" "vpc" {
  cidr_block           = var.cidr_blocks.vpc
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = {
    Name = "vpc"
  }
}

resource "aws_subnet" "subnet" {
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = var.cidr_blocks.subnet
  availability_zone       = var.availability_zone
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet"
  }
}

resource "aws_route_table" "rt" {
  vpc_id = aws_vpc.vpc.id

  route {
    cidr_block = var.cidr_blocks.global
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "rt"
  }
}

resource "aws_route_table_association" "a" {
  route_table_id = aws_route_table.rt.id
  subnet_id      = aws_subnet.subnet.id
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "igw"
  }
}

resource "aws_security_group" "sg" {
  name   = "sg"
  vpc_id = aws_vpc.vpc.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.my_cidr_block]
  }

  ingress {
    from_port       = 22
    to_port         = 22
    protocol        = "tcp"
    security_groups = aws_ec2_client_vpn_network_association.vpn.security_groups
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = [var.cidr_blocks.global]
  }
  tags = {
    Name = "sg"
  }
}

resource "aws_instance" "myinstance" {
  ami                         = var.instance_info.ami
  instance_type               = var.instance_info.instance_type
  vpc_security_group_ids      = [aws_security_group.sg.id]
  subnet_id                   = aws_subnet.subnet.id
  key_name                    = var.ssh_keyname
  associate_public_ip_address = true

  tags = {
    Name = "myinstance"
  }
}


resource "aws_ec2_client_vpn_endpoint" "vpn" {
  description            = "client vpn endpoint test"
  server_certificate_arn = data.aws_acm_certificate.server_certificate.arn
  client_cidr_block      = var.cidr_blocks.client_vpn
  authentication_options {
    type                       = "certificate-authentication"
    root_certificate_chain_arn = data.aws_acm_certificate.client_certificate.arn
  }
  connection_log_options {
    enabled = false
  }
  tags = {
    Name = "vpn"
  }
}

resource "aws_ec2_client_vpn_network_association" "vpn" {
  client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.vpn.id
  subnet_id              = aws_subnet.subnet.id
}
# outputs.tf

output "instance_public_ip" {
  value = aws_instance.myinstance.public_ip
}

output "instance_private_ip" {
  value = aws_instance.myinstance.private_ip
}

適当な場所に上記の.tfを作ってもらって、

$ terraform init 
$ terraform plan 
$ terraform apply

とし、自分のCIDR範囲を入力してください。(123.1.1.1/32みたいなやつです) 実行が終わると、outputでインスタンスのpublic_ipが吐き出されるので、その情報を使ってsshインスタンスに接続してください。

ssh -i keyname ubuntu@instance-public-ip

認証とルートテーブルを追加する

実は先ほどの動作で、上記の4つの手順のうち2つが終わっています。あとは、認証とルートテーブルを付け加えて、完成です。

認証は、以下から

f:id:hayata-yamamoto:20191004000754p:plain

このように、サブネットのCIDR範囲を設定しておきます。

f:id:hayata-yamamoto:20191004000820p:plain ルートテーブルも先ほどと同様の画面から

f:id:hayata-yamamoto:20191004000854p:plain

インターネットに出れるようにルートを設定しておきましょう

f:id:hayata-yamamoto:20191004000916p:plain

接続する

tunnelblickをインストールしておいてください。

tunnelblick.net

OpenVPNやTunnelblickで使う設定ファイルをクライアントエンドポイントのページからダウンロードしてください。 適当にファイル名は変えて大丈夫です。

f:id:hayata-yamamoto:20191004001056p:plain

最初に作ったclient用の証明書と鍵と同じ場所に設定ファイルを配置し、

# hoge.vpn
...

cert /path/to/client1.domain.tld.crt
key /path/to/client1.domain.tld.key

と追記してこの.opvnファイルを開いてください。設定ファイルを読み込んだらTunnelblickを起動し、接続できるか確認しましょう。

Jupyterを立てる

さて、最終ゴールはJupyterの環境にアクセスすることでした。 ひとまず設定したVPNの環境を切断して、sshからEC2のインスタンスにアクセスしてください。

$ ssh -i keyname ubuntu@instance-global-ip

ちなみに、このインスタンスDeep Learning AMIを使っています。なので最初からJupyterNotebookは使えます。sshでトンネリングしてjupyterにアクセスすればOKです。サーバー側で

$ jupyter notebook --no-browser --NotebookApp.token=""

を実行し、別のタブで以下を実行してトンネリングを行います。

$ ssh -i keyname ubuntu@instance-private-ip -L 8888:localhost:8888 -N 

ブラウザでlocalhost:8888/treeにアクセスして、jupyterの画面が表示されれば設定は完了です。

まとめ

手軽にできるVPNの設定を書いてみました。利便性が高く、クライアント側の設定がほとんどいらないのがメリットです。分析環境などを用意する際は、データをバケットやRDSに用意することが多いと思いますが、作業するEC2だけにARNを設定したりしてセキュリティをいい感じに高めながら安全に、かつ快適に開発を進めていきたいものです。

英会話の教材をCloud Natural Languageで解析してみた

はじめに

昼休みの1時間、会社近くのゴールドジムへ通ってベンチプレスが日課なのですが、最近胸の形が良くなったと定評のある35歳の男、塚田です。

ちなみに好きなプロテインはリッチミルクです。

さて、トレーニングと同じくコツコツやる系といえば英会話です。 我々の提供しているレアジョブ英会話の中で重要な構成要素の一つは教材です。

レアジョブ英会話が提供している教材は自社で開発しており、様々な国籍の方が働いているチームで品質チェックを行い、 厳しいチェックをパスしたものが世に出ます。 今回はその人気教材の一つ、Daily News Articleを使って、GCPのCloud Natural Languageで自然言語解析を行いたいと思います。 www.rarejob.com

解析対象コンテンツ

今回はコンテンツをそのまま機械的に翻訳をかけるのではなく、 作業端末内にテキストとして保存したところから解析を始めています。 ちなみに解析したコンテンツはこちら。

www.rarejob.com

A new robot created by a team of Toyota engineers set a Guinness World Record after demonstrating its impressive free-throw shooting skills.

という出だしからわかる通り、Guinness World Record を取得したToyota engineers チームの話しです。

解析エンジン

GCP では主に、2つの自然言語解析機能が提供されています。

  • AutoML Natural Language (最小限の労力と機械学習の専門知識で高品質な独自のカスタム機械学習モデルをトレーニングすることで、感情の分類、抽出、検出ができる)
  • Natural Language API (Natural Language API の強力な事前トレーニング済みモデルにより、デベロッパーは感情分析、エンティティ分析、エンティティ感情分析、コンテンツ分類、構文分析などの自然言語理解の機能を操作できる)

今回は既に学習済みのモデルであるNatural Language APIを活用することにします。 Natural Language APIが提供する機能は以下の通りです。

  • エンティティ分析(analyze-entities)... ドキュメント(領収書、請求書、契約書など)の中のエンティティを識別し、ラベル(日付、人、連絡先情報、組織、場所、イベント、商品、メディアなど)を付ける
  • 感情分析(analyze-sentiment) ... テキストのブロック内で示されている全体的な意見、感想、態度の感情を読み取る
  • コンテンツの分類(classifyText) ... 事前定義された 700 以上のカテゴリでドキュメントを分類
  • 構文解析(analyze-syntax) ... 文の抽出、品詞の特定、各文の係り受け解析ツリーの作成

全部試してみた

エンティティ分析(analyze-entities).

実施コマンド

gcloud ml language analyze-entities --content-file=/tmp/original > /tmp/analyze-entities

結果

{
"entities": [
  {
    "mentions": [
      {
        "sentiment": {
          "magnitude": 0.1,
          "score": 0.1
        },
        "text": {
          "beginOffset": 6,
          "content": "robot"
        },
        "type": "COMMON"
      }
    ],
(中略)
  {
    "mentions": [
      {
        "sentiment": {
          "magnitude": 0.1,
          "score": 0.1
        },
        "text": {
          "beginOffset": 56,
          "content": "Guinness World Record"
        },
        "type": "PROPER"
      }
    ],
    "metadata": {},
    "name": "Guinness World Record",
    "salience": 0.03506625,
    "sentiment": {
      "magnitude": 0.1,
      "score": 0.1
    },
    "type": "EVENT"
  },

単語を取ってくれるようです。 特筆すべきは、Gusiness, World, Recordのように認識されてもおかしくないのに、 Guinness World Recordのようなワード(複合名詞)を1つの単語として扱ってくれます。

これについて調べてみると、 mentions には PROPER と COMMON の 2 つのタイプがあります。 PROPERの場合は、固有名詞。COMMONの場合は普通名詞という風に区別されています。

cloud.google.com

ちなみに、エンティティ分析は、学術的には固有表現認識(Named Entity Recognition )と呼ばれています。 機関名や人物名などを文章の中から識別する問題を解いており、以下のようなマスク処理(データの匿名化)で使われることがあるそうです。

固有表現認識を使って文書を黒塗りする - Qiita

感情分析(analyze-sentiment)

実施コマンド

gcloud ml language analyze-entity-sentiment --content-file=/tmp/original > /tmp/analyze-entity-sentiment

結果

{
"documentSentiment": {
  "magnitude": 5.2,
  "score": 0.3
},
"language": "en",
"sentences": [
  {
    "sentiment": {
      "magnitude": 0.9,
      "score": 0.9
    },
    "text": {
      "beginOffset": 0,
      "content": "A new robot created by a team of Toyota engineers set a Guinness World Record after demonstrating its impressive free-throw shooting skills."
    }
  },

各センテンス毎に全体的な態度(ポジティブかネガティブか)を特定する分析。 score と magnitude の数値によって表されるようです。

コンテンツの分類(classifyText)

実施コマンド

gcloud ml language analyze-entity-sentiment --content-file=/tmp/original > /tmp/analyze-entity-sentiment

結果

{
"categories": [
  {
    "confidence": 0.59,
    "name": "/Sports"
  }
]
}

文章全部を読んで、Sports ですね。というカテゴリーに分類されました。 自動でタグを打ちたい時とか便利かもしれません。

構文解析(analyze-syntax)

実施コマンド

gcloud ml language analyze-syntax --content-file=/tmp/original > /tmp/analyze-syntax

結果

{
  "language": "en",
  "sentences": [
    {
      "text": {
        "beginOffset": 0,
        "content": "A new robot created by a team of Toyota engineers set a Guinness World Record after demonstrating its impressive free-throw shooting skills."
      }
    },
    {
      "text": {
        "beginOffset": 142,
        "content": "Toyota Motor Corporation\u2019s robot, CUE3, completed 2,020 consecutive basketball free throws in a demonstration last May."
      }
    },
(中略)
    {
      "text": {
        "beginOffset": 970,
        "content": "In a separate demonstration meant to test CUE3\u2019s ability to make three-point shots, the robot was able to score 62.5% of the time."
      }
    },
    {
      "text": {
        "beginOffset": 1103,
        "content": "This figure beats NBA player Kyle Korver\u2019s single season record of 53.6% and two-time NBA MVP Stephen Curry\u2019s average of 43.6%."
      }
    },

(中略)
    {
      "dependencyEdge": {
        "headTokenIndex": 23,
        "label": "AMOD"
      },
      "lemma": "impressive",
      "partOfSpeech": {
        "aspect": "ASPECT_UNKNOWN",
        "case": "CASE_UNKNOWN",
        "form": "FORM_UNKNOWN",
        "gender": "GENDER_UNKNOWN",
        "mood": "MOOD_UNKNOWN",
        "number": "NUMBER_UNKNOWN",
        "person": "PERSON_UNKNOWN",
        "proper": "PROPER_UNKNOWN",
        "reciprocity": "RECIPROCITY_UNKNOWN",
        "tag": "ADJ",
        "tense": "TENSE_UNKNOWN",
        "voice": "VOICE_UNKNOWN"
      },
      "text": {
        "beginOffset": 102,
        "content": "impressive"
      }
    },

センテンスがキリのいいところで区切られます。 改行が \u になってしまうので、そのまま使うわけにはいかないので、ちょっと工夫が必要なようです。

単語も拾えます。 特に "tag": "ADJ", "content": "impressive" という情報はいろいろ流用できそうです。

We're hiring!

レアジョブは右肩上がりで成長している英会話事業会社です。 その中でも、完全自社コンテンツを持っているのは強みの一つで、 例えば今回の自動解析、自動分類によって、単語アプリを新しく世に出す事も選択肢になるでしょう。 我々は全方位で仲間を募集しています。 https://appeal.rarejob.co.jp/recruit/

興味ある人がいましたら、ぜひ一緒にランチでも食べながら「こんなことできそうだよね」って話しができたら嬉しいです。

nscdでDNS問い合わせを減らす

DevOps チームの うすい と申します。

標題の通り、減らしていきます。

nscdでDNS問い合わせを減らす

当社のレアジョブ英会話の主要なサーバは AWS 上にあるのですが、AWS では このような 制限が存在し、ある程度のアクセスに達すると DNS の問い合わせに失敗してしまいます。 実際にレアジョブ英会話でもレッスンが多い時間帯において、データベース接続エラーが発生しました。

そこで、DNS への問い合わせ件数を減らすために問い合わせ結果を各サーバ内でキャッシュすることとしました。

下記の記事も参考にしてください。

aws.amazon.com

上記記事では dnsmasq を使用していますが、今回は nscd を使いたいと思います。

インストール後、設定ファイルを変更して起動するだけとなります。 設定ファイルの例です。

#
# /etc/nscd.conf
#
# An example Name Service Cache config file.  This file is needed by nscd.
#
# Legal entries are:
#
#   logfile         <file>
#   debug-level     <level>
#   threads         <initial #threads to use>
#   max-threads     <maximum #threads to use>
#   server-user     <user to run server as instead of root>
#   server-user is ignored if nscd is started with -S parameters
#   stat-user       <user who is allowed to request statistics>
#   reload-count    unlimited|<number>
#   paranoia        <yes|no>
#   restart-interval    <time in seconds>
#
#   enable-cache        <service> <yes|no>
#   positive-time-to-live   <service> <time in seconds>
#   negative-time-to-live   <service> <time in seconds>
#   suggested-size      <service> <prime number>
#   check-files     <service> <yes|no>
#   persistent      <service> <yes|no>
#   shared          <service> <yes|no>
#   max-db-size     <service> <number bytes>
#   auto-propagate      <service> <yes|no>
#
# Currently supported cache names (services): passwd, group, hosts, services
#


#   logfile         /var/log/nscd.log
#   threads         4
#   max-threads     32
    server-user     nscd
#   stat-user       somebody
    debug-level     0
#   reload-count        5
    paranoia        yes
    restart-interval    60

    enable-cache        passwd      yes
    positive-time-to-live   passwd      600
    negative-time-to-live   passwd      20
    suggested-size      passwd      211
    check-files     passwd      yes
    persistent      passwd      yes
    shared          passwd      yes
    max-db-size     passwd      33554432
    auto-propagate      passwd      yes

    enable-cache        group       yes
    positive-time-to-live   group       3600
    negative-time-to-live   group       60
    suggested-size      group       211
    check-files     group       yes
    persistent      group       yes
    shared          group       yes
    max-db-size     group       33554432
    auto-propagate      group       yes

    enable-cache        hosts       yes
    positive-time-to-live   hosts       3600
    negative-time-to-live   hosts       20
    suggested-size      hosts       211
    check-files     hosts       yes
    persistent      hosts       yes
    shared          hosts       yes
    max-db-size     hosts       33554432

    enable-cache        services    yes
    positive-time-to-live   services    28800
    negative-time-to-live   services    20
    suggested-size      services    211
    check-files     services    yes
    persistent      services    yes
    shared          services    yes
    max-db-size     services    33554432

    enable-cache        netgroup    no
    positive-time-to-live   netgroup    28800
    negative-time-to-live   netgroup    20
    suggested-size      netgroup    211
    check-files     netgroup    yes
    persistent      netgroup    yes
    shared          netgroup    yes
    max-db-size     netgroup    33554432

定期的に nscd 自身には再起動してもらいたいので、paranoiayesにしてrestart-intervalを設定しています。 netgroup は使用していないのでenable-cachenoにしています。/etc/netgroupを nscd が見に行ってエラー吐いちゃいますので。 あと nscd が再起動されるとその度にログが吐かれるので、/var/log/messages 的なのがまぁまぁ増えますのでご注意ください。 また、AWS を使用しているとあるあるだと思いますが、各 Endpoint の後ろにいるインスタンスなりは AWS 側のタイミングで切り替わるので、再起動の間隔やキャッシュ保持時間などは調整する必要があるかと思います。

結果

tcpdump dst port 53 などしてみた結果です。

nscd 有効

tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
3624 packets captured
3636 packets received by filter

無効

tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
10444 packets captured
10449 packets received by filter
減らせました。

レアジョブ英会話は PHP で動いており、ABI が変更されたときに出るようなgetaddrinfo failedの発生にびっくりしたのですが、今回の設定で落ち着いた次第です。