RareJob Tech Blog

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

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の発生にびっくりしたのですが、今回の設定で落ち着いた次第です。

見やすいページを簡単に作る

どうも!デザイナーの渡辺です!

最近エンジニアさんたちと同じ部署に配属されました!

これからたまに出てくると思います!
よろしくお願いしますヽ(=´▽`=)ノ



さてさて、せっかくデザイナーとエンジニアが一緒の部署になったということで、
デザイナーの知識を皆さんにお届けできればなと思います!
なので今回は誰でも簡単にできる、
見やすいページを作る小技をご紹介いたします。パチパチ

やることは簡単! 分ける・空ける・揃える

これだけですね(゚∀゚)
といってもなにをすればいいのって話ですよね!

それでは1つ1つ説明していきます!
(っ’-‘)╮ =͟͟͞͞ 【分ける・空ける・揃える】 ブォン

今回はこちらの情報をもとに説明していきます! f:id:yui_1027:20191024113611p:plain

分ける 大カテゴリ、中カテゴリ、タイトル、テキスト、イメージなど早い話が仕分けですね!
一体それがどんな役割を果たすのか明確にします!

空ける 余白を入れてグループとグループの間を空けます これだけでだいぶ見やすくなりました!

揃える 分けた要素はグループごとに同じ見た目に、
空けた余白は類似のもので同じサイズに揃える!
たったこれだけで断然見やすく情報を伝えられるページになります!

実はこれは小技というよりは、デザインの基本ですね!

逆を言えば、こういう部分に着目するだけで
そのページをデザイナーが作ったかそうでないかがわかります!

「あ、揃えるとこ揃えてない。これデザイナーじゃないな。フム」

こんな具合です |ω・)ミテマスヨ

ページを作るときは、騙されたと思って

分ける・空ける・揃える

を合言葉にぜひ作成してみてください!

それだけであら不思議!見やすいページの出来上がりヽ(=´▽`=)ノ
パワポなどでの資料作成でも使えると思います!

なんだこんな簡単なら、デザイナーもいけるんじゃね?ってなります!
これが簡単そうで意外と出来ないものなんですよ( ・´ー・`)ドヤ

しかしそれが出来たらあなたはもうデザイナー!
ようこそこちらの世界へ!(∩´∀`)∩ワーイ

「そんなの知ってるよ!当たり前じゃないか!」

そう思ったそこのデザイナーさん! 我々はデザイナーを募集しております!ヨッ

www.wantedly.com

見やすいページを作る合言葉は

分ける・空ける・揃える

覚えてくださいねー!
 それでは ノシ

レッスンルームのチャットUIを MessageKit に差し替えたときのツラミとレガシーコードとの向かい方

お疲れさまです。 APP/UI チームの玉置です。

最近のマイブームは鬼滅の刃(きめつのやいば)です。
ツイッターのタイムラインで鬼滅の刃のワンシーンを見てしまい「おっ、刀がメインのアニメか!」と思ってNetflix で動画を見始めたのがきっかけです。
当時の動画のワンシーンは登場キャラの善逸くんが雷の呼吸「雷の呼吸 壱の型 霹靂一閃」を解き放って敵キャラを瞬殺したシーンです。
善逸くんのキャラが「るろうに剣心」の十本刀の「宗次郎」にとても似ていてそのシーンだけで気に入ってしまいました。
(* 善逸くんは宗次郎のキャラとは全然違います)

そんなこんなで動画を見つけた日は善逸くんの上記のシーンまでの動画を徹夜で観ていました。

さて、そんな話しは置いとくとして今日は先月ずっと行っていた
レッスンルームのチャットのデザインで使っていたライブラリのリプレイス作業をずっと行っていました。
そして、やっと今月リリースできそうなのでその当時に色々思うことがありましたので文章化してみました。

リプレイスする前に使っていたライブラリについて

リプレイス前に使っていたライブラリは「JSQMessagesViewController」という名称のものです。

github.com

2016年頃まではチャットアプリでごく一般的に使われていた有名なライブラリです。
こちらのライブラリですが2016年に大体の開発が終了してしまって2017年1月で完全に開発がストップしてしまいました。

f:id:qed805:20190929184418p:plain
github_commit_log

