Kesinの知見置き場

知見を共有していきたいじゃないですか

FirebaseのCloud Functionでpuppeteerを動かす解説

先日、GAEとCloud Functionsでpuppeteerを動かすコードを紹介しているエントリが公開されました。 cloud.google.com

puppeteer、というかChromeを動かすためには実は様々な依存ライブラリをインストールする必要があるのですが、Cloud Functionでは環境そのもののカスタマイズはできなかったので今まではpuppeteerを動かすことはできませんでした。
GAEとCloud Functionでnodejs 8が使えるようになったのと同時に、実行環境のベースのイメージが新しくなった(Ubuntu 18.04とのこと)のでpuppeteerが使えるようになったようです。

ちょっと試してみる分には、先程の記事のソースコードgithubにアップされているので自分のGCPのプロジェクトにデプロイすることで動くことが確認できると思います。

ただFirebaseからCloud Functionsを使っている場合、エントリで紹介されている方法では個人的に不便なところがありました。

  • Firebaseを使っているとfirebase deploy --only functions でデプロイが済むので、gcloudのコマンドを使いたくない
  • nodejs 8のランタイム指定をGUIではなく、コードの中か設定ファイルに書いておきたい
  • 同様にマシンのスペックもコードか設定ファイルに記述したい
  • firebase servefirebase function:shellを使ってローカル環境でデバッグするときにはヘッドレスモードをオフにしたい

それぞれを実現できないか調べたところ、全て可能だったので紹介していきます。

ランタイムの指定

firebase initでCloud Functionsを使うようにセットアップした場合、package.jsonのscriptsのところに”deploy": "firebase deploy --only functions”というコマンドが追加されており、npm run deployだけでデフォルトではindex.jsでexportsしている関数がすべてCloud Functionsにデプロイされます。
ただ、firebase deployでは先程のブログで紹介されているgcloud beta functions deployのようにランタイムを指定するオプションは存在しません。

ではどうするかというと、実はpackage.json”engines”: {“node” : “8” }という設定を追加することで、firebase deployしたときに自動的にランタイムがnodejs 8に設定されます。
https://firebase.google.com/docs/functions/manage-functions?hl=en#set_nodejs_version

ドキュメントによるとCloud Function 2.0.0、firebase-tools 4.0.0から可能と書いてあるので、おそらく最近増えた項目だと思われます。

実行環境のマシンスペックの指定

ランタイムの指定と同様に、firebase deployのコマンドにはマシンスペックを指定するオプションはありません。 今度はpackage.jsonではなく、デプロイする関数のコード中で関数のタイムアウト時間と実行環境のマシンスペックを指定します。

const runtimeOpts = {
  timeoutSeconds: 300,
  memory: '1GB'
}

exports.myStorageFunction = functions
  .runWith(runtimeOpts)
  .storage
  .object()
  .onFinalize((object) = > {
    // do some complicated things that take a lot of memory and time
  });

コードはこちらから https://firebase.google.com/docs/functions/manage-functions?hl=en#set_timeout_and_memory_allocation

指定できるメモリサイズは今の所128MB、256MB、512MB、1GB、2GBの5種類と決められています。

タイムアウトについて

ドキュメントにも書いてありますが、timeoutSecondsの最大はCloud Functionsの制約上540(9分)となっています。

MAX実行時間が9分というのは結構重要な制約です。大量の処理をpuppeteerで行わせる場合は、1回の実行が9分に収まるように外部にキューなどの仕組みを用意してタスクを小分けにする必要があるでしょう。

マシンスペック

実行環境のマシンのメモリとCPUは固定されており、先程のコードでmemory: '1GB'とした場合のCPUスペックは1.4GHzです。メモリ512MBの環境ではCPUは800MHz、2GBの環境ではCPUは2.4Ghzという具合です。
https://cloud.google.com/functions/pricing?hl=en#compute_time

puppeteerはそこそこ重いのでサンプルコードでも’1GB’が使われています。ただ、スペックが高いと料金に影響(詳しくは後述)するので試しにワンランク下の512MBで試したところ自分の環境では問題なく動作しました。
長期的に試していないので512MB環境での安定性はまだ分かりませんが、多少動作が遅くても問題のない用途であればスペックを抑えるという選択肢はアリかもしれません。

