LCL Engineers' Blog

夜行バス比較なび(高速バス比較)・格安移動・バスとりっぷを運営する LCLの開発者ブログ

InstabugのWeb版(Beta)導入方法

InstabugでWebサイトのフィードバックを送れるように設定しました。 すでにiOSでは使い始めていて、iOSの設定については以下の記事で軽く触れています。 techblog.lclco.com

Web版は現時点(2017/11/13)でBeta版のためか、少し導入でつまづいた点もありましたので、手順を記録しておきます。

Web版(Beta)の導入方法

以下の2つの手順で動作します。

  1. Instabugの管理画面に貼ってある2つのscriptタグを</body>の直前に貼ります。 f:id:lcl-engineer:20171109083946p:plain ※Bower経由でも入れられるようです
    ※開発環境のみに導入する場合は、上記のタグを開発環境のみで出力するように記述する必要があります。

  2. scriptを貼ってWebサイトを表示させると右下にInstabugのボタンが出てきます。

    ※注意※

    Instabugを使うドメインで最初に表示させる必要がありそうです。 私は、先にローカル環境で動作確認をしてからテストサーバーへ反映しましたが、以下のようなエラーが出てテストサーバーでは動作しませんでした。

Failed to load https://api.instabug.com/api/sdk/v3/features?application_token=xxxxxxxxxxxxxxxxxxxxxxxx: The 'Access-Control-Allow-Origin' header has a value 'http://localhost:8080' that is not equal to the supplied origin. Origin 'https://xxxxxx.xxx.xxxx(ドメイン)' is

管理画面ではドメインを設定するところはなかったので、最初に表示させたところをホームとして認識するような仕様なのかもしれません。 この辺は正式版リリースまでに変わるかもしれませんね。

以下のようなアイコンが出てくれば成功です。 f:id:lcl-engineer:20171109084352p:plain

Chrome拡張機能を入れるとスクリーンショットを添付できますが、入れなくてもテキストのみで報告が可能です。 f:id:lcl-engineer:20171109084613p:plain

導入手順は、以上です。

少し使ってみましたが、LocalStorageやconsoleの値等、いろいろな情報が見えるのが良いです。 翻訳機能があるのもおもしろいです。 f:id:lcl-engineer:20171113155810p:plain

必要な機能は揃っていそうですが、気になる点は以下のとおりです。

  • 現時点(2017/11/13)では、スマホ(実機)で報告できない(ボタンは表示されますが、押しても反応しない)
  • 動画が撮れないので、特定のステップのときのみに起こるバグの報告はしにくい
  • 画面に書き込むのもマウス操作のみなので、きれいに書けない

なお、スマホについては、Chromeデベロッパーツールで以下の設定にしたときだけは動きました。 「Responsiveモード かつ、幅が800px以上の時」 f:id:lcl-engineer:20171114071437p:plain

スマホで報告できないのは、とても大きなマイナス点です。 正式リリースまでに改善されることを望みます。

ChatWorkのWebhookをRuby/Railsで受信する

先日、ChatworkがWebhookに対応されました。弊社では普段からChatworkを利用しており、早速検証しました。

ビジネスチャット「チャットワーク」がWebhookとOAuthに対応、オープンβ版の提供開始〜2018年春までにクラウドサービス17社18サービスとデータ連携開始予定〜 ~ ChatWorkニュースリリース

Webhook対応でできること

ChatworkがWebhookに対応したことで、「自分宛のメッセージ受信」「特定のルームでの発言」等をトリガーに、任意のURLを呼び出し処理を実行できるようになります。

今までも、APIで特定のルームのメッセージをポーリングすることで、同様のことは実現できました。ただし、APIリクエスト数の「5分あたり100回」という制限もあり、リアルタイム性に課題がありました。

Webhookの利用方法

公式ドキュメントに詳しく記載されていますが、簡単に紹介したいと思います。

Webhook - チャットワークAPIドキュメント

Webhookの受信側の準備

AWS EC2上にRailsアプリケーションを構築し、Webhookから呼び出すコントローラを準備します。

class Chatwork::WebhookController < ApplicationController    
  def test
    # 処理を実装
  end
end

Webhookの作成

Chatworkの設定画面で、Webhookメニューが新しく追加されており、そこからWebhookが作成できます。

  • Webhook名
    • 任意です
  • Webhook URL
    • httpsから始まる必要があります。
  • イベント
    • トリガーとなるイベントを設定します。
    • アカウントに対するイベントか、特定のルームでのイベントのどちらかが選択できます。
    • ルームイベントの場合は、自身が所属するルームIDしか入力できません。

f:id:lcl-engineer:20171104171523p:plain

Webhookの確認

該当のルームでメッセージを投稿すると、指定したWebhook URLが呼び出され、Chatworkから以下のデータがPOSTされます。 ポーリングとは異なり、即座に反応します。

 {"webhook_setting_id"=>"xxxxxxxxxxxx", 
 "webhook_event_type"=>"message_created", 
 "webhook_event_time"=>1509724361, 
 "webhook_event"=>{"message_id"=>"xxxxxxxxxxxx", 
 "room_id"=>xxxxxxxxxxxx, 
 "account_id"=>xxxxxxxxxxxx, 
 "body"=>"テストメッセージ", 
 "send_time"=>1509724361, 
 "update_time"=>0}, 
 "room_id"=>"xxxxxxxxxxxx", 
 "webhook"=>{"webhook_setting_id"=>"xxx", 
 "webhook_event_type"=>"message_created", 
 "webhook_event_time"=>1509724361, 
 "webhook_event"=>{"message_id"=>"xxxxxxxxxxxx", 
 "room_id"=>xxxxxxxxxxxx, 
 "account_id"=>xxxxxxxxxxxx, 
 "body"=>"テストメッセージ", 
 "send_time"=>1509724361, 
 "update_time"=>0}}}

