RareJob Tech Blog

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

PlantUMLでAWSのサービスを使う

こんにちは、前回書いた記事からはや2ヶ月ちょっと。 GOTO( Ghost of Tsushima Omoshiroi-zo )はプレイ時間60時間を超え、無事トロコンも完了し、対馬には平和が訪れました。 なので今日はまたPlantUMLの話をします。

PlantUMLでAWSアイコンを出したい

結論から言うと公式がマクロセットを用意してくれているのでこれをincludeすることで実現できます。 CacooやDraw.ioも使うんですが、図自体の可読性や差分の見やすさだとやはりUMLのほうが私は見やすので好きです。 個人のアイデアをまとめたり、説明のために整理するのによく使っています。

@startuml
title AWS

!define AWSPuml https://raw.githubusercontent.com/awslabs/aws-icons-for-plantuml/master/dist
!includeurl AWSPuml/AWSCommon.puml
!includeurl AWSPuml/EndUserComputing/all.puml
!includeurl AWSPuml/Storage/SimpleStorageServiceS3.puml

actor "Person" as personAlias
WorkDocs(desktopAlias, "Label", "Technology", "Optional Description")
SimpleStorageServiceS3(storageAlias, "Label", "Technology", "Optional Description")

personAlias --> desktopAlias
desktopAlias --> storageAlias

@enduml

これで

f:id:jumbos5:20200925111606p:plain

これが出ます。 マクロ一覧もまとめてくれているので参考にしながら書くのが良いと思います。

マクロ一覧

注意点

!includeurl AWSPuml/EndUserComputing/all.puml

仕組み的に「必要なアセットをincludeしてdefineされているマクロに引数を私で描画する」と言う仕組みのため、 includeが必要になりますが、all.pumlは各フォルダ・コンポーネントのまとまりごとに用意されており、まとめてincludeできるのですが、重いので必要なコンポーネントだけincludeするようにしましょう。

どう言う仕組みで実現されているか1 「define」

PlantUMLにはマクロがありこれを定義して実現されています。

!define SampleMacro(v1, v2) [v1] -> [v2]
SampleMacro("value1", "value2")

こんな感じで定義できるので、これを使っています。 あまり自分で書いているときはそこまで再利用性考えて作らないのですが、アイコンつけたりする場合は真似したいですね。

どう言う仕組みで実現されているか2 「sprite」

PlantUMLで画像を読み込む際はグレースケールにした値を定義する必要があります。

sprite $ARVR [64x64/16z] {
xTG3WiH054NHzutP_th7RHkfsmnEdE1HZMZsIn0_DGDuuVsZJwnMVJ-57txuuKrsP4Tv1mjl3Nw43qZlo147VO9xPueyu8j1l3jm7V0GtPFWe8_UKzpL3rzc
TO4l0gZEzufCsDd-rnhoN2zKtKLoWk-bkHq--vabr0TypEy_WiwEmc9K7FATAd_fVDwOZygdU_uEF_pmLgUMA_wChkV1SavCc4LdXNVe2m
}

この機能もPlantUML自体が持っているので自前の画像を定義してAWSのようにマクロを追加することも可能です。

$ java -jar plantuml.jar -encodesprite 16z xxx.png

構造が複雑化してくると、線がカオスになってしまい図が複雑化してくるところを除いてとても気に入っています。 それは良い週末を

Google Chatのbotからスプレッドシートデータを読み出してみる

こんにちは、はじめまして
ITソリューションチームの平川です。

ITソリューションチームはいわゆる「情報システムチーム」でして、
日々PCについての問い合わせや依頼を受けています。
PCの作業依頼はワークフローで届くので、PC管理番号と利用者が正しいかなどの確認を行なって承認→作業となっていきます。

そこで、PC管理番号と利用者の確認はPC台帳なるものを都度確認するわけですが、この作業がめんどくさいんです。

本当に。

いちいち台帳開かなくても、誰かに聞けば教えてくれるようにならないかなと思いGoogle Chatを使って試してみました。

ではやっていきます。

GCPプロジェクトの作成

まずはGoogle Chat botを扱うプロジェクトを作成していきます。
下記リンクに沿って進めていきます。

Publishing bots  |  Google Chat API  |  Google Developers

[Enable API]ボタンをポチッと押して新しいプロジェクトを作成します。
ここでは「ChatbotTEST」と名前をつけて進めます。 

f:id:Peperonciiiiiiino:20200903182145p:plain

JSONファイルはダウンロードしてもしなくても大丈夫です。

f:id:Peperonciiiiiiino:20200903182337p:plain

これでプロジェクトが作成されました。

ボットスクリプトの作成

続いてボットスクリプトを作成していきます。
これもテンプレートが公開されているので利用していきます。

https://script.google.com/create?template=hangoutsChat

 クリックするとスクリプトが作成されますので、名前を変更してデプロイIDを取得します。
[公開] -> [マニフェストからの配置] -> [Get ID] 

f:id:Peperonciiiiiiino:20200903183618p:plain

f:id:Peperonciiiiiiino:20200903183658p:plain

表示された[Deployment ID]を控えておきます。

APIの構成とデプロイ

作成したプロジェクトからボットAPIの構成とデプロイを行なっていきます。

Google Cloud Platform

[ナビゲーションメニュー] -> [APIとサービス] -> [ダッシュボード] 

f:id:Peperonciiiiiiino:20200903184653p:plain

右ペイン最下部に「Hangouts Chat API」があるのでクリックします。

f:id:Peperonciiiiiiino:20200903184748p:plain

ここからボットAPIの設定を行なっていきます。
左メニュー -> [設定]
▼アプリケーション情報
下記情報を入力します
 [ボット名]
 [アバターのURL]
 [説明]