デバッグ時はヘッドレスモードをオフにしたい

Seleniumやpuppeteerを使ったことがある方であれば分かると思いますが、本番環境で動かす場合にはChromeのヘッドレスモードが必須であるものの、ブラウザに表示されているものが分からない状態ではデバッグすることが非常に困難です。

そもそもCloud Functions自体をデバッグするのもなかなか大変ですが、FirebaseにはCloud Functionsをローカル環境でエミュレートするfirebase serve --only functionsfirebase functions:shellというコマンドが用意されています。
従って、あとはローカル環境で動かすときにヘッドレスモードをオフにしてpuppeteerを起動すれば実現できそうです。

Cloud Functionsは、Googleのサーバーで実行されるときにはNODE_ENV環境変数に’production’が入っているので、環境変数に応じてpuppeteerを起動するときのオプションを変更します。

async function getBrowserPage () {
  const isDebug = process.env.NODE_ENV !== 'production'
  console.log("NODE_ENV: ", process.env.NODE_ENV)

  const launchOptions = {
    headless: isDebug ? false : true,
    args: ['--no-sandbox']
  }

  const browser = await puppeteer.launch(launchOptions)
  return browser.newPage()
}

ローカルで実行した場合にはChromiumGUIが立ち上がりますので、実際にブラウザが動く様子を見ながらpuppeteerのコードを効率よく書くことができます。

ソースコード全体

ここまで紹介した内容を冒頭のブログで紹介されているコードに適用すると、このようになります。
あとは、通常のFirebaseでCloud Functionsをデプロイするのと同じようにfirebase deploy --only functionsするだけでpuppeteerが動くfunctionをデプロイできます。

// package.json
{
  "engines": { "node": "8" },
  // 他は省略
}
// index.js
import * as functions from 'firebase-functions'
import puppeteer from 'puppeteer'

let page

async function getBrowserPage () {
  const isDebug = process.env.NODE_ENV !== 'production'

  const launchOptions = {
    headless: isDebug ? false : true,
    args: ['--no-sandbox']
  }

  const browser = await puppeteer.launch(launchOptions)
  return browser.newPage()
}

exports.screenshot = functions
.runWith({timeoutSeconds: 540, memory: '1GB'})
.https.onRequest(async (req, res) => {
  const url = 'http://www.google.com'

  if (!page) {
    page = await getBrowserPage()
  }

  await page.goto(url)

  const imageBuffer = await page.screenshot()
  res.set('Content-Type', 'image/png')
  res.send(imageBuffer)
})

料金

最後にお金の話をしましょう。
Googleは太っ腹なことに、Cloud Functionsの呼び出しをなんと200万回まで無料としてくれています。ただし、よくよくドキュメントを読んでいくと実は呼び出し回数の他に、計算に使用したメモリとCPUによっても課金されるということが分かります。
普通の用途であればデフォルトの低スペック環境なので問題になることは少ないと思いますが、puppeteerのために1GBの環境にスペックを上げていたことを思い出してください

うっかり使いすぎてクラウド破産しないよう、事前にある程度の見積もりは計算しておくことが大事です。参考までにどれぐらいであれば無料枠の範囲におさまるのか自分が計算した結果を貼っておきます。
(2018/09/01の料金表を元に計算しています。さらに自分の計算が正しいことを保証するものではありません)

  • 512MB環境で1回の実行が9分の場合: 540回/月
  • 1GB環境で1回の実行が9分の場合: 260回/月

デフォルトの200万回まで無料と比較すると圧倒的に少ない回数しか無料枠には収まらないことが分かります。
どのような用途で使用するのかしっかりと考慮した上で、GCEやGAEで動かした場合と比較して本当にCloud Functions上で動かす価値があるのかは考えましょう。

実際に見積もりをする場合にはドキュメントの計算式を参考にスプレッドシートで計算シートを作成するか、https://cloud.google.com/products/calculator/#tab=functions で見積もりをしておくとよいでしょう。

まとめ

FirebaseからCloud Functionsでpuppeteerを使う場合に必要な設定を全てコード中に含める方法や、ちょっとしたテクニックの紹介をしました。