これでWebhookの設定は完了です。あとはパラメータの内容を解析して、任意の処理を実装すれば、Chatworkでの投稿をトリガーとしたBotが作成できます。

リクエストの署名検証

実装上必須ではありませんが、Webhookリクエストの送信元がChatworkであることを、確認する方法が用意されています。

  1. トークンをBASE64デコードしたバイト列を秘密鍵として、HMAC-SHA256アルゴリズムによりリクエストボディのダイジェスト値を得ます
  2. ダイジェスト値をBASE64エンコードした文字列が、リクエストヘッダに付与されたsignature(X-ChatWorkWebhookSignatureヘッダの値)と一致することを確認します

上記の説明だけでは少し複雑ですが、Ruby/Railsでは、以下のように実装しました。

require 'openssl'
require 'base64'

def test
  if chatwork_signature.present? && chatwork_signature == Base64.strict_encode64(digest)
    render text: 'success', status: 200
  else
    render text: 'invalid', status: 403
  end
end

private

def secret_key
  token = '[トークン]'
  Base64.decode64(token)
end

def request_body
  request.body.read
end

def chatwork_signature
  request.headers[:HTTP_X_CHATWORKWEBHOOKSIGNATURE]
end

def digest
  OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), secret_key, request_body)
en

※ トークンは、Webhookの設定画面から確認できます。

f:id:lcl-engineer:20171104171542p:plain

まとめ

Webhookを利用すると、簡単にリアルタイム性の高いやりとりが実現できることが確認できました。

弊社では、既にChatworkとHubot連携してデプロイ等を行っていますが、Webhookを利用して更に自動化を進めていきたいと思います。

techblog.lclco.com

LCLではエンジニア・インターン募集中です。 サービス開発はもちろん、開発プロセスの自動化も積極的に推進しています。 少しでも興味がある方は、ご連絡お願い致します。

https://www.lclco.com/recruit/

Baltoからの移行先としてInstaBugとBugseeを比較してみました

先日、Baltoがサービスを終了するという悲しいお知らせがありました。

【重要】フィードバックツール「Balto」サービス終了について

日々のフィードバックや開発の中盤から終盤の確認フェーズにかけて、
大変お世話になり、Baltoのおかげで開発スピードが何倍にも短縮されていました。

Baltoを利用して素早くサービスを改善する
面倒だったフィードバックが簡単に!Baltoを使ったサービス改善。 LCL

しかし、サービス終了とSwift4の未対応を理由に
残念ではありますが、他のサービスへ移行をすることにしました。

今回はBaltoから移行するサービスとして以下の2つを比較してみました。

どちらもメインはユーザ向けのフィードバックとバグレポートのサービスですが
Baltoの代替目的として、以下の項目を比較することとします。

  • プラットフォーム、ライブラリ管理などの対応
  • フィードバックから送信可能な要素(画像、動画 ..etc)
  • フィードバックの送りやすさ(iOSのみ)
  • レポート一覧の閲覧性とアクション(コメント ..etc)

対応項目

項目 InstaBug Bugsee
プラットフォーム Web (beta), iOS, Android, Cordova, React Netive, Xamarin Web(Chrome), iOS, Android, Cordova, React Netive, Xamarin, Unity
ライブラリ管理 Carthage,CocoaPods,Manual CocoaPods,Manual

フィードバックの送りやすさ

Instabug

送信画面を出す方法が複数用意されています。
Baltoと同様に表示したい場合はfloatingButtonがおすすめです。

case none                // 表示は自作する
case shake               // デバイスを振る
case twoFingersSwipeLeft // 2本指で左へスワイプ
case rightEdgePan        // デバイスの右端から左へスワイプ
case floatingButton      // 右下にボタンを表示

Baltoと同様にボタンはいろいろなところへ移動させることができます。
ボタンを押すとメニューが表示され、項目ごとにレポートが送信できます。

全ての機能で画像と動画が添付可能であり、複数枚同時に送ることもできます。
フィードバックを行う際にBaltoは予めユーザログインが必要でしたが、
こちらはユーザ向けなので、メールアドレスを入力すれば誰でも送信できます。
一度入力されたメールアドレスは記憶されているので再入力の必要はありませんでした。

画像の編集では線、モザイク、拡大スコープを入力が可能です。

f:id:lcl-engineer:20171104025703p:plain

Bugsee

デフォルトでは通常のスクリーンショットと同様の
Home button + Lock button で送信画面を表示することができます。
また、以下のようにオプションを設定することで振って表示することも可能です。

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 
    let options: [String: Any] = [ 
        BugseeMaxRecordingTimeKey : 60,
        BugseeShakeToReportKey: true,
    ] 
    Bugsee.launch(token: "XXXXXXXXXXXXX", options: options) 
    return  true 
}

Bugseeの録画機能は全てのレポートで行われるようで、
送信者が手動で録画をするのではなく勝手に手前の行動が録画されています。
上記のBugseeMaxRecordingTimeKeyが録画時間となるようです。

f:id:lcl-engineer:20171104025729p:plain

こちらもメールアドレスを入力すれば誰でも送信できます。
画像の編集では線のみ入力が可能です。