▼機能
 [ボットはダイレクト メッセージ内で機能します]にチェック
▼接続設定
 [App Script Project]を選択し、先の手順で入手している [Deployment ID]を入力します
▼権限
 必要に応じて設定してください

f:id:Peperonciiiiiiino:20200904112851p:plain

続いてサービスアカウントにプロジェクトのオーナ権限を付与していきます。
[ナビゲーションメニュー] -> [IAMと管理] -> [サービスアカウント]

f:id:Peperonciiiiiiino:20200903190302p:plain

表示されたサービスアカウントをコピーします。

f:id:Peperonciiiiiiino:20200903190647p:plain

[ナビゲーションメニュー] -> [IAMと管理] -> [IAM] ->[追加]
サービスアカウント追加して[オーナー]権限を付与します。

f:id:Peperonciiiiiiino:20200903203205p:plain

f:id:Peperonciiiiiiino:20200903203243p:plain

これで、GoogleChatのボットに登録されます。

動作確認その1

作ったボットの動作を確認します。
GoogleChatを開き、登録したbotを追加します。

https://chat.google.com/

[BOT]の横にある+から登録したbotを追加します

f:id:Peperonciiiiiiino:20200904113533p:plain

f:id:Peperonciiiiiiino:20200904113552p:plain

botに話しかけると入力したテキストを返してくれます。

f:id:Peperonciiiiiiino:20200904113836p:plain

テンプレートのスクリプトが動く事を確認できました。

ここまでうまく動いたので、スクリプトを修正してスプレッドシートからデータを読み出しを行なっていきたいと思います。

スプレッドシートの準備

スプレッドシートでPC台帳っぽいものを準備します。
今回はこんな感じの台帳を使います。

f:id:Peperonciiiiiiino:20200904114052p:plain

それからスプレッドシートのURLからIDを取得します。

スクリプトの修正

今回はPC番号[A列]を入力したら、貸出者[B列]が返ってくるようにスクリプトを修正していきます。

こんな感じに修正します。

f:id:Peperonciiiiiiino:20200904115010p:plain

動作確認その2

では、動作を試していきます。
(最初に文字を入力するとスプレッドシートへのアクセスリクエストが行われますので、スプレッドシートのアクセス権があるアカウントで[許可]を行なってください)

f:id:Peperonciiiiiiino:20200904115853p:plain
存在するPC番号の貸与者が正常に表示されました。

これでわざわざ台帳を見なくてもPC番号から利用者(貸与者)がわかるようになりました!


まとめ

GCPやGAS(Google Apps Script)はほとんど使ったことありませんでしたが、テンプレートやらナレッジがたくさんあったので割と簡単に作ることができました。

今回作ったのは単純なルールベース型のボットですが、もう少し作り込めばいくつかの業務改善が行えそうだなと思いました。

 

では、また。

 

 

TypeScript Generics編

APP/UXチームに所属しております、フロントエンドエンジニアの田原です。

皆さん、この夏如何お過ごしでしょうか?
私は夏がとても好きなので本来であれば夏っぽいこと(e.g.海や花火大会や BBQ など)をしたいのですが、 今年はコロナ禍ということもあって外出を極力自粛しているのでこれといって夏らしいことができておりませんでした。

先日、チームのメンバーに「この夏、なにか夏っぽいことしましたか?」と聞いてみたところ 「スイカ食べましたよ ♪」と言われ
なるほど!!どこか外出することやイベントに参加するだけが夏ではないッ...!!
これは盲点だった...っと、とても感銘を受けたと同時にとてもほっこりした気分になりました。

こんな感じで業務以外の話も結構することも多く、チームのメンバーとは仲良しです。
(片思いじゃないって信じてる)
チームだけでなく弊社の開発メンバーは朗らかでとても親切な方々ばかりなので日々とても楽しくお仕事ができております。

さて、少し弊社の雰囲気を感じて頂けたかな?というところで今回の本題に入らせて頂きたいと思います。

弊社でも例の如くTypeScriptを利用して開発を行なっているのですが、今回はその中でも便利な機能であるジェネリクスについて簡単にご紹介できればと思います。

※尚、TypesScriptの基本的な型の説明については一部割愛させて頂いております。

目次

Generics(ジェネリクス)とは?

簡単にいうと型にも引数を設定できるようにして柔軟な取り回しができるようにし、関数やClassの再利用性をあげよう!!というものになります。

これだけでは「お、、、おう、、」
そうなんだなという感想しか出ないと思いますので 以下、簡単な実例で説明します。

引数の内容をそのまま返すだけの簡単な関数があったとします。

const echo = (value) => {
  return value;
};

通常、この関数の引数(value)に型を指定してあげる場合(value:string)(value:number)のように記載すると思います。
引数の型が限定的で明確化している場合についてはその型を指定するで良いのですが複数の型が入る可能性がある時には(value:string|number)というように Union 型で 示したりする場合もあると思います。

const echo = (value: string | number): string | number => {
  return value;
};

Union型のままでも良いのですがこのままではtypeガードやアサーションを設定してあげてvalueの値を関数内の早期に指定してあげないと、 editor上でのインテリセンスがいい感じに効かないという弊害があります。
(つまり、その型の持つJavaScriptの固有のメソッド候補がでない。Union型の場合は共有したものだけがインテリセンスされる。)

const echo = (value: string | number) => {
  // ここではstring型とnumber型に共有したメソッドのみインテリセンスが効く
  // そのため、toUpperCase()などは候補に出ない
  if (typeof value === "string") {
    // ここで初めてstring型だけのインテリセンスが効く
    return value;
  }
};