最後にはなりましたが、そもそもクラウドでpuppeteerを使うにしてもGCEやGAEでなく、なぜあえて制約の多いCloud Functionsでやるのか?という疑問があるかもしれません。
まだ実用例がほとんど見当たらないので個人的な予想ではありますが、例えばE2Eテストの用途ではCloud Functionsの特長を生かして圧倒的に並列にスケールさせることができれば実行時間を短縮させることができるはずです。

実際にAWS lambdaではそのような使い方が紹介されていたりします。
aws.amazon.com

近いうちにCloud Functionsを使ったE2Eテストの実験してみるつもりなので、うまく実現できたらまたブログで紹介しようと思います。

BigQueryでinsert or updateを実現する

BigQueryにデータをインポートする基本的な手段はloadCSVJSONのデータからテーブルを作成したり、既存のテーブルへの追記です。
ただ、今回は要件的にRDBでいうところのinsert or update(またはupsertとも呼ばれる)を行いたかったので、BigQueryで可能なのか調べてみました。

tl;dl

MERGEを使いこなせば可能。ただし料金には注意。

INSERT/UPDATE

BigQueryはSQLのINSERT/UPDATE/DELETEによってテーブルのデータを直接編集することが可能です。
https://cloud.google.com/bigquery/docs/reference/standard-sql/dml-syntax

ただし、残念なことにMySQLにおけるINSERT ON DUPLICATE KEY UPDATEpostgresqlINSERT ON CONFLICE UPDATEに相当するいわゆるinsert or updateの機能は存在していません。

なんとか実現するとしたら、更新したいデータのうちBigQueryのテーブルに既に存在するものと、まだ存在していない2種類に分割してそれぞれをINSERT/UPDATEすることで一応可能でしょう。
しかし、データを分割するのに手間がかかるだけではなく、BigQueryの料金は1クエリ毎に計算される仕組み(料金については後述)なのでその点でも非効率です。

MERGE

BigQueryにはINSERT/UPDATE/DELETEに加えて、MERGEという操作が存在します。 詳しい説明はドキュメントにあるのですが、説明がなかなか複雑なのでどういう挙動をするのか初見では理解するのが難しいと思います。
自分の理解でざっくりと説明すると、MERGEは操作する対象のテーブル(target)と変更するためのデータのテーブル(source)をJOINして、その結果に応じてINSERT/UPDATE/DELETEが行える操作です。

という挙動であることを把握してからドキュメントのサンプル事例を見ると、これを活用することでinsert or updateの挙動が実現できることに気がつきます。
まず、変更したいテーブルをtarget、変更するためのデータを適当なテーブルにアップロードしてsourceとし、それらをPrimary Keyと考えたいカラムでJOINします。そして、既にPrimary Keyが存在する場合はUPDATE、存在しない場合はINSERTを実行します。

実際にBigQueryのSQLにすると以下になります。

#standardSQL
MERGE
  `blog.target` AS T
USING
  `blog.source` AS S
ON
  T.user_id = S.user_id
WHEN MATCHED  THEN
  UPDATE SET T.comment = S.comment
WHEN NOT MATCHED THEN
  INSERT (user_id, comment)
  VALUES (user_id, comment)

これを以下のtargetとsourceのテーブルに対して実行してみます。

targetテーブル

user_id comment
A null
B null

sourceテーブル

user_id comment
B 'Merge update'
C 'Merge insert'

MERGE実行後のtargetテーブル

user_id comment
A null
B 'Merge update'
C 'Merge insert'

狙い通り、1つのクエリだけでinsert or updateの挙動が実現できていますね。

料金について

MERGEは大変便利なのですが、BigQueryにデータを追加する基本手段のloadが無料であるのに対して、MERGEはSELECT同様に課金が発生しますのでここについても触れておきます。

INSERT/UPDATE/DELETEの料金は現在のドキュメントでは以下となっています。 https://cloud.google.com/bigquery/pricing#free-tier https://cloud.google.com/bigquery/pricing#on_demand_pricing

注意すべきは、どれだけテーブルのデータ数が少なかったとしても最低のデータ処理容量は10MBだということです。

料金は MB 単位のデータ処理容量(端数は四捨五入)で決まります。クエリが参照するテーブルあたりのデータ最小処理容量は 10 MB、クエリあたりのデータ最小処理容量は 10 MB とします。