レポート一覧の閲覧性とアクション

Instabug

前述の通り、Instabugは送信元が3パターン存在します。
管理画面はその中からチャットとその他2つで振り分けられます。
今回はチャット機能については関係がないので割愛します。

レポートは「Bugs」メニューから一覧をみることが可能です。
「問題を報告」、「改善を提案」はそれぞれbugfeedbackタグが付けられます。

一覧にタグが表示されないのが残念ではありますが、
上部のFilterから絞り込むことは可能で、条件を保存しておけます。

f:id:lcl-engineer:20171104025744p:plain

Baltoと同様にステータス管理もできます。
コメントはチャットのようなUIではないですが、かわりなく使えそうです。

f:id:lcl-engineer:20171104025801p:plain

とても高機能でデバイス情報やログなども取得して同じ画面に載っているので、
エンジニア以外の方だと情報が多すぎるのが見づらく感じるかもしれません。

Bugsee

こちらは比較的シンプルな作りとなっており、「ISSUES」に一覧が表示されます。
ちなみに「FEEDBACK」は別途にチャット機能があり、そのためのページです。

f:id:lcl-engineer:20171104025813p:plain

個別ページはInstaBugと似ていますが、こちらはログの詳細が
ボタンで切り替わるようになっているため、ごちゃごちゃした雰囲気はありません。

下部のシークバーで動画を再生している際のデバイス、アプリの状態を確認できます。
秒単位でViewのサイクルやメモリなどが見れるのはエンジニアとしては助かりますね。

f:id:lcl-engineer:20171104025826p:plain

終わりに

今回、InstaBugの方は実際に数日前から運用しているのですが
やはりごちゃごちゃしているという意見がでています。

Baltoが開発者向けのフィードバックツールであり、
必要最低限の機能でスムーズに使えた良さを改めて感じました。

ごちゃごちゃしているとの意見はありますが、
Baltoの移行先としてはやはり似た振る舞いのInstaBugが良さそうです。

開発側としてはBugseeの機能も捨てがたいので、
こちらも解析ツールとして導入したままにしようと思いました。

エンジニアを募集しています

LCLではエンジニア積極的に募集中です。
興味のある方はお気軽にご連絡よろしくお願いいたします。
インターンも募集しています。

https://www.lclco.com/recruit/

モバイルアプリエンジニアとして入社して3ヶ月が経ちました

2017年8月よりモバイルアプリエンジニアとして入社しました 山下です。
入社後1ヶ月ほどから下書きを書きつつ、あっという間に3ヶ月が経っていました。

新しい環境にも慣れてきたので、今回は転職の経緯や入社までの所感、
入社後3ヶ月経った上での会社の紹介を書いていきたいと思います。

なぜLCLを選んだか

(※ 自己紹介要素が多めなので飛ばしてもらって構いません)

前職は受託開発を行う小さな会社に所属していました。
そこでは営業と開発サイドの2極化となっており、
クライアント対応(メール)、クライアント向けのドキュメント作成、
簡単なデザイン、開発 、 運用 ..等、できることが増えると、
開発以外のことも次々と仕事が任されるという感じでした。

新卒で入社したのでとても良い経験となりましたが、
業務に余裕ができた辺りでクライアント相手の業務に気持ちが入らず、
その先のユーザに対してアプローチしたいという気持ちが強くなりました。

さらに、小規模でしたがWEBフロントエンド・バックエンド、
モバイルアプリ、デスクトップアプリなど幅広くやらせてもらう中で
モバイルアプリエンジニアとしてメインのスキルを伸ばしたいと思い始めました。

ちょうどその頃、エンジニア業界で転職活動の雰囲気が強く出始め
色々な企業の方とお話させていただく機会があり、そのうちの1社がLCLでした。

転職を意識するに辺り、転職先の基準を以下のように決めており、
LCLはぴったりマッチしていたことから、面談機会を設けていただきました。

  • 自社開発でカスタマー向けのサービスを運営している
  • 会社としてユーザファーストの意識がある
  • モバイルアプリ専業で仕事ができる(けれど裁量は限定的過ぎない)
  • 開発環境が整っている

入社までの所感

連絡や次回面談の決定などを即時に返して頂いたので好感を得ました。
面接においても堅苦しさはなく、自然と素に近い雰囲気で話ができていました。

正直、面接の機会をいただくまでLCLのことは知らなかったのですが
面接時にしっかりと現状のことや今後の計画などをお話していただき、
社風や開発スタイルの雰囲気が自分に合っていると感じました。

中途社員で構成されており、社員の年齢層は落ち着いているのも
まだ下の世代として多くを吸収したい自分にとっては魅力的でした。

オフィス内を案内していただいた際は、明るくて広々とした雰囲気のスペースや
トリトンタワー40階からの眺めをみて、ここで仕事したいと思える光景でした。

f:id:lcl-engineer:20161014230926j:plain f:id:lcl-engineer:20161014223720j:plain

内定をいただいた後日、用意していただくMacをiMacかMBPか決める際に
WWDCの発表を待ってもらえたのも嬉しかったです。

入社後3ヶ月が経って

冒頭でも書きましたが、11月で3ヶ月が経ちました。
業務としては専属でiOS/Androidの新規機能追加や改善を進めています。

業務自体の改善も好きなのでDangerを導入したり、細かい改善をやっています。
こうしたツールや開発プロセスなどを臨機応変に試せる環境であるため
新しいもの好きな自分としてはとてもやりがいを感じます。

ここからは社内全体と開発部に分けてざっくり紹介します。