現在は非推奨となっています。
レアジョブアプリはライブラリ管理でcocoapodsを採用しているため、ターミナルでpod installをした際にこのライブラリが非推奨というワーニングが表示されます。

f:id:qed805:20191014183045p:plain
depricated_JSQMessagesViewController

今はチャットライブラリで有名なものとして「MessageKit」というOSSがあります。
チャットアプリを開発する場合は自作するかこちらのライブラリを使うことが一般的になっています。

github.com

今回はこの「JSQMessagesViewController」からの脱却をメインに書いていきます。

リプレイスするきっかけと足枷になったこと

本当はすぐにでもJSQMessagesViewControllerを脱却したい気持ちがありました。

その理由を挙げるだけでも

  • 2017 年から公式が非推奨
  • 公式からのサポートが得られない
  • 事業的にリファクタリングよりも新規機能の開発が優先
  • リファクタリングのコストを支払っても利益が出ないため報われない
  • リプレイスにかかるコスト(工数見積が定かではない)
  • iOSのライブラリ管理ツールである「CocoaPod」からdepricatedというワーニングが表示

などの要因がありました。
また、当社にはカスタマーサポートの部署があり(以下、CSと省略します)、CSよりユーザー様から
「講師側から長い文章が送信されると最後の1行が途切れている」という問い合わせを受けていました。

f:id:qed805:20191014184124p:plain
最後の1行のメッセージの空白

この問題に対して臨時対応を5月頃に実施して解消されているはずでしたが今年8月にも別のユーザー様から同じような問い合わせを受けてしまいました。

そのため使っていただいているユーザー様のためにもできるだけ早くこの問題の根本原因を解消しなければならないと思い、
チームのタスク的にも8月までに必要なタスクを終えて9月は1ヶ月丸々リファクタリング期間を頂いたのでこれを機会にチャットUIライブラリを差し替える業務に本格的に着手することにしました。

リプレイスする上でのツラミ

実際にライブラリのリプレイスをすると決意してからが大変です。
今回のライブラリは「レッスンルーム」に直接影響を与える箇所ですのでデグレに気をつけなければなりません。
その他、個人的に感じたリプレイスする上でのツラミは

  1. 開発メンバーに申し訳ない感
  2. iOSのキーボードが意図通りに表示してくれない
  3. 差し替えたあとにiPad横画面でデグレが起きる

という三点でした。

それぞれ解説していきます。

開発メンバーに申し訳ない感

こちらはリプレイス云々とは関係ありませんが事前に工数見積をしていても全てが事前に見積もれる訳ではありません。
特に今回のようなレアジョブアプリのメイン機能である「チャット」のデザインだと寧ろデザイン崩れやデグレが発生している方が問題になります。
つまり既存仕様を全て正確に新しいライブラリで再現する必要がでてきます。
ただしどうしても新しいライブラリで「再現できない機能」はチームメンバーやプロダクトオーナーに相談して捨てることは可能だと思っています。

デグレ」と「捨てて良い機能」の定義はタスクや機能によって違いますが、
今回の定義では「事前に把握しているかどうか」です。

それはそうとして話しを戻しますが
今回のような見え方や挙動がだいたい同じデザインだと、作業中も私の中ではタスク全体として「達成感」はあまり感じられませんでした。
着手前と着手後で表向きとしては成果物が「同じ」ですので事業にインパクトを直接与える作業ではないのであまり楽しくなかったんですよね。

なので自分が楽しくないことにひたすら挑戦していたので(おまけに事業の売上に貢献していないことも含めて)
メンバーにその感覚が伝わってそうでリプレイス中はずっとメンバーに申し訳ない気持ちで一杯でした。

このツラさを解消できるKPTでいうところ、「Try」があればいいなと思いました。
そのためチーム全体が楽しくレガシーコードと向き合える方法があればリファクタリングが楽しいものになるんだろうなと思いました。

  1. iOSのキーボードが意図通りに表示してくれない

後述する内容ですがリプレイス先の「MessageKit」のInputBarAccessoryViewがうまく意図通りに動いてくれなかったため、全ての挙動を既存仕様に合わせることが難しかったです。
特にキーボードの表示・非表示の制御がiPhoneiPadに関わらず大変で着手中ずっとキーボードの制御に苦しめられました。
レアジョブアプリではチャットのviewがメインのViewControllerにCotainerとして乗っている設計でchildViewControllerになっています。