BigQueryの無料枠は1GB/月ですので、100回を超えてクエリを実行するとそこから課金が発生します。ちなみに、SELECTも最小容量は10MBですがJOINすると倍の20MBになるので注意です。

そして、MERGEもJOINと同じく最小容量は20MBです。つまり50回を超えると課金発生です。1ヶ月に50回ですので案外少ない回数で無料枠を使い切ってしまいますね。

ちなみに先ほどのサンプルSQLを実行した結果はこのようになっており、Bytes Billedが実際に20MB分となっていることが分かります。

Bytes Processed  44 B
Bytes Billed    20.0 MB

20MBはあくまで最小容量ですので、操作対象となるテーブルのデータ量が多い場合は、当然1クエリあたりの料金は比例して多くなります。バッチ処理などで定期的にMERGEを使う想定の場合、一度クエリを実行してみてそのときの情報から一ヶ月単位での利用料を軽く見積もってみるといいでしょう。

注意

前述の料金の説明は2018/06/18日時点でのドキュメントを参考にしています。
また、MERGEについても執筆時点ではベータ版です。従って、将来的に使い方が変更される可能性もゼロではないです。

参考

Firestoreをもっと手軽に使えるfirestore-simpleを作った

追記: v2についてのエントリはこちら

Firebaseの新しいDBであるFirestoreは使っているでしょうか?
今まではどうしてもサーバー側の実装が必要だったDBへのデータの格納がクライアントだけで済むようになったので圧倒的に便利ですね。
そんなわけで自分もjsからFirestoreを使う機会が増えてきたのですが、毎回書くたびにAPIが微妙に使いづらいと感じていました。そこで、Firestoreをいい感じに扱えるfirestore-simpleというライブラリをリリースしました。

サンプルコード

まず、比較のためにFirestoreを素で使った場合のコードです。

const admin = require('firebase-admin')
const serviceAccount = require('./firebase_secret.javascripton')
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) })

const main = async () => {
  const firestore = admin.firestore()
  const collectionPath = 'users'
  await firestore.collection(collectionPath).add({ name: 'alice', age: 20 })
  await firestore.collection(collectionPath).add({ name: 'bob', age: 22 })

  const user_documents = await firestore.collection(collectionPath).get()
  const users = []
  user_documents.forEach((document) => {
    const data = document.data()
    users.push({ id: document.id, name: data.name, age: data.age })
  })
  // [ { id: 'lZ9HtQAEJvumL9grjGtc', name: 'bob', age: 22 },
  // { id: 'rFx0a6dZQZPIHEvXGGtO', name: 'alice', age: 20 } ]

  const batch = firestore.batch()
  users.forEach((user) => {
    const documentRef = firestore.collection(collectionPath).doc(user.id)
    batch.delete(documentRef)
  })
  await batch.commit()

}
main()

そしてこちらがfirestore-simpleを使った場合のコードです。

const admin = require('firebase-admin')
const { FirestoreSimple } = require ('firestore-simple')
const serviceAccount = require('./firebase_secret.json')
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) })

const main = async () => {
  const firestore = admin.firestore()
  const collectionPath = 'users'
  const dao = new FirestoreSimple(firestore, collectionPath)
  await dao.add({ name: 'alice', age: 20})
  await dao.add({ name: 'bob', age: 22 })

  const users = await dao.fetchCollection()
  // [ { id: '3mBYA0uXw0lw6b883jgk', age: 20, name: 'alice' },
  // { id: 'lE9SfP8f3H8w0Uwk0euQ', age: 22, name: 'bob' } ]

  const ids = users.map((user) => user.id)
  await dao.bulkDelete(ids)
}
main()

どうでしょうか。素のFirestoreに比べて短く、スッキリとしたコードで書けると思います。

firestone-simpleの特徴

1インスタンス1コレクション

素のFirestoreでは例えばuserにアクセスする場合、firestore.collection(‘user’)と書く必要があります。コードの各所からuserのドキュメントにアクセスしたい場合、毎回同じコードを書くのはイヤなので自分ならUserDaoとかUserModelみたいなクラスを作り、そのクラス経由でアクセスするようにしたいです。

firestore-simpleはコンストラクタにcollectionPathを渡してインスタンスを作ります。1インスタンス1コレクションという制約を設ける代わりに、collectionPathを指定するのは最初の1回だけで済みます。