社内全体

社内全体でコミュニケーションツールとしてChatworkを使っています。
そのため、オフィスは落ち着いた静かな雰囲気なので
集中して業務を進めることができます。

DevOpsがしっかりと構築されており、
社員数は多くないですが 個々の役割がしっかりと分けられていて、
エンジニアが雑務をやるということも一切ありません。

男女比も半々だったり、長期インターンシップも複数人いたりと
全体としてすごくバランスが取れていることに驚きました。

大江戸線-トリトンは人が多いので通勤が大変かと心配していましたが
勤務時間が10-19時で他の会社とずれているので問題ありませんでした。

フリードリンクなので毎日無限コーヒーしてます。

f:id:lcl-engineer:20161014230917j:plain

開発部

開発メンバーは現在7名で、フロントエンド・バックエンド・iOS/Androidアプリ、
インフラ構築・運用など それぞれがメインの領域を持ちつつ幅広く担当しています。

自分は現在、専属でモバイルアプリをやっていますが、
他の技術をやりたいとなればそちらを選択することができる環境なのもいいです。

前述の通り、社内の役割がしっかりと分けられているため、
仕様から実装までエンジニア任せに全てを決めるといったことはなく、
事業開発を促進する社員がしっかりと企画・検討・分析を行ってくれます。
発信は両者から行われ、ユーザにとって良いものにしていこうという意欲のもと、
意見交換や仕様決定が行われるので全員に裁量があります。

開発環境

全社員にハイスペックのMacとセカンドディスプレイが用意されています。
そのため机も広く、両手を広げられるくらいのスペースが確保されています。
椅子はバロンで長時間座っていても身体に負担が少なく感じます。

f:id:lcl-engineer:20161017235536j:plain

f:id:lcl-engineer:20171102111701p:plain

業務

入社してから電話やメールは一切なく、
業務中もエンジニアとして全く関係ないタスクをすることもありません。
リファクタなどの改善タスクもしっかりと工数を確保することができます。

残業

社内全体として定時で直ぐに退社する雰囲気があり、
開発部では30分もするとほとんど誰も残っていません。

リモートワーク

開発部はコアタイム・MTG以外でのリモートワークが許可されています。
午前は在宅で午後から出社するメンバーも居たり、
前日までにチャットで一言連絡をすれば自由に取ることが可能です。
子持ちの方は業務の合間を縫って子供の送り迎えをしています。

関連記事: リモートワークへの取り組み

また、オフィス内でも防音材で囲まれた集中スペースがあったり、
土禁のスペース、ハイデスクがあるため 色々なところで作業ができます

関連記事: 働き方を工夫して効率UP

チャット文化

チャット文化なので終日対面で話さないという日もあるため、
メンバーとの付き合いが難しそうに思いますが、そういう雰囲気は全く無いです。

ほどよく上下関係がないので、たまにランチをしたり、
チャット上でも気軽にコミュニケーションがとれます。
絵文字を使う文化もあるので、チャット文化特有の気まずさなどもないです。

入社1ヶ月間はチャットを追うのが大変に感じましたが、
慣れてくると既読が必要なものも判別できるようになり、
今ではまったく苦にならなくなりました。

ドキュメント文化

大きな作業内容や変更などは、Qiita:Teamにドキュメントを残す文化があります。
入社後は環境構築なども記事を見ながらをスムーズ行うことができました。

自分はドキュメントを積極的に書いていくタイプなのですが、
やはり自分以外の方の投稿もあるとモチベーションに繋がります。

参考書籍

参考書籍などは会社負担で購入することができるため技術書籍が多くあります。
WEB+DBや東洋経済、新聞を定期購読していたり
サービスの関係上、カフェスペースには旅行雑誌もたくさんあります。

最後に

エンジニアにとって、とても仕事がしやすい環境です。
毎日のやりがいと共に3ヶ月があっという間に感じました。

今はiOSの既存のコードのSwift4対応やリファクタ等を行っていたり、
AndroidもKotlin移行など まだまだやりたいことも沢山あるので
この環境を活かして、良いプロダクトをユーザへ届けたいと思います。

エンジニアを募集しています

LCLではエンジニア積極的に募集中です。
興味のある方はお気軽にご連絡よろしくお願いいたします。
インターンも募集しています。

https://www.lclco.com/recruit/

Bitriseで動作するFastlane/Gymでビルドが失敗する問題の解決方法

Xcode9 / Swift4の対応後、とあるブランチを配布しようとした際に PodsでインストールしたライブラリでProvisioning Profileが参照できずビルドが失敗する現象が発生しました。

以下はBitrise上のログです。(Provisioning Profile等の情報は伏せてあります)

[02:35:57]: Detected provisioning profile mapping: {:"XXX"=>"XXX"}

+--------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| Summary for gym 2.62.1 |
+--------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
| clean | true |
| xcargs | DEVELOPMENT_TEAM="XXX" PROVISIONING_PROFILE_SPECIFIER="XXX" CODE_SIGN_IDENTITY="iPhone Distribution" |
| scheme | XXX |
| export_method | ad-hoc |
| export_options.provisioningProfiles.XXX | XXX |
| workspace | ./XXX.xcworkspace |
| destination | generic/platform=iOS |
| output_name | XXX |
| build_path | /Users/vagrant/Library/Developer/Xcode/Archives/2017-10-30 |
| output_directory | . |
| silent | false |
| skip_package_ipa | false |
| buildlog_path | /var/folders/90/5stft2v13fb_m_2dsfafd0gn/T/fastlane_logs222222/gym |
| skip_profile_detection | false |
| xcode_path | /Applications/Xcode-beta.app |
+--------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+