また、引数の数が限られている場合はこれでも良いのですが以下の様に引数にオブジェクトを渡す場合など、一つ一つ書いていくと冗長になっていくこともあると思います。

const echo = ({ studentId, tutorId, islesson }:{
  studentId: number
  tutorId: number
  isLesson: boolean
}) => {
  return studentId
}

このような時にGenericsを使うとこの様な記述になります。

const echo = <T>(value: T) => {
  return value; // valueの型はTで渡された型になる
};

// 関数呼ぶ時に型を指定してあげる
echo<string>("hello");
echo<number>(2);

Tが型の引数となり関数の引数の型として利用できるようになります。 ※慣例的に大文字のアルファベット(T,S,U)などが使われますがなんでも良いみたいです。

このように柔軟に型を指定できインテリセンスも適切な候補が表示されるようになります。

更にコンパイラが型を推測できる場合には型引数を省略して書けるのでオブジェクトを渡す場合でも冗長にならず柔軟な型指定が可能になります。

const echo = <T>(value: T) => {
  return value; 
};

echo({ studentId: 1, tutorId: 2, isLesson: true });

以下、画像のようにインテリセンス効き、型も適切に判定されていることがわかります。

f:id:ssp0727-lnc:20200828103844p:plain
editor TypeScript

ここまでで簡単に Generics の機能と使い方について理解頂けたかと思います。 以下でもう少し使い方を深掘っていきたいと思います。

■ extends で制約を与える

引数で渡した型に制約を与えたい場合についてはextendsを使います。

const echo = <T extends { lessonSlotNum: number }>(value: T) => {
  // valueに必ずlessonSlotNumが存在する型のメンバとして推測されるためエラーにもならず、インテリセンスも効く。
  console.info(value.lessonSlotNum); // OK
};

echo({ lessonSlotNum: 2 }); // OK
echo({ teacherName: "Mike" }); // Error

このように書くことで関数が呼ばれるときに必ず{ lessonSlotNum: number }の型があるかを確認し、存在しない引数を渡した上で関数を呼ぼうとするとエラーになります。

extendsは以下のようにinterfaceとの組み合わせで指定可能な型を制限させて使うことが多いです。

interface LessonInfoType {
  tutorId: number;
  studentId: number;
}

const echo = <T extends LessonInfoType>(value: T) => {
  return value;
};

echo({ tutorId: 1, studentId: 2 }) // OK
echo({ hobby: "soccer" }); // Error
echo({ tutorId: 1, studentId: 2, hobby: 'soccer' }) // OK

■ keyofを使ってオブジェクトキーの型を動的に型としてプロパディのアクセスに利用する

interface LessonInfoType {
  tutorId: number;
  studentId: number;
}

const echo = <T extends LessonInfoType, U extends keyof T>(
  value: T, // { tutorId: number, studentId: number} の型になる
  key: U // "tutorId" | "studentId" のUnion型になる
) => {
  console.info(value[key])
};

echo({tutorId: 1, studentId: 2}, 'tutorId') // OK
echo({tutorId: 1, studentId: 2}, 'counselingId') // Error

このように型を設定しくことで関数の第一引数に渡したオブジェクトに存在するkey名でのみ、第二引数に渡せるようになります。

■ Vuexで使うとこんな感じで型付けられるよのサンプル

これらを踏まえてVuexでのstoreに簡単に型をつけてみました。
Vuexから提供されている型を拡張する形で型付けを行なっております。
※尚、stateが関数形式なのはNuxt内のVuexであるからです。

何故、これを試してみたかというとVuex4系から入ってくる予定のcreateStoreを使えばVuexの型を CompositionAPIの方法で作成したVueComponentでもTypeSafeに実装ができるようになるみたいなので試してみた次第です。この辺りについてはまたの機会に書きたいと思います。

import { GetterTree, ActionTree, MutationTree, ActionContext } from "vuex";
import { MutationTypes } from "./counterTypes/mutation-types";
import { ActionTypes } from "./counterTypes/action-types";

export const state = () => ({
  counter: 0 as number,
});
export type RootState = ReturnType<typeof state>;

export const getters: GetterTree<RootState, RootState> & Getters = {
  getcounter: (state) => state.counter,
  doubledCounter: (state) => state.counter * 2,
};
export type Getters = {
  getcounter(state: RootState): number;
  doubledCounter(state: RootState): number;
};

export const mutations: MutationTree<RootState> & Mutations = {
  [MutationTypes.SET_COUNTER]: (state, payload: number) => {
    state.counter += payload;
  },
};
export type Mutations<S = RootState> = {
  [MutationTypes.SET_COUNTER](state: S, payload: number): void;
};

export const actions: ActionTree<RootState, RootState> & Actions = {
  [ActionTypes.GET_COUTNER]({ commit }, payload: number) {
    commit(MutationTypes.SET_COUNTER, payload);
  },
};

type AugmentedActionContext = {
  commit<K extends keyof Mutations>(
    key: K,
    payload: Parameters<Mutations[K]>[1]
  ): ReturnType<Mutations[K]>;
} & Omit<ActionContext<RootState, RootState>, "commit">;

export type Actions = {
  [ActionTypes.GET_COUTNER](
    { commit }: AugmentedActionContext,
    payload: number
  ): void;
};

■ まとめ

今更、Genericsの話をするのも情報として鮮度が低いのですが、雰囲気でTypeScriptを使ってしまっていた部分もあったので備忘録も兼ねた内容を書かせて頂きました。 Genericsを使ってでできることについてもまだまだ沢山ありますので、近々第二弾を紹介できればいいなと思います。 まだまだTypeScriptそのものについても全てを理解しきれてはおりませんが面白い機能やテクニカルな方法が沢山あるので適切に使っていけるように更に理解を深めていきたいです。