普通のObjectを返す

Firestoreのドキュメントを取得するとき、得られるのはDocumentSnapshotなのでidを参照するにはdoc.id、それ以外の要素にアクセスするにはdoc.data()が必要です。 そのため、{title: ‘test’}というドキュメントをFirestoreから取り出すときには、いちいちdoc.data().titleとする必要があり地味に面倒くさいです。

firestore-simpleはfetchDocumentfetchCollectionの参照系だけではなくaddsetといった更新系も基本的にObjectを返します。このObjectにはドキュメントのデータに加えてidも追加されているためdoc.iddoc.titleというようにアクセスすることが可能です。

普通のArrayを返す

Firestoreでコレクションから複数のドキュメントを取得する場合、得られるのはQuerySnapshotです。ドキュメントのサンプルコードではforEachを使っているのでmapも使えると思いきや使えません。どうもQuerySnapshotのリファレンスを見る限りではforEachしか実装されていないようで、mapfilterが使えないので大変不便です。

firestore-simpleではfetchCollectionfetchByQueryといった複数のドキュメントを取得するメソッドはArrayを返します。従ってmapも普通に使うことが可能です。

キーの別名マッピングに対応

jsに限らずJSONでデータをやりとりするAPIのクライアントを書くときに地味に面倒くさいのが、キーの名前をsnake_caseやCamelCaseといったクライアント側の言語の流儀に合わせて変換することです。 この変換処理をコードの色々なところで繰り返したくないので、大抵はAPIクライアントかModelクラスにJSONのキーとのマッピングを書くことになると思います。

大規模なコードであればFirestoreのコレクションと1対1で対応するModelクラスを用意してもいいのですが、そこまでしたくない場合のためにfirestore-simpleではキーの別名マッピングに対応しています。

const dao = new FirestoreSimple(firestore, collectionPath, { mapping: {
  createdAt: "created_at"
}})
await dao.add({ name: 'alice', age: 20, createdAt: new Date() })

このようにすると、js側ではcreatedAtというキーで扱いますが、Firestore側のドキュメントではcreated_atというキーで保存されます。

メソッド

参照系

fetchDocument

1つのドキュメントをフェッチします。

await dao.fetchDocument(documentId)

fetchCollectoin

コレクション全てのドキュメントをフェッチします。

await dao.fetchCollection()

fetchByQuery

where, orderBy, limitなどを使ってコレクションから絞りこんだりソートしてフェッチします。

const query = dao.collectionRef
  .where('name', '==', 'alice')
  .orderBy('age')
  .limit(1)
await dao.fetchByQuery(query)

更新系

add

idを自動採番でFirestoreにドキュメントを追加します。

await dao.add({ name: 'alice', age: 20 })

set

引数のObjectのidに対応するFirestore側のドキュメントを更新します。
そのidに対応するドキュメントがまだFirestoreに存在しない場合はそのidで追加されるので、addによる自動採番ではなく任意のidで登録したい場合もsetを使います。

await dao.set({ id: '1111', name: 'bob', age: 22})

addOrSet

引数のオブジェクトにidが含まれていればset、含まれなければaddと同じ挙動になります。
元々のFirestoreには存在しないfirestore-simpleのオリジナル機能です。

await dao.addOrSet({ name: 'alice', age: 20 }) // add
await dao.setOrSet({ id: '1111', name: 'bob', age: 22}) // set

delete

引数のidに対応したドキュメントをFirestoreから削除します。

await dao.delete(documentId)

Bulk系

ドキュメントで一括書き込みと書かれている処理です。複数のドキュメントへの処理をアトミックに行いたい場合は使用します。

bulkSet

引数に渡した複数のオブジェクトでFirestoreのドキュメントを一括で更新します。

await dao.bulkSet([
  { id: '1111', name: 'alice', age: 20 },
  { id: '2222', name: 'bob', age: 22 }
])

bulkDelete

引数に渡した複数のidでFirestoreのドキュメントを一括で削除します。

await dao.bulkDelete(['1111', '2222'])

できないこと

update

Firestoreにはドキュメント更新の方法としてset以外にupdateも存在しますが、自分はupdateを使う必要性を感じなかったので実装していないです。