[02:35:57]: $ set -o pipefail && xcodebuild -workspace ./XXX.xcworkspace -scheme XXX -destination 'generic/platform=iOS' -archivePath /Users/vagrant/Library/Developer/Xcode/Archives/2017-10-30/XXX\ 2017-10-30\ 02.35.57.xcarchive DEVELOPMENT_TEAM="XXX" PROVISIONING_PROFILE_SPECIFIER="XXX" CODE_SIGN_IDENTITY="iPhone Distribution" clean archive | tee /var/folders/90/5stft2v13fb_m_gv3c8x9asdfagn/T/fastlane_logs23ddd6627/gym/XXX.log | xcpretty

[02:36:02]: ▸ ❌ error: AWSCore does not support provisioning profiles. AWSCore does not support provisioning profiles, but provisioning profile XXX has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor. (in target 'AWSCore')

[02:36:02]: ▸ ❌ error: AWSSNS does not support provisioning profiles. AWSSNS does not support provisioning profiles, but provisioning profile XXX has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor. (in target 'AWSSNS')

...

[02:36:02]: ▸ ❌ error: Pods-XXX does not support provisioning profiles. Pods-XXX does not support provisioning profiles, but provisioning profile XXX has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor. (in target 'Pods-XXX')

[02:36:02]: ▸ Target Pods-XXX product Pods_XXX cannot link framework Foundation.framework

[02:36:02]: ▸ Target Pods-XXX product Pods_XXX cannot link framework Foundation.framework

[02:36:02]: ▸ unexpected duplicate creator 'PhaseScriptExecution [CP]\ Copy\ Pods\ Resources /Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikqfheukvzieglddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/IntermediateBuildFilesPath/XXX.build/Test-iphoneos/XXX.build/Script-C604611C209DA5CF0468AAE9.sh' for node '/Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikqfheukvzieglddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/InstallationBuildProductsLocation/Applications/XXX.app' with prior creator 'MkDir /Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikqfheukvzieglddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/InstallationBuildProductsLocation/Applications/XXX.app' (in target 'XXX')

[02:36:02]: ▸ ignoring duplicated output file: '/Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikqfheukvzieglddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/InstallationBuildProductsLocation/Applications/XXX.app' (in target 'XXX')

[02:36:02]: ▸ ** ARCHIVE FAILED **

❌ error: AWSCore does not support provisioning profiles. AWSCore does not support provisioning profiles, but provisioning profile XXX has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor. (in target 'AWSCore')

❌ error: AWSSNS does not support provisioning profiles. AWSSNS does not support provisioning profiles, but provisioning profile XXX has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor. (in target 'AWSSNS')

❌ error: DZNWebViewController does not support provisioning profiles. DZNWebViewController does not support provisioning profiles, but provisioning profile XXX has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor. (in target 'DZNWebViewController')

❌ error: OALayoutAnchor does not support provisioning profiles. OALayoutAnchor does not support provisioning profiles, but provisioning profile XXX has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor. (in target 'OALayoutAnchor')

❌ error: SimulatorRemoteNotifications does not support provisioning profiles. SimulatorRemoteNotifications does not support provisioning profiles, but provisioning profile XXX has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor. (in target 'SimulatorRemoteNotifications')

❌ error: TTRangeSlider does not support provisioning profiles. TTRangeSlider does not support provisioning profiles, but provisioning profile XXX has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor. (in target 'TTRangeSlider')

❌ error: Pods-XXX does not support provisioning profiles. Pods-XXX does not support provisioning profiles, but provisioning profile XXX has been manually specified. Set the provisioning profile value to "Automatic" in the build settings editor. (in target 'Pods-XXX')

Target Pods-XXX product Pods_XXX cannot link framework Foundation.framework

Target Pods-XXX product Pods_XXX cannot link framework Foundation.framework

unexpected duplicate creator 'PhaseScriptExecution [CP]\ Copy\ Pods\ Resources /Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikqfhefdsfgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/IntermediateBuildFilesPath/XXX.build/Test-iphoneos/XXX.build/Script-C604611C20EEE0468AAE9.sh' for node '/Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikadfeglddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/InstallationBuildProductsLocation/Applications/XXX.app' with prior creator 'MkDir /Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikasdfavzieglddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/InstallationBuildProductsLocation/Applications/XXX.app' (in target 'XXX')

ignoring duplicated output file: '/Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikqfheukvzieglddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/InstallationBuildProductsLocation/Applications/XXX.app' (in target 'XXX')

** ARCHIVE FAILED **

[02:36:02]: Exit status: 65

+---------------+------------------------------+
| Build environment |
+---------------+------------------------------+
| xcode_path | /Applications/Xcode-beta.app |
| gym_version | 2.62.1 |
| export_method | ad-hoc |
| sdk | iPhoneOS11.1.sdk |
+---------------+------------------------------+

[02:36:02]: ▸ Build system information

[02:36:02]: ▸ warning: unexpected duplicate creator 'PhaseScriptExecution [CP]\ Copy\ Pods\ Resources /Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikqasdfvzieglddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/IntermediateBuildFilesPath/XXX.build/Test-iphoneos/XXX.build/Script-C604611C2EEEE0468AAE9.sh' for node '/Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikqfheasdfvzieglddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/InstallationBuildProductsLocation/Applications/XXX.app' with prior creator 'MkDir /Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikqfheudsafddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/InstallationBuildProductsLocation/Applications/XXX.app' (in target 'XXX')