■ 参考

TypeScript を教えてくれた人

iOS 版レアジョブアプリが Sign in with Apple に対応した話

APP/UX チームの玉置です。
今回は iOS 版レアジョブ アプリの Sign in with Apple 対応が完了しましたので、それについてエモい話をします。
はい、 Sign in with Apple にやっと対応することができました。非常に大変でした。

【目次】

Sign in with Apple とは何か

まずモバイルアプリ界隈では非常に有名ですが、その他の業界ではSign in with Appleの認知度はそこまで普及していないので簡単に説明してみます。
Sign in with AppleApple が開発した Apple アカウント( AppleID )を使ったソーシャルログインの認証システムです。
例えば、 TwitterFacebook のソーシャルログインといった感じのものですね。

レアジョブ が Sign in with Apple に対応した背景と対応後のデザイン

去年、 Apple が突如 iOS13 と一緒に Sign in with Apple を公開して「皆の者、これに対応しなさい!」という号令が発表されました。
全てのアプリがこれに対応しなければならないわけではなく、 FacebookTwitter といった Apple 以外のソーシャルログインを実装しているアプリが対象にされました。

6月末までにソーシャルログインを使う場合は Sign in with Apple 対応が必要になりました。レアジョブは Facebook ログインをサポートしています。
レアジョブ における Facebook ログインユーザーの比率が無視できないぐらい高いので、この Sign in with Apple に対応しなければならなくなりました。

もともとのレアジョブの登録・ログイン画面はこちらになります。

登録画面 ログイン画面
f:id:qed805:20200820135650p:plain
過去の登録画面
f:id:qed805:20200820135712p:plain
ログイン画面

それが Sign in with Apple の対応で iOS 13 以上と iOS 12 以下で見え方が変わるように対応しました。

iOS 13 登録 iOS 13 ログイン画面
f:id:qed805:20200820135924p:plain
リリース後のiOS13での登録画面
f:id:qed805:20200820135956p:plain
リリース後のiOS13でのログイン画面
iOS 12 登録 iOS 12 ログイン画面
f:id:qed805:20200820140048p:plain
リリース後のiOS12以下での登録画面
f:id:qed805:20200820140119p:plain
リリース後のiOS12以下でのログイン画面

スケジュールと見積もりについて

プロダクトオーナーに見積もりを提示してスケジュールを引くために Sign in with Apple の仕組みと Apple サーバーから得られる情報を確認する必要がありました。
Sign in with Apple のサンプルプロジェクトを知ったのは実際に開発が始まってからでしたので、見積もり段階ではどんな情報がどんな風に得られるかわからなかったのですね。

そのため自分のマイマシンで Sign in with Apple を実装する必要がありました。実装すること自体はそんな手間ではありませんでした。

これから Sign in with Apple を実装するのでしたら、先に Apple が公開しているサンプルプロジェクトを確認しておいた方がいいと思います。

Appleのドキュメントとサンプルプロジェクトファイルのダウンロードページ

https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple

見積もりは正確な情報が分かりませんでしたので、 Sign in with Apple 自体のシステムの調査として

  1. 実装方法
  2. 得られるデータの確認
  3. 仕様の調査

の3つの項目を2回に分けて見積もりしました。

アプリ側の実装自体は5人日ぐらいのボリューム感でしたが、 Web 版や API との連携も含めた見積もりで2週間に膨れ上がってしまいました。

開発前に不明であった内容について

  1. ログイン後の挙動 (email, fullnameの扱い)
  2. メールを非公開の内容
  3. ログアウトの方法

1. ログイン後の挙動 (email, fullnameの扱い)

Sign in with Apple で有名なのがログイン後に取得できるデータについてです。
Apple サーバーから取得できるユーザー情報の一部に email と fullname の情報がありますが、この情報は最初の1回目しか取得できず2回目以降は空が渡ってくることです。

再度 email と fullname を取得する手段は残されていて、端末の設定アプリから

Apple ID > パスワードとセキュリティ > Apple IDを使用中のApp > 特定のアプリ

で表示される「Apple IDの使用を停止する」をタップして停止させるとアプリでの Sign in with Apple の使用が止まって 再度 email と fullname を取得できるようになります。

2. メールを非公開の内容

次に Apple 認証の際に Apple ID に紐づいているメールアドレスを公開するかどうかの選択できる部分の仕様について紹介します。

Apple 認証を用いるとアプリで使用する際にメールアドレスを公開するかどうかを選択できます。

  • 「メールを共有」
  • 「メールを非公開」

の2つのオプションがあります。

「メールを共有」して Sign in with Apple するとアプリ側でユーザーのメールアドレスが公開されます。
「メールを非公開」して Sign in with Apple すると Apple 側で生成されたプライベート用のメールアドレスが発行されます。

abcde12345@privaterelay.appleid.com
alk32943dw@privaterelay.appleid.com
bl31dkr03e@privaterelay.appleid.com

このような形式です。
アットマーク前は10文字の固定長のランダム文字列というのが特徴的です。

3. ログアウトの方法

こちらは Sign in with Apple のログアウト方法です。
1でApple IDの使用を停止する」をタップして使用を停止させるとそれがログアウトになる仕様のようでした。

フロント側で必要な対応

  • iOS 12 以下と iOS 13 以降での画面の切り分け
  • ユーザー情報のキーチェーンへの保存
  • アプリ起動時に Apple 認証しているかどうかのチェック

iOS 12 以下と iOS 13 以降での画面の切り分け