トランザクション

自分が使いたい用途としてはbulkSetbulkDeleteでほぼ十分だったことと、トランザクションをサポートするには実装が複雑になってしまうことから見送りました。

onSnapshot()

これも自分が今の所使う必要がなかったため未サポートです。 ただ、firestore-simpleのインスタンスからcollectionRefにアクセス可能なので、このようなコードでonSnapshot()を使うことは可能です。

dao.collectionRef.onSnapshot((snapshot) => {
  snapshot.docChanges.forEach((change) =>{
    console.log(change.type)
    console.log(change.doc.data())
  })
})

ReactNative

firestore-simpleはReactNativeFirebaseでも使用可能です。
ReactNativeFirebaseではfirestoreのインスタンスをゲットする方法が多少異なりますが、それ以後の使い方は全く同じです。

import { FirestoreSimple } from 'firestore-simple'
import firebase from 'react-native-firebase';

export default class App extends React.Component {
  // signInAnonymouslyAndRetrieveDataを使う場合はfirebaseのコンソールで匿名ログインをオンにする必要があります
  async getFirestore() {
    await firebase.auth().signInAnonymouslyAndRetrieveData()
    return firebase.firestore()
  }

  async componentDidMount() {
    const firestore = await this.getFirestore()
    const collectionPath = 'sample_react_native'
    const dao = new FirestoreSimple(firestore, collectionPath, {
      mapping: {
        createdAt: 'created_at'
      }
    })

    // ... other usage are same as nodejs client
  }
}

追記6/29: CloudFunction

CloudFunctionでも問題なく使えることを確認しました

const functions = require('firebase-functions');
const admin = require('firebase-admin')
const { FirestoreSimple } = require('firestore-simple')
admin.initializeApp()

exports.helloFirestore = functions.https.onRequest((request, response) => {
  const firestore = admin.firestore()
  const dao = new FirestoreSimple(firestore, 'cloud_function')
  dao.add({ name: 'alice', age: 20}).then(doc => {
    console.log(doc)
    return response.send("add { name: " + doc.name + ", age: " + doc.age + "}")
  })
})

TypeScript

firestore-simpleはTypeScriptで書かれているため、型定義も同梱されています。
逆にflowには対応していません。

まとめ

Firestoreをもっと手軽に扱えるfirestore-simpleを作りました。
簡単な用途ならfirestore-simpleを素のまま使っても便利だと思いますが、firestore-simpleを組み込むか、継承することでより重厚なFirestoreクライアントを自作したいというニーズにも応えられると思います。

自分はオリジナルのFirestoreのAPIを忘れてしまうぐらい便利に使いまくっているので、ぜひ使ってみてください。

USB接続したiOSのudidをCLIから取得する方法あれこれ

iOS開発をしていると、ちょっとしたことを自動化したいということがたまにある。
今回は、開発用のMacに接続しているiOS実機のudid(とついでにnameも)をrubyから扱いたくなったので、CLIから情報を取得する方法を調べたのでまとめてみた。

3種類の方法を紹介します。

  • xcrun instruments -s
  • mobiledevice
  • FastlaneCore::DeviceManager

xcrun instruments -s

やりたいことをググると一番多く見つかるのがこれ。
実際に実行してみると、たくさんのiOSシミュレータのudidがズラズラと列挙されて分かりにくいが、3行目あたりに接続しているiOS実機のnameとudidが表示されているはず。 少々めんどくさいが、正規表現を使えば抽出することが可能。

ちなみに、よく-sの後にdeviceを付ける紹介を見かけるが、自分が試した感じではdeviceの有無で結果は全く変わらなかった。diffコマンドで確認しても差分はなかった。

そもそもxcrun instrumentsを実行したときに-sというオプションは表示されていないので出自からして若干謎である。隠しコマンドなのだろうか?

mobiledevice

imkira/mobiledevice

iOSバイスの様々な情報を表示したり、アプリのインストールなどができるCLIツール。

仕組みとしては、iTunesが使っているプライベートフレームワークであるMobileDevice.frameworkを叩いてCLIツールとして機能を提供しているようだ。
残念ながら自分はこのあたりに詳しくないが、このMobileDeviceを外から呼び出すライブラリは他にも存在するみたいでこの界隈では有名らしい。
MobileDevice Library - The iPhone Wiki