[02:36:02]: ▸ Build system information

[02:36:02]: ▸ warning: ignoring duplicated output file: '/Users/vagrant/Library/Developer/Xcode/DerivedData/XXX-dwpikqfhadfdaieglddgbhcojmrj/Build/Intermediates.noindex/ArchiveIntermediates/XXX-dev/InstallationBuildProductsLocation/Applications/XXX.app' (in target 'XXX')

[02:36:02]:

[02:36:02]: ⬆️ Check out the few lines of raw `xcodebuild` output above for potential hints on how to solve this error

解決方法

Fastfileのgymメソッドに以下のオプションをを追加します。 PROVISIONING_PROFILE_SPECIFIERで使用するProvisioning Profileを指定します。

    gym(
        clean: true,
        xcargs: xcodebuild_args,
        scheme: options[:scheme],
        verbose: true,
        + export_options: {
        + provisioningProfiles: {
        +  BUNDLE_ID => PROVISIONING_PROFILE_NAME
        + }
        + },
        + xcargs: "PROVISIONING_PROFILE_SPECIFIER='#{PROVISIONING_PROFILE_NAME}'"
    )

又、CODE_SIGN_IDENTITYがProvisioning Profileに紐付いていないものが設定されているとエラーとなります。

         buildSettings = {
                ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
                CODE_SIGN_ENTITLEMENTS = "XXX/XXX.entitlements";
                CODE_SIGN_IDENTITY = "iPhone Distribution";
                - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer: 紐付いていない個人証明書 (XXX)";
                + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
                DEVELOPMENT_TEAM = XXX;
                FRAMEWORK_SEARCH_PATHS = (
                    "$(inherited)",

原因

gymのexport_optionsについては、xcode9からmatchを使っていない場合に明記する必要があるとのことです。

https://docs.fastlane.tools/codesigning/xcode-project/#xcode-9-and-up

またProvisining Profileを設定しないとxcode側で自動でAutomatically manage signingになる(?)とのことで PROVISIONING_PROFILE_SPECIFIERでの設定が必要となります。

CODE_SIGN_IDENTITY については、恐らくどこかのタイミングでAutomatically manage signingを設定してしまい 対象のビルド環境のCode Signing Identityの設定が証明書とProvisioning Profileの紐付かないものになっていました。

開発環境の証明書にはmatchを使っているのですが、リリースの証明書周りは以前から作られている証明書を使っているため 参照する証明書の設定がズレていたのが原因です。

ハマったポイント

  • Bitrise上のログ(特にProvisioning Profile等の文字列)は成功時のものと同じ
  • Xcode9が原因かと思われたが同環境のdevelopブランチは成功した
    • 動作するdevelopブランチのxcodeprojをチェックアウトしてきても発生した
  • 上のことからCacheが影響していた?(Podsフォルダが残っていて成功していた可能性があったのでPodsフォルダを都度削除するようにした)
  • Xcode 9で同様のIssueがたくさん立てられていたが、この問題は実はXcode9ではなくXcode8からある現象だった
  • Apple側がAutomatically manage signingを推奨している(設定しないと自動で"自動"に切り替わる)
  • Bitrise上でのみでしか実行を試せなかったので手間と時間が余計にかかった

終わりに

言語やXcodeの変わり目ではこういった変更と合併した設定ミスでハマりやすいですね。 ローカルで動作する環境を用意しなかったのも余計に時間がかかった原因でした。 自動化をすると疎かになりがちですが、個々の設定内容も理解しなければいけないと思います。

Swift 4とiPhone Xの対応をしました

iPhone Xの発売まであと1週間ほどとなり、
弊社のiOSアプリ「高速バス比較」でも、Swift4とiPhone Xの対応を行いました。
今回は、Swift3からSwift4への作業内容をまとめていきたいと思います。

準備

Xcode上でビルドするためにコードやライブラリの更新をします。

Xcodeの設定

ローカルのSwiftのバージョンをSwift3からSwift4へ変更する必要があります。 Swiftenv等を使用していなければ、Xcodeの設定から変更可能です。

SwiftのバージョンをSwift 4に変更

Build Settings の Swift Language Version を Swift 4 に設定します。

Update to recommended settingsを実行

新しく追加されたコンパイル時の警告などが有効化されます。

ライブラリをアップデート

Swift4に対応していないライブラリがある場合は、削除して代わりのものを見つけましょう。
今回はCarthageで管理しているライブラリに未対応のライブラリがあったので以下の作業を行いました。

  • Cartfileから削除
  • Build Phasesの Link Binary With Libraries からリファレンスを削除
  • Frameworksからリファレンスを削除

マイグレーション

ビルドできる環境が整ったので次はコードをSwift4へ対応します。

Auto Conversion

バージョンの違いで変更があるコードの一部は自動変換が行えます。

言語設定をSwift4へ変更した際にダイアログが出ると思いますが、 そこで操作をキャンセルした場合は Edit > Convert > To Current Swift Syntax でも実行可能です。

弊社の環境では以下の変更が行われました。

  • NSObjectを継承していたクラスやdynamicで暗黙的に付与されていた@objcが明示的に付与されるようになった
    • Realmで扱っていたプロパティ
    • #selector() で参照しているメソッド 等
  • NSMuteAttributedStringkCTForegroundColorAttributeNameで定義していた箇所をNSAttributedStringKeyで定義するようになった
NSAttributedStringKeyに変更

この変換は既存のコードで書き方が整っていなかった関係か、一部正常に動作しなかったので手動で対応しました。

NSAttributed~~ as StringkCTLanguageAttributeNameなどのString型で設定していた箇所をNSAttributedStringKey型を使用するように変更します。

let textAttributes: [NSAttributedStringKey: Any] = [
    NSAttributedStringKey.foregroundColor: UIColor.red,
    NSAttributedStringKey.font: UIFont.systemFont(ofSize: 12),
    NSAttributedStringKey.paragraphStyle: textStyle,
    kCTLanguageAttributeName as NSAttributedStringKey: "ja"
]
Objective-Cの initilize()

一部でMethod Swizzlingが使われており、Objective-Cのinitilize()メソッドの記述が残っていました。 (Method Swizzlingとは既存のメソッドを自前のメソッドに差し替える手法)

簡易的な対応として、Swizzleの処理をメソッドで括り AppDelegateのdidFinishLaunchingWithOptionsで実行することにしました。

@UIApplicationMain

class AppDelegate: UIResponder, UIApplicationDelegate, AdjustDelegate, TAGContainerOpenerNotifier {
...
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        UIFont.swizzleSystemFont() 
        ...
}
  
extension UIFont {
    func swizzleSystemFont() {
          ...
    }
}

レイアウト崩れ、iPhone Xへの対応

UITableViewのドリルダウンでアニメーションが崩れる

iOS11からUIScrollViewへadjustedContentInsetというプロパティが追加され、自動でInsetを付けてくれるようになりました。 しかし、自前でContentInsetsを設定している場合は このプロパティ追加の影響でレイアウト崩れを起こしてしまうようです。

弊社のアプリでも所々で該当する箇所があったので、下記を記述して無効化しました。

if #available(iOS 11.0, *) {
    tableView.contentInsetAdjustmentBehavior = .never
}
UITableViewのHeader、Footerの設定

上記のadjustedContentInsetと直接の関係はないと思いますが、 NavigationBarに隣接して設置していたUITableViewにおいて、 余白として設けていたHeaderViewが表示されずに、上に詰まってしまう崩れが起きていました。
原因はHeaderViewの設置にwillDisplayHeaderViewメソッドを使用していたことで 上記のメソッドではなく、viewForHeaderInSectionを使うようにし、対応しました。

// Swift 3
func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
    let headerView = view as! UITableViewHeaderFooterView
    headerView.backgroundView?.backgroundColor = UIColor.gray
    headerView.layer.borderColor = UIColor.red.cgColor
    headerView.layer.borderWidth = borderWidth
}