Sign in with AppleiOS 13 以降でした使えないので端末バージョンで処理を分けるしかありません。
iOS に ContainerView というコンポーネントがありますので ViewControler を分離させました。

    override func viewDidLoad() {
        super.viewDidLoad()
        setupSocialLoginView()
    }

    private func setupSocialLoginView() {
        if #available(iOS 13, *) {
            /// iOS 13以降で走る処理
            let socialNewVC = UIStoryboard.load("SocialRegister", idetifier: "SocialNewRegisterViewController") as! SocialNewRegisterViewController
            addSocialContainer(vc: socialNewVC)
        } else {
            /// iOS 12以下で走る処理
            let socialOldVC = UIStoryboard.load("SocialRegister", idetifier: "SocialOldRegisterViewController") as! SocialOldRegisterViewController
            addSocialContainer(vc: socialOldVC)
        }
    }

このように分岐させました。

Container 側のデザインは storyboard でデザインしています。

f:id:qed805:20200721140543p:plain
storyboard

ユーザー情報のキーチェーンへの保存

Apple 認証後に得られるユーザー情報をAPIに送信するのですが、確実に送信させないと行けませんので保存する領域を決めないといけませんでした。

ちょうどレアジョブアプリは iPhone のキーチェーン( Keychain )を使っていますので、得られたユーザー情報の一部をキーチェーンの領域に保存することにしました。

アプリ起動時に Apple 認証しているかどうかのチェック

Sign in with Apple でログイン中のレアジョブユーザーが再度レアジョブ アプリに戻ってきたときにログイン中かどうかを確認する必要がありました。

そこで AppDelegate で user をキーにしてログイン中かどうかを判別することにしました。
ログイン中であればその状態を維持して、ログアウトされていればアプリをログアウトする必要があります。

ソースコード上では次のような対応を行いました。

    private func checkAppleLoginStatus() {
        /// Apple認証後のuserの情報をキーチェーンから取得する
        guard let userIdentifier = UserDataStore.currentUserIdentifier else { return }
        /// Apple ログイン中かどうかキーチェーンの情報を使って確認する
        if #available(iOS 13.0, *) {
            let appleIDProvider = ASAuthorizationAppleIDProvider()
            appleIDProvider.getCredentialState(forUserID: userIdentifier) { (credentialState, error) in
                switch credentialState {
                case .authorized:
                    /// 端末側のApple ログイン中
                    break
                case .revoked, .notFound:
                    /// 端末側のAppleログインセッション切れ
                    // ログアウトする
                default:
                    break
                }
            }
        }
    }

Web フロントへの対応で共有すべき情報

  • Web でのログインに必要な ServiceID と Key
  • Web からのメール送信ができるようにするための対応
  • AppId と ServiceId との関係

Web でのログインに必要な ServiceID と Key

Web 版 Sign in with AppleApple 認証で必要なものがありました。 ServiceID と Key です。どちらも Apple Developer サイトで設定・取得できるものです。
ServiceID とは iOS アプリの AppID と似たものです。

iOS アプリでは BundleID を発行してユニークなIDを発行しますが、Web に該当するものが ServiceID になります。 ServiceID は AppID と同じものは指定できずさらにユニークなものにしなければなりません。 ServiceID はApplD.xxxx みたいに使っている AppID の後にユニークな文字列を追加しました。

さらに Key も設定してファイルをダウンロードして Web エンジニアに共有しました。

Web からのメール送信ができるようにするための対応

レアジョブのサービスに Web サーバーからメール送信する機能があります。例えば、レッスン予約時やレッスン開始前に送信される確認メールだったりします。

実は上で話しました「メールを非公開」にして Apple 認証すると Apple がプライベートなメールアドレスを生成します。
このメールアドレスに何も設定せずにメールを送信しようとするとエラーが発生して「メールが送れません」みたいなエラーメールが返ってきて正常にメールを送信できません。

そこでサービス元から Apple のプライベートメールアドレスにメールを送信するために Apple Developer サイトで設定を行う必要があります。
普段のアプリ開発のリリースでは触らない Certificates ページの「 More 」の項目に進みます。

f:id:qed805:20200721163337p:plain
Moreページ

「 Configure 」ボタンをクリックすると「Configure Sign in with Apple for Email Communication」ページにアクセスできます。
このページで Apple のプライベートメールアドレスに送信する「送信元のメールアドレス」を登録する必要があります。

上限があるのかまでは確認していませんが考えられるメール送信元のメールアドレスを全て登録しておきます。
この設定で Apple のプライベートメールアドレスにメールを送信できるようになります。

詳細について知りたい場合は Apple 公式のドキュメントがありますのでこちらを確認すれば把握できると思います。

Configure Private Email Relay Service

https://help.apple.com/developer-account/?lang=en#/devf822fb8fc

AppId と ServiceId との関係

最後に AppID と ServiceID との関係についてです。

さらっと説明すると
AppID は iOS アプリ側で Apple 認証に必要な ID で、 Web で Apple 認証するのに必要なものが ServiceID です。

AppID と ServiceID は密接に紐づいています。
AppID でApple 認証した AppleID アカウントであれば、それに紐づいている ServiceID を使えば同じ AppleID アカウントで Web 版の Apple 認証に成功することができます。
AppID と ServiceID が紐づいていないもので Apple 認証をしようとすると失敗してしまいます。

そこだけ注意すれば大丈夫でした。

最後に

とても長くなりましたが無事にアプリと Web とで Sign in with Apple に対応できましたので、是非とも使ってみてください。
リリースしてからしばらく経っていることもあって利用者の方も徐々に増え始めています。とても嬉しいことですね!
実際にログインするときは指紋認証か顔認証でログインできるようになりますので文字入力をする必要がないのはとても快適です。