homebrewからインストールできるが、残念ながらHigh Sierraではインストールできなかった。幸い、git cloneして自分でmakeすればHigh Sierraでも普通に使える。
makeに失敗する場合はxcode-select --installでcommand line developer toolsをインストールすればmakeできるようになるはずだ。

使い方は、mobiledevice list_devicesで接続しているiOSバイスのudidだけが列挙される。シンプル。
udid以外の情報も取得可能で、例えばnameならmobiledevice get_device_prop -u UDID DeviceNameで取れる。UDIDにはlist_devicesで表示されたudidを入れてほしい。
mobiledevice list_device_propsで他にも取得可能な要素の一覧が表示されるので、興味があれば色々試してみると面白いかもしれない。

FastlaneCore::DeviceManager

自分は最終的にここに行き着いた。みんな大好きFastlane。
connected_devicesというメソッドを使うことで接続されているiOS実機の情報を取得できる。 fastlane/device_manager.rb at 2.91.0 · fastlane/fastlane · GitHub

ドキュメントは多分存在しないが、使い方はこんな感じで簡単だ。

require 'fastlane_core/device_manager'
require 'pp'

devices = FastlaneCore::DeviceManager.connected_devices('iOS')

pp devices

# [#<FastlaneCore::DeviceManager::Device:0x007f883c2a3bb0
#  @ios_version="11.2.6",
#  @is_simulator=false,
#  @name="Kesin's iPhone",
#  @os_type="iOS",
#  @os_version="11.2.6",
#  @state="Booted",
#  @udid="*****">] 自分のiPhoneのudidが表示されているので伏せさせて頂く

裏側の仕組みとしては、やはりxcrun instrumens -sの結果を頑張って正規表現でパースしてインスタンスを作っている。
先人の努力に感謝しつつ、ありがたく使わせて頂きます。

まとめ

  • xcrun instruments -s
    • 実行するのは最も手軽。ただし、自力で正規表現を頑張る必要はある
  • mobiledevice
    • macOS 10.2までならhomebrewでインストールできるかも?High Sierraの人は自分でmakeする必要がある
    • 正規表現を頑張る必要はないのでインストールさえできれば簡単
  • FastlaneCore::DeviceManager
    • 自分と同じくrubyから使いたい人は最も手軽

自分の用途に合った方法をどうぞ。

SRE-SET Automation Night #2にお邪魔してきました

2018/03/06に開催されたSRE-SET Automation Night #2へブログ枠として参加させて頂きました。

SRE-SET Automation NightはCIやテスト、Seleniumなどを使った自動化についてが主なテーマの勉強会です。
第1回目のときにも参加させて頂いたので、今回は2回目の参加でした。

会場に着いたのが早めだったので、まだ皆さんが集まっていないうちにケータリングの写真を撮らせて頂きました。 定番のお寿司もいいですが、たまにはこういう違ったおしゃれメニューもいいですね!

f:id:Kesin:20180306191120j:plain

今回の発表の中で特に自分が興味持った2つを紹介します。

AWS Lambdaで作る GitHub bot @siroken3

GitHubでもイベントに反応するbotAWS Lambdaに構築するという発表でした。
おそらく公式ドキュメントだとこのあたりのことなのではないかと思います
https://developer.github.com/apps/getting-started-with-building-apps/

GitHub botができることの例として紹介されていたもの。

  • PRへのコメント
  • Issueの自動ラベリング
  • PRの内容チェック

Issueの自動ラベリングとか、自動クローズとかはOSSで最近見かけることがありますね。

botをLambda上に構築する仕組みは、GitHubのwebhookでAWS Lambdaを起動してLambdaに置いたスクリプトからGitHubAPIを叩いて実現しているようです。 つまりこういう流れ。

Github(webhook) -> Amazon SNS(Topicsを設定) -> Lambda -> Github API

帰宅してからちょっと調べたら、似たような構成でGitHub -> Lambda -> Slackの連携を構築している方がいました。
https://qiita.com/ooharabucyou/items/2a3dca643f6b7783d665

注意点としては、GitHubと連携するためのpersonal access tokenはAWS KMSを使って暗号化しておき、Lambdaから使うときに復号化する必要があるようです。