(main) RoomViewController > MenuViewController > ChatViewController (child)

という設計になっています。

この場合は ChatViewController で MessageKit のInputBarAccessoryViewを表示しようとすると初期表示時にInputBarAccessoryViewが画面外に隠れてしまう現象が起きます。

f:id:qed805:20191014193557p:plain
InputBarAccessoryViewが画面下にあるので表示されない

このような問題にも対応しなければなりませんでした。

念の為、この問題を解消するのに参考になったページを張っておきます。

github.com

github.com

  1. 差し替えたあとにiPad横画面でデグレが起きる

最後のツラミとして、今年5月頃に私が開発したものですがiPadのレッスンルームを横画面にも対応したのですが、MessageKitがそのあたりもいい感じに補正してくれると期待していたところ見事にそれを裏切ってくれました。

iPhoneでだいたい既存の同じ挙動になることを確認してからデグレ起きなかったらいいなと淡い期待を持っていましたが、

単純にJSQMessagesViewControllerからMessageKitに差し替えてもiPadを横画面にすると既存と全く同じ挙動にならなかったのです。
ライブラリリプレイスの本実装の調査前の段階である程度工数見積をしましたが、
iPad横画面ではその当時確認していませんでした。できなかったこともありますが。
そのため実際にこれを改修する前にデザインが崩れてしまう現象のパターンを洗い出して再度工数見積を行いました。

着手前に見積もっていた工数も残りわずかというか使い切ってしまいましたので、
恐怖に思いながらiPad横画面でのデグレ箇所の工数見積をしていました。

想定外のデグレ工数はメンタルに良くないですね。

実際のリプレイス作業

ということで長々と書いてしまいましたが、ここで実際にライブラリをリプレイスしたときに行った手順を復習します。

ライブラリ自体の調査と挙動の調査 (工数見積まで)

  • アプリの既存の動きの調査
  • MessagesKit自体のデザインの調査
  • MessagesKitがどこまで柔軟に対応できるか
  • 試しimport でどれだけのデグレが発生しているか

実際にライブラリをimportする (実際に着手開始)

  • MessageInputBar が使えず、InputBarAccessoryViewが必要
  • どのバージョンのInputBarAccessoryViewを使えばいいのか情報が少ないのでその調査を行う

そのためcocoapodのPodfileは下記のようにInputBarAccessoryViewをimportしました。

  pod 'MessageKit'
  pod 'InputBarAccessoryView', '4.2.2'

ハマリポイントの改修 (想定外の対応)

  • ChildViewControllerでは初期表示時にInputBarAccessoryViewが表示されない
  • レッスンルームを一つのViewController で管理できるかの調査 (既存設計を大きく変える方法の調査)
  • メインのViewController で becomeResponder()を読んでinputBarを意図的に表示させたときの調査 (既存設計を踏襲する場合の調査)
  • iPad横画面でのデグレの対応

上記のような流れになりました。

レガシーコードと向き合った感想と気をつけるべきこと

今回はレガシーコードではなくライブラリのリプレイス作業について解説してみました。
新規事業だとガンガン突き進めて事業にもインパクトを与えられますがレガシーなものを新しいものに書き換えるようなリプレイス作業は見た目が変わらないので作業していてもなかなか辛い部分がありました。

ですがレガシーコードは綺麗にせずにそのままにしておくと将来の負債となったりメンテナンスできなくなって「ゼロから作り直し」になったりする非常にセンシティブな部分でもあったりします。
現場によってはプロジェクトのソースコードを「1から作り直し」になってしまった所もあるのではないでしょうか。
モバイルアプリは最初の1,2年は開発していてとても楽しいですがさらに長く運用していると様々な要因で昔のレガシーコードが影響して開発スピードが著しく遅くなってしまう傾向がありますね。

またAppleGoogleSDKが年々新しくなって昔のソースコードがいきなり非推奨になったりします。
なので、時間がある時にできる限りソースコードを綺麗にして保守性を上げる開発体制が一番安全かなと思いました。

今回のチャットUIを新しくしたことでレアジョブアプリを使って頂いているユーザー様により快適にレッスンルームをご利用頂けると思うと開発者として報われる気持ちになりますね。

ということで長文になりましたがぜひレッスンルームを使って頂ければと思います。