Cypressを試してe2eテストやってみた

お久しぶりです。プラットフォームチームの南です
TSUTAYAで昔のドラマをレンタルするのにハマってます
(最近はラスト・フレンズを観て哀しい気持ちになりました)

今回は自分が個人的に触ってみたe2eテストツールについて紹介したいと思います

なんでe2e

当然ですがリリースをする際には動作確認を各環境で行います
その時、毎度同じ操作を画面で行うのが煩わしく感じることがあると思います(自分はそんなに嫌いじゃないです)

  • リリースを行う際、前後の動作確認は重要
  • 手順書としてドキュメントを用意しているが、人がやることなので抜けやミスがある可能性は否定できない
  • 確認事項は毎回変わるわけではない
    • ある程度手順が決まっている
  • 開発環境・ステージング環境・本番環境とそれぞれでやるのが割とコストとして重い

ざっとこんな感じの問題点があったので、e2eを導入して効率よく自動化できないかと考えました

Cypressとは?

ツールは

  • 王道的なseleniumとかよりもちょっと新しくていい感じのやつ触りたい
  • ciや他サービスとの連携がとりやすい
  • 導入・学習コストは下げたい

このあたりをポイントとして選定し、Cypressにいきつきました

www.cypress.io

Cypressの特徴として

  • javascriptで動作する
  • 軽量さくさく
  • dashboardが使いやすい
  • 導入・実装が容易

このあたりがあるようです
とりあえず使ってみます

導入方法

yarnやnpmでインストールできるので簡単でした

$ yarn add -D cypress

インストールが完了すると

$ yarn run cypress open

上記コマンドで動作確認できます
Cypressにはチュートリアル的なテストファイルがデフォルトで用意されているので、動かしながら使い方を学べます

e2eテストを書いてみる

実際にe2eテストを書いてみます
以前、vueを自習してたときに作ったtodo listに対してテストコードを実装してみます f:id:nannannanan:20200806154750p:plain
我ながらとてもシンプルです

機能としてはこんな感じです

まずcypress.jsonにテスト対象アプリケーションのbaseURlを指定します

{
  "baseUrl": "http://localhost:8080/"
}

テストファイルはintegrationディレクトリの中に作ります
今回はtodo_spec.jsというファイルを作ります
そして完成したコードがこちらです

describe('todo list e2e', function () {
    beforeEach(() => {  // functionの前に必ず実行される
        // baseUrlのrootへ
        cy.visit('/');

        // todoを1つ追加
        cy.get('.todo')
            .type('test todo').should('have.value', 'test todo')
        cy.get('.addBtn').click()
        cy.get('.todo').clear()

        // 2つ目のtodoを追加
        cy.get('.todo')
            .type('test todo2').should('have.value', 'test todo2')
        cy.get('.important').check()
        cy.get('.addBtn').click()
        cy.get('.todo').clear()
    });
    it('should create new todo list', function () {
        // .todoList -> 1行目の.rowTodo => Titleには"test todo"が入ってるよね?ImportantはFalseだよね?のテスト
        cy.get('.todoList')
            .find('.rowTodo:first').should('have.class', 'rowTodo')
            .find('.tIndex').should('have.text', '1')
            .siblings('.tTitle').should('have.text', 'test todo')
            .siblings('.tImportant').should('have.text', 'False')

        // 全部でtodoは2行あるよね?のテスト
        cy.get('.rowTodo').should('have.length', '2')

        // .todoList -> 2行目の.rowTodo => Titleには"test todo2"が入ってるよね?ImportantはTrueだよね?のテスト
        cy.get('.todoList')
            .find('.rowTodo:last').should('have.class', 'rowTodo')
            .find('.tIndex').should('have.text', '2')
            .siblings('.tTitle').should('have.text', 'test todo2')
            .siblings('.tImportant').should('have.text', 'True')
    });
    it('should delete complete todo', function () {
        // 2行目のtodoをCompleteするぞ!の動作
        cy.get('.todoList')
            .find('.rowTodo:last')
            .find('.compTask').check()

        // 全部でtodoは1行あるよね?のテスト
        cy.get('.rowTodo').should('have.length', '1')

        // 残ったtodoの中身は正しいよね?のテスト
        cy.get('.todoList')
            .should('have.length', '1')
            .find('.rowTodo:first').should('have.class', 'rowTodo')
            .find('.tIndex').should('have.text', '1')
            .siblings('.tTitle').should('have.text', 'test todo')
            .siblings('.tImportant').should('have.text', 'False')
    });
});

checkやclickのアクションを使ってDOMを操作し、shouldでassertを行います
siblingsとかのselectorをみるとjQueryを思い出しました
作ったテストを動かすには

$ yarn cypress run --browser chrome --spec=./cypress/integration/todo_spec.js

これで動きます
コマンドをみればわかるように、動作させるブラウザを選ぶことができます
コマンドラインで実行もできますが、 cypress openのコマンドなら動かすテストファイルをGUIで選択することもできます f:id:nannannanan:20200806164213p:plain

Cypress dashboardの紹介

最後にCypressの大きな魅力であるdashboardについて紹介します
CypressにログインしProject IDとRecord Keyを取得します
取得したProject IDをcypress.jsonに追記します

{
  "baseUrl": "http://localhost:8080/",
  "projectId": "xxxxxxxx"
}

設定はこれだけ
あとは実行コマンドにRecord Keyを追記して

yarn cypress run --browser chrome --spec=./cypress/integration/todo_spec.js --record --key 999999999

これでテストの結果がdashboardと連携できます
f:id:nannannanan:20200806170428p:plain
dashboardではテストの結果やciを含む外部サービスとの連携設定、テスト動画・スクショの確認などが行えます
正直、dashboardが使いたいからCypressを使ってる感じもあります