今回の発表では、サンプルとしてissueにコメントすると単純に同じコメントをbotが返すecho botの作り方を紹介していました。
リポジトリはこちらのようです https://github.com/siroken3/SreSetAutomationNightVol2

echo botはissueにコメントされたタイミングでwebhookを受け取る必要があるのですが、webhookのデフォルトの設定だとpushしたタイミングでしか発火してくれないとのことです。
発表では、コメントされたタイミングでもwebhookが発火するようにcurlでリクエストを投げて設定を変更されていました。 調べてみたところ、リポジトリのsettingsからwebhookを設定するUIにもそれっぽいものがあったので、おそらくこれでもいけるのではないかと思います。

2018/03/08 追記
webhookの設定ではなく、Integration & servicesからAmazon SNSの設定をされているとのことでした。たしかにこちらだとUIから発火するイベントの選択はできないようでした。
訂正いたします

自動回帰テストフローとGitHub Apps @Quramy

こちらもGitHubと連携するbot(Apps)を使って、回帰テストフローを自動化したという内容でした。 少しややこしいのですが、@siroken3さんの発表とは異なるGitHub Appsという仕組みを使っています。
GitHubと連携するbot(Apps)には@siroken3さんのpersonal access tokenを使う方法とは別のGitHub AppsとOAuth Appsという2つの仕組みがあり、 この2つの違いはこのようになっているとのことです。

GitHub Appsの場合は個人のアカウントに紐付かないためチーム開発においてはOAuthより便利だということです。
公式のドキュメントだと多分このあたり
https://developer.github.com/apps/getting-started-with-building-apps/

@Quramyさんのチームではフロントエンド用の自動回帰テスト環境として、修正前の環境でのスクリーンショットと、コードに修正を加えた後の環境でのスクリーンショットを比較して問題がないかの確認を行うツールを作成して運用しているとのことでした。
今回の発表では触れられていませんでしたが、2017年のNode学園祭で発表されていたREG-SUITというツールだと思われます。 https://speakerdeck.com/quramy/introduction-to-visual-regression-testing

スクリーンショットを比較するテストの場合、画像を比較して差分が出ていたとしてもデザイン変更を伴う正しい修正かもしれないため、機械的にOKかNGかを判断することはできず人間がチェックします。
そして差分が意図したものである場合は、修正後のスクリーンショットを次回テストの正として画像を保存しておく必要があります。

今回作成されたGitHub Appsは以下の作業を自動的にしてくれるようでした。

  • CIで実行してスクリーンショットの差分が存在した際に画像チェック用のページのurlをプルリクにコメントする
  • レビューがApproveされたらそれを検知してマージOKのステータスに変更する
  • 今回のスクリーンショットを次回テストの正とするために画像を保存

実装の詳しい説明までは残念ながらおそらく時間の都合上発表されていませんでしたが、AWS Lambda上で構築されているとのことでした。

感想

他の方の発表も非常におもしろかったのですが、最近自分がDangerというプルリクに対するアクションを簡単に書けるライブラリのプラグインを作っていたので、似たようなことができるGitHub連携ツールの発表が非常に興味深かったです。

プルリクへのコメントや、Issueの自動ラベリングはDangerでも実現できるのですが、DangerはCIサービス上で動くことをウリにしているので手軽に導入できる反面、pushされたタイミングでしか処理が行えないという不便さを感じていました。

botなりGitHub Appsは実装とAWS Lambdaにデプロイする手間はかかるはずですが、GitHub上のあらゆるイベントに対して処理を走らせることができるという点が強みだと思います。 特にApproveされたり、LGTMされたタイミングで何かのアクションをするというのは色々なことに使えそうですね。

便利そうだなーと感じる一方で、残念ながら今のところ自分はまだ便利に使えそうなアイディアが浮かんでいないです。ただGitHub Apps自体が発表されたのは去年のようですので、これからどんどん活用事例が増えていくのではないかなという予感がしています。

ちなみに、自分はまだ触っていないのですがGitHubbotを簡単に作るためのライブラリとしてhttps://probot.github.io/というのもあるようです。
今回の発表やprobotを見ていると、元々便利なGitHubをさらに便利にしていくという流れを何となく感じますね。