// Swift 4
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    let headerView = UIView()
    headerView.frame.size = CGSize(width: self.view.frame.width, height: tableHeaderViewHeight)
    headerView.backgroundColor = UIColor.glay
    headerView.layer.borderColor = UIColor.red.cgColor
    headerView.layer.borderWidth = borderWidth
    return headerView
}
NavigationBarのサイズが小さくなる問題に対応

ナビゲーションバーに表示しているViewの表示内容が見切れていました。 対象のUIViewに下記を記述することによって、以前と同じようにサイズが一杯に広がります。

override var intrinsicContentSize: CGSize {
   return UILayoutFittingExpandedSize
}
Use Safe Area Layout Guidesを有効にする

iPhone Xへの対応で、新しく追加されたSafeAreaを有効にします。 StoryboardのInterface Builderからインスペクタを選択し、該当の項目にチェックを付けると有効になります。

コードのみで実装している場合はUIEdgeInsetsのsafeAreaInsetsからマージンを取得できるので この値を利用します。

最後に

Swift3の時と比べると簡単にバージョンアップすることができました。 Codableなどの新しいプロトコルも試しつつ、コード内容にもSwift4の書き方を取り入れていきたいです。

iPhone Xへの対応については、まだまだ調整を加えたり、サイズを活かしたレイアウトにする必要がありそうです。
特にSafeAreaはiPhone Xのみの概念なので、レイアウトを変更する際にしっかり確認する必要があります。OSや端末など環境による条件分岐が増え続けるのはツラいですね。

JOB管理をJenkinsからKuroko2へ移行しました

弊社では最近、JOB管理をJenkinsからKuroko2へ移行しました。

Kuroko2についての概要や導入方法は、以下の記事などが詳しいため割愛させて頂き、弊社での具体的な導入内容について紹介したいと思います。

導入の背景

元々、数百ほどあるバッチ処理をJenkinsで運用していました。Jenkinsから、各JOBサーバにsshして、JOBを起動するという単純な構成で運用していたため、下記の課題がありました。

  • 一つのホスト上で同時に起動されるJOB数を厳密に制御できず、CPU/メモリのリソースを逼迫してしまう
  • 夜間だけホスト増やすなど、ピークに応じた実行環境の増減に対応しづらい

Kuroko2は、ジョブ管理システムとして必要な「スケジュール、エラー通知、JOBの依存関係制御」等の機能に加え、Wokerを増やすことで動的にJOB起動数を制御できるため、上記課題を解決可能と考えました。

構成

Kuroko2の基本的なアーキテクチャは、以下の図のようになっています。(GitHubから抜粋)

f:id:lcl-engineer:20171029232828p:plain

弊社では、kuroko2-consoleとkuroko2用のDBは同インスタンス上に構築し、kuroko2-workerは各サービス毎のJOBサーバに導入しています。つまり、一つのconsoleで、複数サービスのJOBを横断して管理しています。