さいごに

今回、試験的にCypressを使ってみましたが

  • 動作はめちゃ速い
  • コードの書き方は覚えやすい
  • dashboardとの連携は楽
  • dashboardきれい

などの利点がありました
ただし

  • DOMを変更したときの対応
  • テストデータの管理
  • dashboardを4人以上で使うには課金が必要

などなど考慮しなければいけない課題もあるにはあると思いました
Cypressだけでなく、Postman -> Newman などツールは世の中に山程あるので、うまく組み合わせられると効率化できそうですね

以上!

マルチステージビルドで環境毎のLaravelイメージを作る

はじめに

こんにちは、サービス開発チームの加々美です。初投稿になります。

先日直属の上司の方に「最近2日に1回はカレー食べてます」と謎の共有をしたところ、「疲れてるんじゃないですか?」と言われました。 私はただカレーにハマってるだけだと思っていますが、もし週に何回もカレーを食べてしまうという方は注意が必要かもしれません。

早速ですが本題に入っていきたいと思います。

Dockerイメージを作る際に開発環境と本番環境でインストールしたいパッケージや設定が異なることがあると思います。

一方で全く異なるかというとそんなことはなく、基本的には同じで開発時のみデバッグツール(PHPだとXdebug)を利用できるようにしたい、といったケースが多いです。

今回はLaravelの環境毎に利用できるDockerイメージ作成時の知見についてまとめた内容になります。

マルチステージビルドに関する記事は多いですが、意外とLaravelでの記事がなかったためこの機会にまとめました。

今回のフォルダ構成

今回のサンプルは以下のフォルダ構成で作成しています。 srcにはLaravelアプリケーションのファイルを置いてます。

├── docker
│   ├── app
│   │   ├── Dockerfile
│   │   └── docker-php-ext-xdebug.ini
│   └── web
│       ├── Dockerfile
│       └── default.conf
├── docker-compose.yml
└── src

マルチステージビルドとは

Docker 17.05から利用できるようになった機能で、マルチステージビルドが登場する前は次に説明するDockerfileを複数作成するなど共通の定義を利用できませんでした。 マルチステージビルドを利用することでベースとなるイメージを作成し、それを再利用することによりイメージ毎に不要なファイルが含まれることを防ぐことができイメージサイズの削減が可能になります。

matsuand.github.io

マルチステージビルドを使用しない場合

マルチビルドステージを利用しない場合はDockerfileを複数作成する方法がありますが、Dockerfile毎にそれぞれで定義する必要があるため共通で使用する箇所が多い場合でも全て記載しないといけません。

Dockerfileを別々で定義する場合、変更する度にそれぞれに対して変更を加える必要があり管理が煩雑になります。

開発用のイメージのビルド

何も考えずにdocker/app/Dockerfileを作成すると以下になります。

FROM composer:latest AS composer

FROM php:7.4-fpm-alpine

COPY --from=composer /usr/bin/composer /usr/bin/composer

RUN set -eux \
  && apk update \
  && apk --no-cache add \
    git \
    oniguruma-dev \
    libzip-dev \
    zip \
  && docker-php-ext-configure zip \
  && docker-php-ext-install pdo_mysql mbstring zip \
  && composer config -g repos.packagist composer https://packagist.jp \
  && composer global require hirak/prestissimo \
  && apk --no-cache add \
    autoconf \
    gcc \
    g++ \
    make \
    openssh-client \
  && pecl install xdebug \
  && docker-php-ext-enable xdebug

COPY ./src /var/www/

上記の中で開発環境用に追加するパッケージはこちらです。

  && apk --no-cache add \
    autoconf \
    gcc \
    g++ \
    make \
    openssh-client \
  && pecl install xdebug \
  && docker-php-ext-enable xdebug

RUN内で2度apk --no-cache addを行なっていますが、本番用のイメージと比較しやすいようにあえてこのように記述しています。

このDockerfileをビルドしてみます。

docker build . -f docker/app/Dockerfile -t sample1

docker imagesで作成されたイメージを確認してみます。

REPOSITORY     TAG       IMAGE ID          CREATED            SIZE
sample1        latest    536e1516fffb      2 hours ago        378MB

本番用のイメージのビルド

ビルドするファイルの内容は以下になります。

FROM composer:latest AS composer

FROM php:7.4-fpm-alpine AS builder

COPY --from=composer /usr/bin/composer /usr/bin/composer

RUN set -eux \
  && apk update \
  && apk --no-cache add \
    git \
    oniguruma-dev \
    libzip-dev \
    zip \
  && docker-php-ext-configure zip \
  && docker-php-ext-install pdo_mysql mbstring zip \
  && composer config -g repos.packagist composer https://packagist.jp \
  && composer global require hirak/prestissimo

COPY ./src /var/www/

こちらもdocker imagesで作成されたイメージを確認してみます。

REPOSITORY  TAG       IMAGE ID          CREATED           SIZE
sample2     latest    dbb97bb45402      9 seconds ago     136MB
sample1     latest    536e1516fffb      2 hours ago       378MB

Xdebug関連のインストールがないだけでイメージサイズが半分以下になっているのがわかりますね。

マルチステージビルドを使う

docker/app/Dockerfileは以下になります。

FROM composer:latest AS composer

FROM php:7.4-fpm-alpine AS builder

COPY --from=composer /usr/bin/composer /usr/bin/composer

RUN set -eux \
  && apk update \
  && apk --no-cache add \
    git \
    oniguruma-dev \
    libzip-dev \
    zip \
  && docker-php-ext-configure zip \
  && docker-php-ext-install pdo_mysql mbstring zip \
  && composer config -g repos.packagist composer https://packagist.jp \
  && composer global require hirak/prestissimo


FROM builder AS dev

RUN set -eux \
  && apk --no-cache add \
    autoconf \
    gcc \
    g++ \
    make \
    openssh-client \
  && pecl install xdebug \
  && docker-php-ext-enable xdebug

COPY ./src /var/www/

FROM builder AS prod

COPY ./src /var/www/

簡単に解説していきたいと思います。

FROM php:7.4-fpm-alpine AS builderの箇所で共通で利用できるステージを定義しています。

devprodのステージを定義する際にbuilderステージを利用します。

builderの利用方法はFROM builder AS devのように利用します。

devではデバッグ用のパッケージをインストールしていますが、prodではbuilderをそのまま利用しています。

ビルドする際は--target devのようにビルドしたいステージをオプションで指定します。

それぞれビルドします。

docker build . docker/php/Dockerfile -t sample3 --target dev

docker build . docker/php/Dockerfile -t sample4 --target prod

実行結果を確認すると以下のようになりました。

REPOSITORY    TAG       IMAGE ID          CREATED              SIZE
sample4      latest     39bd6a456c9d      29 seconds ago       136MB
sample3      latest     e1bee9d50280      About a minute ago   378MB

マルチステージビルドを利用しないパターンと同じイメージがビルドできました。

弊社では主にAWSを使用しており、ECSへの移行も考慮しているので各環境毎にビルドしたイメージをECRにpushする場合などは--targetでステージを指定することで対象のDockerイメージをビルドすれば良さそうです。

また、ステージを指定してビルドした場合は、対象ステージのみビルドされます。

つまり、prodを指定してビルドする際はdevはビルドされません。

docker-composeでステージを指定して利用する

最後にdocker-composeで指定する方法について解説したいと思います。

今回使用したdocker-compose.ymlはこちらでです。

version: '3.7'
services:
  app:
    build:
      context: .
      dockerfile: ./docker/app/Dockerfile
      target: dev
    volumes:
      - ./src:/var/www
      - ./docker/app/docker-php-ext-xdebug.ini:/usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
  web:
    build:
      context: .
      dockerfile: ./docker/web/Dockerfile
    ports:
      - "8000:80"
    volumes:
      - ./src:/var/www
      - ./docker/web/default.conf:/etc/nginx/conf.d/default.conf

ステージを指定している箇所はtarget: devの箇所です。

意外と今までやったことがなかったですが、簡単に指定できました。

あとはdocker-compose upを実行し、立ち上がったらdocker-compose exec app shでコンテナ内に入り、composer create-project laravel/laravel .を実行すればブラウザで確認できるようになります。

おまけ

サンプルのDockerfile内でcomposerに関して以下のように利用していました。

FROM composer:latest AS composer

COPY --from=composer /usr/bin/composer /usr/bin/composer

これは外部イメージをステージとして利用する方法です。

https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/#use-an-external-image-as-a-stage

検証できていないですが、この方法を使用すればbuilderで定義したイメージをCIでECRなどにあげておき、開発者はそこからステージとして開発環境に利用できるのかも?と思ったりしました。

まとめ

今回はマルチステージビルドを利用したLaravelのDockerイメージ作成について説明しました。

もっと良い使い方あるという方はコメント頂けると嬉しいです。

思ったより記事を書くのに時間がかかったので今夜はカレーを食べたいと思います。(実は5日連続。。)

UMLをコードベースで管理する

さて、ジャンボです。本当はGOTO( Ghost of Tsushima Omoshiroi-zo )という話を書きたいところですが大人しくテックブログを始めようと思います。今日はPlantUMLの話をします。

PlantUMLの導入

みんな好きですよね、設定しましょう

1. 必要なツールを落とす

brew install graphviz
brew install plantuml
brew install maven

2. vscode拡張機能を入れる

  • PlantUML

これだけも利用を始めることができます。xx.pu というファイルを作り、Preview Current Diagram を実行すればUMLを表示することが可能です。ただxx.pu のファイルを画像に変換するときにこの拡張機能はリモートのサーバを見ているのでレンダリングがかなり遅いです。そのためローカルのマシン上にPlantUMLのサーバを立てます。

3. ローカルでPlantUMLサーバを動かす

git clone https://github.com/plantuml/plantuml-server.git
cd plantuml-server
mvn jetty:run -Djetty.port=10001

jettyのデフォルトのportが他と被りやすいので明示的にport指定します。 このプロセスが動いていないとレンダリングには失敗します、起動は少し時間がかかります

4. 拡張機能の設定変更

ここから f:id:jumbos5:20200721093242p:plain

ここを変更 f:id:jumbos5:20200721093222p:plain

この辺を設定する。 RenderとServerの値を変更して下さい。

5. markdownでプレビューする

.md内でプレビューしたい場合は上記の設定を終了後に#4の設定で対象とする拡張子指定で md を指定できるので、ここから設定しておくと設定されたローカルサーバを参照してマークダウン内でもプレビューされます。

f:id:jumbos5:20200721093624p:plain

tips

お手軽に試したいようであればこちらのプラグインでもプレビュー可能です。ただ外部サーバにリクエストしているようなのでレンダリングが遅かったり、またセキュリティ上も微妙かなと思います。 shd101wyy.github.io

また前回弊社のいとうさんが記事を書いてくださり、GitLabでPlantUMLをプレビューできると書いてくれています。これは便利なんですが、!include 機能がGitLabでは現在まだ利用できないように見えるので、これはただ期待するばかり。

用法用量を守って楽しくUMLしましょう。