現時点のkuroko2(v0.4.2)では、権限制御機能がないため、メンバー毎に管理可能なJOBを制御したい場合は、kuroko2-consoleを分割する必要があります。

Queueの設計

弊社では、サービス・JOB種類別にQueueを分けています。例えば日中に定期的に稼働するJOBはQueue-A,夜間に集中的に稼働するJOBにはQueue-Bとしています。

あるJOBがCPUやメモリを極端に使用する場合、そのJOBだけでサーバリソースを圧迫してしまうことになるため、同じQueueに所属するJOBは、できるだけ似通った処理特性のOJOBにしています。

エラー通知

Kuroko2では、メールとチャット等(Webhook)に各種通知可能です。弊社では、メールは利用せず専用のチャットルームへ通知し、通知があればそこで都度チャットで確認をしています。若干、オオカミ少年化している通知もあるため、通知の精度向上に取り込んでいます。

タグのルール

Kuroko2では、JOBに複数のタグを設定でき、タグによる絞込ができます。当初は複数のタグを細かく付与していましたが、タグが増えすぎると絞込時に探しづらい状態になりました。結局、以下の2種類付与しておけば問題なく運用できています。

  • Queueの名称
  • JOBの分類

実行ログ

Kuroko2では、JOB内で標準出力したログは、管理コンソール上で確認ができます。ただし、リアルタイムではなくJOBが完了後に初めて確認できます。(ログが多いと途中までしか表示されません)

AWS CloudWatch Logsを利用することで、管理コンソール上でリアルタイムにログが確認できるようになりますが、ログの出力量が多いと、JOBの実行時間が遅くなってしまったため、弊社では採用を見送りました。

バックアップ

管理コンソール上で定義したJOBの情報等は、kuroko2のDBに格納されているため、mysqldumpを利用してバックアップを取得するようにしています。

ログの削除

現時点のKuroko2では、ログは自動的に削除されないため、弊社では定期的にログの削除を実施しています。kuroko2は以下のテーブルで構成されます。

mysql> show tables;
+---------------------------------+
| Tables_in_workerdb              |
+---------------------------------+
| kuroko2_admin_assignments       |
| kuroko2_ar_internal_metadata    |
| kuroko2_executions              |
| kuroko2_job_definition_tags     |
| kuroko2_job_definitions         |
| kuroko2_job_instances           |
| kuroko2_job_schedules           |
| kuroko2_job_suspend_schedules   |
| kuroko2_logs                    |
| kuroko2_memory_consumption_logs |
| kuroko2_memory_expectancies     |
| kuroko2_process_signals         |
| kuroko2_schema_migrations       |
| kuroko2_stars                   |
| kuroko2_tags                    |
| kuroko2_ticks                   |
| kuroko2_tokens                  |
| kuroko2_users                   |
| kuroko2_workers                 |
+---------------------------------+

kuroko2_job_instances,kuroko2_logsが、特にデータが貯まりやすいテーブルです。これらのテーブルの過去N日以前のデータを定期的に削除するようにしています。(オフィシャルな情報ではないので、自己責任で)

監視

Kuroko2が正常に動作しているかは、下記の観点で監視しています。

  • 管理コンソールURLを外形監視
  • Workflow,Schedulerのプロセス監視

また、JOBが滞りなく実行されているかは、以下のAPIを利用してWaiting JOB数を監視しています。

/v1/stats/waiting_execution

Waitingが増えた場合には、JOBスケジュールの見直しやWokeの追加(HOSTの追加)を検討します。

その他

最後に少しハマったところや、TIPS的なものを紹介します。

PostgreSQLでの稼働

Kuroko2の動作環境として、System requirementsには「MySQL >= 5.6」と記載がありますが、PostgreSQLの方が運用に慣れているため、最初はPostgreSQLで構築しておりました。ある程度は正しく動いていたのですが、細かいところでSQLエラーが発生していたため、結局はMySQLでの運用に切り替えました。

強制キャンセル

Workerの強制終了等の理由により、Kuroko2上でJOBが完了しないまま残りつづける場合がありました。

その場合は、「Job Instance Details」画面で、Backslashを押下することで、Force Cancelボタンが有効になり、Force CancelでJOBを強制終了させることができます。

f:id:lcl-engineer:20171029233213p:plain

参考) https://github.com/cookpad/kuroko2/issues/23

HOST追加時にWokerが認識されない

Kuroko2では、Host名+Queue名をキーとしてWokerを管理しているため、HOSTを追加した場合に、既に同名のQueueがある場合は、Host名を変更する必要がありました。

拡張機能

いくつかChromeの拡張機能を作っていますので、少し紹介します。

デフォルト入力

新しくJOBを作製する際、デフォルトで入力する項目はある程度決まっています。デフォルトボタン(下記の図のオレンジボタン)を用意して、デフォルト値を設定するようにしています。

f:id:lcl-engineer:20171029233228p:plain

スケジュール削除ミス防止

スケジュールのDeleteボタンを押すと、確認なく削除されるため、間違えて押下した際に、以前のスケジュールがわからなくなって困るケースがありました。ミス防止のため、拡張機能でダイアログを出すようにしています。

f:id:lcl-engineer:20171029233236p:plain

まとめ

JenkinsからKuroko2への移行することで、JOBサーバを柔軟に増減することが可能となりました。今後は、ECSやスポットフリートを利用して、よりJOBの効率化・コスト削減を実施していきたいと思います。

LCLではエンジニア・インターンを積極的に募集中です。興味のある方は採用ホームページからお気軽に、ご連絡よろしくお願いいたします。