Zeals TECH BLOG

チャットボットでネットにおもてなし革命を起こす、チャットコマース『Zeals』を開発する株式会社Zealsの技術やエンジニア文化について発信します。

pytestのparametrizeの使い方とその有用性について

f:id:RyotaAraki:20191211175222j:plain

※こちらはZeals AdventCalendar 12日目の記事です。

こんにちは!

Pythonエンジニアの荒木です。
夏頃に社内の開発プロジェクトにPythonメンバーとしてアサインされ、本格的にPythonを業務で書くようになり、最近はほぼ毎日Pythonを書いています。

Pythonの業務としては主に新機能実装とDBリファクタリングをやっています。

今日はpytestのparametrizeの基本的な使い方
parametrizeでちょっと詰まったところがあったので、それらについて書きます。

目次は以下のようになります。

  • 前提
  • parametrizeについて
    • parametrize とは
    • parametrize の使い方
  • fixtureをparametrizeする
    • fixtureとは
    • fixtureのparametrize
  • 終わりに

前提

まず、以前にこちらの記事で紹介したように、Zealsは、LINEおよびFacebook Messenger上で動作するチャットボットサービスで、サービスは大きく分けると、

  1. チャットボットの管理用Webアプリケーション
  2. チャットボットへのメッセージ送受信サービス
  3. 1と2から共通処理を切り出したマイクロサービス群

の3つに分かれています。
詳細はこちらの記事をご覧ください。

tech.zeals.co.jp

その中の 2.メッセージ送受信機能フレームワークを採用しないピュアなPythonで実装しており、チャットボットに流入したエンドユーザーに対して、設定されたシナリオの応答や配信処理 を行います。
実際に、10万人を超えるユーザーに対する配信もここで行っています。

このようにZealsにおけるPythonコードは直接エンドユーザーとやりとりするので、誤った挙動をしてしまうとそのまま配信事故につながるリスクが大きいです。
例えば、想定していた配信先と異なる配信先にメッセージを送信して閉まった場合は、そのまま配信事故に繋がることになります。

そのためにこの配信機能の開発はより慎重さが求められ、当然テストコードの重要性も高くなります

parametrizeについて

parametrize とは

Zealsでは、Pythonのテストフレームワークとしてpytestを導入しています。

parametrizeとはpytestで使えるデコレータの1つで変数と結果をパラメータでテストコードに渡すことで、1つのテストメソッドで複数パターンのテストを行うことができます。

parametrizeを使ってテストコードを書くことで
テストコードをDRYにして改修コストを下げることができます。

parametrize の使い方

実際にparametrizeを使ってみます。

import pytest


numeric_datas = [
    (2, 5, 10),
    (3, 6, 18),
    (5, 9, 45)
]


@pytest.mark.parametrize("a, b, expect", numeric_datas)
def test_multiple(a, b, expect):
    assert a*b == expect

与えた2つの変数a, bの積がexpectであるかどうかを判別するテストです。 これを実行してみると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/hogehoge/Workspace/myproject
collected 3 items

test_parametrize.py ...                                                [100%]

============================= 3 passed in 0.01s ==============================

となり、いずれの計算結果も正しかったことが分かりました。
このブログのコンソールのUIは白黒ですが、実際はテストが通ると1番下のラインが綺麗な緑色になります。

試しに与えるexpectを誤ったものに変えてみます。

numeric_datas = [
    (2, 5, 8),
    (3, 6, 18),
    (5, 9, 45)
]

この内容でテストを実行すると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/hogehoge/Workspace/myproject
collected 3 items

test_parametrize.py F..                                                [100%]

================================== FAILURES ==================================
____________________________ test_multiple[2-5-8] ____________________________

a = 2, b = 5, expect = 8

    @pytest.mark.parametrize("a, b, expect", numeric_datas)
    def test_multiple(a, b, expect):
>       assert a*b == expect
E       assert (2 * 5) == 8

test_parametrize.py:16: AssertionError
======================== 1 failed, 2 passed in 0.05s =========================

このように、どのパラメータを与えた時にテストが落ちたのかが分かります。
あるパラメータでテストが通過しなくても、他のパラメータが別で独立してテストを続行してくれるところはありがたいですね。

parametrizeについてのドキュメントはこちらです。

docs.pytest.org

実際のZealsにおけるparametrizeのユースケースとしては
LINEとFacebook Messengerで共通した処理のテストをLINE, Facebook Messengerそれぞれの変数をパラメータで渡したり、個人情報に関するメッセージの情報の種類(郵便番号や住所など)をパラメータとして渡すことで活用しています。

fixtureをparametrizeする

さて、次はfixtureのparametrizeについてです。

fixtureとは

fixtureとはpytestで使い回すことができる、テスト用のインスタンスを定義しているメソッドのことです。

実際にpytestでテストメソッドを回す際には、fixtureをテストメソッドの引数に与えてテストを回すことが多いです。
引数として与えられたfixtureはテストメソッドが実行される前に必ず実行されるようになっています。

fixtureのparametrize

やりたかったことは「このfixtureをパラメータに渡してテストメソッドを回したい」ということでした。

そのため、↓のようにfixtureを定義してメソッドを実行してみます。

@pytest.fixture
def a():
    return 'A'

@pytest.fixture
def b():
    return 'B'

@pytest.fixture
def c():
    return 'C'


characters = [
    ('a', 'A'),
    ('b', 'B'),
    ('c', 'C')
]


@pytest.mark.parametrize("letter, expect", characters)
def test_capitalize(letter, expect):
    assert letter == expect

実際に実行すると、いずれのテストも失敗します。
テストケース3つともすべて同じ原因で失敗をするのですが、エラーメッセージは以下のように表示されます。

____________________________ test_capitalize[a-A] ____________________________

letter = 'a', expect = 'A'

    @pytest.mark.parametrize("letter, expect", characters)
    def test_capitalize(letter, expect):
>       assert letter == expect
E       AssertionError: assert 'a' == 'A'
E         - a
E         + A

test_parametrize.py:47: AssertionError

letterがfixtureとして認識されていないために、変数 'a' として返されていることが分かります。

そこで、与えた変数をfixtureに変換してあげる必要があります。
その変換のためのメソッドが getfixturevalue() です。

公式ドキュメントは↓です。

docs.pytest.org

これを用いて変数を変換してみましょう。

@pytest.fixture
def a():
    return 'A'

@pytest.fixture
def b():
    return 'B'

@pytest.fixture
def c():
    return 'C'


@pytest.fixture
def get_fixture_values(request):
    def _get_fixture(fixture):
        return request.getfixturevalue(fixture)
    return _get_fixture


characters = [
    ('a', 'A'),
    ('b', 'B'),
    ('c', 'C')
]


@pytest.mark.parametrize("letter, expect", characters)
def test_capitalize(letter, expect, get_fixture_values):
    letter = get_fixture_values(letter)
    assert letter == expect

これを実行すると、

============================ test session starts =============================
platform darwin -- Python 3.7.4, pytest-5.3.1, py-1.8.0, pluggy-0.13.1
rootdir: /Users/mbp028/Workspace/myproject
collected 3 items

test_parametrize.py ...                                                [100%]

============================= 3 passed in 0.02s ==============================

テストが問題なく通過しました。
ちなみに、Zealsではこのメソッドをpluginで実装しており、Pythonアプリケーション内の様々なファイルから呼び出すことができるように工夫しています。

終わりに

Pythonは日本語の情報が少なく、ましてやpytestやparametrizeと
より細かい領域の話であれば、情報を集めるのになおさらハードルが上がってしまうと感じます。

続きを読む

Mozilla Developer Roadshow 2019 - A short report!

f:id:aburd:20191118171719j:plain

I recently attended the 2019 Developer Roadshow Asia: Tokyo. I am a huge fan of Mozilla's work, not just simply for their Firefox browser, but for their efforts in trying to make the internet accessible to everyone. So when a friend told me that this conference was happening, I knew I had to go!

Obligatory, but here's some new Mozilla swag they were handing out at the door.

It was a relatively short affair, but the presentations were quite fun. Without further ado, here are three summations of my favorite presentations at the event.

1. Karl Dubost and Daisuke Akatsuka - Best Viewed With…Let’s Talk about WebCompatibility

f:id:aburd:20191118171140j:plain

I think there is this tendency to think that our websites work more or less. Short deadlines, ignorance, or just plain laziness can be a leading cause of our pages breaking on a multitude of devices. Karl Dubost and Daisuke Akatsuka to the rescue with some tips and tools to make your websites compatible with an ever complex web.

Karl works with Mozilla's WebCompatibility to fix broken sites, not only when displaying in Firefox, but to increase cross-browser compatibility among all major vendors. If you need a broken site, you can report it to Karl!

Daisuke Akatsuka went over how you can diagnose your own problems by using Firefox's dev-tools, especially surrounding CSS issues. Everybody knows you can change CSS rules in devtools, but did you know that Firefox will also give you hints when you write a busted CSS-rule? How about the ability to change fonts on your page in real time?

f:id:aburd:20191118173538p:plain

2. Hui Jing Chen - Making CSS from Good to Great: The Power of Subgrid

f:id:aburd:20191118171146j:plain

Ok, do you remember the days of float and clearfix? Thank goodness CSS has matured a bit since then. Yes, we have flexbox, but what about grid? Have you tried yet? I know I haven't given it the time it probably deserves...and Hui Jing Chen confirmed all my worst fears. I'm missing out!

Hui Jing Chen explained that CSS-Grid allows you to define that size of content in your layouts from the parent's point of view, rather than the child's point of view. This is an incredibly powerful tool and if you've ever wanted to go deep on flexbox, you can checkout her speaking yourself on youtube! www.youtube.com

3. Kathy Giori - Mozilla WebThings: Manage Your Own Private Smart Home Using FOSS and Web Standards

f:id:aburd:20191118171133j:plain

By far my favorite talk was by Kathy Giori, who turned on a bunch of stuff in her California office remotely from the presentation floor in Tokyo!

It's one thing to make stuff on your computer screen move around, but programming an IoT device to make ambulance sounds or a multi-colored light ball to spin around, all in a totally visual programming language? Nerds of the world unite! I am of course talk about the Mozilla IoT - WebThings Framework, which offers developers a way to interface with all your IoT devices so that you can focus on the fun part about IoT, making physical stuff do stuff.

Kathy was even handing out sim cards for FREE to everyone after the show that allowed you to get up and running raspberry pi (and I snagged one, heh). I was lucky enough to chat with her a bit and she let me know what actually, it's not as hard to adapt existing devices for IoT as I thought.

You can watch Kathy Giori talk about Mozilla IoT on youtube: www.youtube.com

続きを読む

Should you convert your React App from Javascript to Typescript?[React開発の言語をJavaScriptからTypeScriptに移行する理由]

f:id:zeals-engineer:20191123153958p:plain

(※画像はGeekbotさんの記事より引用)

ベトナム出身のフルスタックエンジニアNguyen(グエン)さんが、ReactにおけるJavaScriptとTypeScriptについての記事を書いてくれました!

Zealsでは、フロントエンド開発にReactフレームワークを導入しています。React環境には他にもJestなどのテスト環境や、TypeScrptの導入なども先行して行っています。

tech.zeals.co.jp

しかしTypeScriptについては、これまでは主にJavaScriptでフロントエンドの開発を進めてきたため、現在は両方の開発言語が混在している状態にあります。

そのような環境で、実際にフロントエンド開発に携わるエンジニアがどう考えて実装を進めているかについて、Nguyuenさんが記事を書いてくれました。ぜひ色んな方に読んでいただきたいので、後半では文章を日本語訳して書いております。英語が苦手な方はそちらをお読み頂けると嬉しいです!

それでは本編、お楽しみください。

  • Should you convert your React App from Javascript to Typescript?
  • Why we decided to integrate Typescript
    • Potential Issues
  • Let's convert some existing components from JS to TS and see the magic
    • Note
    • Note
  • Conclusion
  • References
  • [日本語訳]なぜReactをJSからTypeScriptに変換すべきなのか?
  • Typescriptを既存のReactに統合した理由
    • 潜在的な問題
  • 既存のJSコンポーネントをTSに変換する方法
    • 注意
    • 注意
  • 結論
  • リファレンス
  • さいごに(訳者あとがき)

Should you convert your React App from Javascript to Typescript?

Hi everyone, my name is Nguyen and I'm a front end engineer at ZEALS. Before, I only used Javascript and React to develop. But since I've learned Typescript and have enjoyed writing in TypeScript for more than a few months, now I feel I will struggle without it! Currently Angular has wonderful Typescript support, but how about Typescript with React? What are the advantages and disadvantages of doing so? I would like to share my throughts on why should we chose to integrate Typescript in our large React.

Why we decided to integrate Typescript

  1. Our team is getting bigger and there are many developers are working together. We think Typescript will help new developers understand our workflow and data structures used throughout the project.
  2. The front-end team wants to decrease small bugs while coding.
  3. We think new developers will find it slightly easier to maintain the existing codebase.
  4. We also think that the code will be easier to refactor in the future.

Potential Issues

  • It takes some time to get used to Typescript.

Let's convert some existing components from JS to TS and see the magic

Note

  • The following example is React <~ 16.8.0

I have a sample React component made in Javascript. This component has props, state and fetches data through an API.

  • The referencing repository are here

https://github.com/knguyen30111/diary

// This is mock API to fetch data from the Database
const todos = [
  {
    id: 1,
    text: "Learn React",
    done: true,
    priority: 1
  },
  {
    id: 2,
    text: "Learn JSX",
    done: false,
    priority: 3
  }
];


export function getTodoList() {
  return Promise.resolve(todos);
}

export function getTodo(id) {
  const todo = todos.find(todo => todo.id === parseInt(id));
  return Promise.resolve(todo);
}
import React, { Component } from 'react'
import PropTypes from 'prop-types
// mock fetch API
import { getTodo } from './mock-api'

class TodoDetailContainer extends Component {
  state = {
    id: null,
    text: '',
    done: null,
    priority: null,
  }

  componentDidMount() {
    const { match } = this.props
    // This is mock fetch API
    getTodo(match.params.id)
      .then(todo => this.setState({ ...todo }))
      .catch(error => console.error(error))
  }

  render() {
    return (
      <div>
        <pre>{JSON.stringify(this.state, null, 2)}</pre>
      </div>
    )
  }
}

TodoDetailContainer.propTypes = {
  match: PropTypes.object
}

export default TodoDetailContainer

When you look at both of the files above, it has some potential problems:

     1. The writer of this component was not clear about the types in the component
     2. I have to guess what the types of the state will be
     3. It's not clear what the type of the API call should return

I applied Typescript to both of files above:

const todos: Array<TodoData> = [
  {
    id: 1,
    text: "Learn React",
    done: true,
    priority: 1
  },
  {
    id: 2,
    text: "Learn JSX",
    done: false,
    priority: 3
  }
];

//This is one object with datatype in array response must have id, text, priority and optional is done
export interface TodoData {
  id: number,
  text: string,
  done: boolean,
  priority: number,
}

//This is mock API to response Promise with Array TodoData
export function getTodoList(): Promise<Array<TodoData>> {
  return Promise.resolve(todos);
}

//This is a mock API that responds with todoData
export function getTodo(id: string): Promise<TodoData> {
  const todo = todos.find(todo => todo.id === Number(id));
  return Promise.resolve(todo);
}
import React, { Component } from 'react'
import { TodoData, getTodo } from './mock-api'


//Define Props dataType
interface Props {
  match: {
    params: {
      id: string
    }
  }
}

type State = TodoData

class TodoDetailContainer extends Component<Props, State> {
  state = {
    id: 0,
    text: '',
    done: false,
    priority: 0,
  }

  componentDidMount() {
    const { match } = this.props
    // This is mock fetch API
    getTodo(match.params.id)
      // Typescript tells us the result of this function is `Promise<TodoData>`, which ensures that setState is being used correctly
      .then((todo) => this.setState({ ...todo })) 
      .catch((error) => console.error(error))
  }

  render() {
    return (
      <div>
        <pre>{JSON.stringify(this.state, null, 2)}</pre>
      </div>
    )
  }
}

export default TodoDetailContainer

After converting to Typescript, the component with me is totally understandable:

  1. First thing I want to talk about Fetch API:

・Through Typescript, I know getTodo(id: string) needs argument id with datatype is string and response data is Promise<TodoData>. We don't need to worry about if the API requires the ID as a number or string. In addition, we know that we are setting the state of the component correctly with TodoData.
・Frontend and backend members know the request and response data. This kind of information can be maintained through documentation, but maintaining documentation about many APIs can be difficult. But when you see above, It's clear to all members that the component works correctly, even if they haven't read the API functions module.
・Typescript's static analysis will let us know when we've made a mistake. If the API changes, simply update the interface and Typescript will tell you if you're missing something or setting something incorrectly.

  1. The second thing to notice is the component is easier to understand:

・There's no need to use the PropType library! We can simply use Typescript's typing system instead of setting propTypes on the component.
・And we can use the same type of system for the state as well! We were able to define the components State as an alias of TodoData, which means that if someone updates that interface, this component's state will automatically be updated. In addition, the compiler will complain that this component's initial state is not being set correctly if there is a change to that interface and it is lacking something necessary.

If you prefer React Hooks, over class components, I have converted the previous example to a React Hooks version and put it below:

Note

  • The following example is React 16.8.0 above
import React, { useState, useEffect } from 'react'
import { getTodo } from '../mock-api'


const TodoDetailContainer = props => {

  const { match } = props;

  const [todo, setTodo] = useState(null);

  useEffect(() => {
    const runEffect = async () => {
      const id = match.params.id
      try {
        const todo = await getTodo(id)
        setTodo(todo)
      } catch (e) {
         console.error(e)
      }
    }
    runEffect()
  }, [match.params.id, setTodo])

  return (
    <div>
      <pre>{JSON.stringify(todo, null, 2)}</pre>
    </div>
  )
}

export default TodoDetailContainer
import React, { useState, useEffect } from 'react'
import { TodoData, getTodo } from './mock-api'

//Define Props interface
interface Props {
  match: {
    params: {
      id: string
    }
  }
}

type State = TodoData

const TodoDetailContainer: React.FC<Props> = props => {
  const { match } = props;

  const [todo, setTodo] = useState<State>({ id: 0, done: false, priority: 0, text: '' });

  useEffect(() => {
    const runEffect = async () => {
      const id: string = match.params.id
      try {
        const todo = await getTodo(id)
        setTodo(todo)
      } catch (e) {
        console.error(e)
      }
    }
    runEffect()
  }, [match.params.id, setTodo])

  return (
    <div>
      <pre>{JSON.stringify(todo, null, 2)}</pre>
    </div>
  )
}

export default TodoDetailContainer

Conclusion

After converting JS to TS, I found the experience mostly positive:

  • Small bugs are gone while coding (or caught by the compiler).
  • The workflow is clear with every single function or component. The types of inputs and outputs of functions/components are defined and communicated to anybody reading the code.
  • New members can immediately maintain our codebase if we have TypeScript components.
  • In the future, if somebody wants to refactor a component or function. It's easier to do because we know for sure the data inside components and functions. In addition, Typescript will tell us if we've forgot to return something, or carry out an operation that is incompatible with the types.

But that's not to say that it was all positive, there are some potential problems with converting an existing JS codebase to TS:

  • I think if the codebase is small, we don't need to apply Typescript. In fact, when applying Typescript to a small codebase you may find that adding types takes more time than is worth the benefit. However, if you are working in a big team with a large codebase, the extra typing will save you hours of hunting down small bugs and increase understanding among your team members.
  • I don't recommend you convert all existing components to Typescript. It takes time to do this and you may not receive much benefit. But when creating the new components applying TypeScript will pay dividends.

References

itnext.io

dev.to

[日本語訳]なぜReactをJSからTypeScriptに変換すべきなのか?

みなさん、こんにちは!グエンです。ZEALSでフロントエンドエンジニアをやっています。

私はこれまで、Zealsのフロントエンド開発でJavascriptしか使ってきませんでした。しかしTypescriptを勉強してからはTypeScriptばかりを使っていて、もはやTypeScriptなしでは生きられない身体になってしまいました。笑

現在、AngularではTypescriptを十分にサポートしていますが、TypeScriptとReactの組み合わせについてはどうでしょうか?大規模なReactにTypescriptを統合することを選択した理由について、メリットやデメリットなど、私の考えを皆さんに共有したいと思います。

Typescriptを既存のReactに統合した理由

  1. 開発チームが大きくなってきたため。 Typescriptであれば、プロジェクト全体で使用されるワークフローとデータ構造を、新しく入ってきたエンジニアがキャッチアップしやすい 2.フロントエンドチームは、開発中に起こり得るバグを事前に少しでも減らしたい 3.新しく入ってきたエンジニアは、新しいコードベースをイチから作るより既存のコードベースを使う方が簡単なはず 4.TypeScriptコードは将来的にリファクタリングしやすくなる(と考えられる)ため

潜在的な問題

  • Typescriptに慣れるには時間がかかる

既存のJSコンポーネントをTSに変換する方法

注意

  • 以降では、Reactは16.8.0以下のバージョンの利用を前提としています

Javascriptで作成したサンプルのReactコンポーネントがあります。このコンポーネントには、APIを介してStateとFetchデータを保持しています。

  • サンプルリポジトリはこちら

https://github.com/knguyen30111/diary

// This is mock API to fetch data from the Database
const todos = [
  {
    id: 1,
    text: "Learn React",
    done: true,
    priority: 1
  },
  {
    id: 2,
    text: "Learn JSX",
    done: false,
    priority: 3
  }
];


export function getTodoList() {
  return Promise.resolve(todos);
}

export function getTodo(id) {
  const todo = todos.find(todo => todo.id === parseInt(id));
  return Promise.resolve(todo);
}
import React, { Component } from 'react'
import PropTypes from 'prop-types
// mock fetch API
import { getTodo } from './mock-api'

class TodoDetailContainer extends Component {
  state = {
    id: null,
    text: '',
    done: null,
    priority: null,
  }

  componentDidMount() {
    const { match } = this.props
    // This is mock fetch API
    getTodo(match.params.id)
      .then(todo => this.setState({ ...todo }))
      .catch(error => console.error(error))
  }

  render() {
    return (
      <div>
        <pre>{JSON.stringify(this.state, null, 2)}</pre>
      </div>
    )
  }
}

TodoDetailContainer.propTypes = {
  match: PropTypes.object
}

export default TodoDetailContainer

上記のファイルには、いくつか問題があります。

  1. このコンポーネントの作成者は、コンポーネントのTypeを明確に示していない
  2. コールドリーディングの際、StateのTypeを予測しなければならない
  3. APIを叩く時に、どのようなTypeが返ってくることを期待しているかがよくわからない

そこで、上記のファイルにTypescriptを適用してみましょう。

const todos: Array<TodoData> = [
  {
    id: 1,
    text: "Learn React",
    done: true,
    priority: 1
  },
  {
    id: 2,
    text: "Learn JSX",
    done: false,
    priority: 3
  }
];

//This is one object with datatype in array response must have id, text, priority and optional is done
export interface TodoData {
  id: number,
  text: string,
  done: boolean,
  priority: number,
}

//This is mock API to response Promise with Array TodoData
export function getTodoList(): Promise<Array<TodoData>> {
  return Promise.resolve(todos);
}

//This is a mock API that responds with todoData
export function getTodo(id: string): Promise<TodoData> {
  const todo = todos.find(todo => todo.id === Number(id));
  return Promise.resolve(todo);
}
import React, { Component } from 'react'
import { TodoData, getTodo } from './mock-api'


//Define Props dataType
interface Props {
  match: {
    params: {
      id: string
    }
  }
}

type State = TodoData

class TodoDetailContainer extends Component<Props, State> {
  state = {
    id: 0,
    text: '',
    done: false,
    priority: 0,
  }

  componentDidMount() {
    const { match } = this.props
    // This is mock fetch API
    getTodo(match.params.id)
      // Typescript tells us the result of this function is `Promise<TodoData>`, which ensures that setState is being used correctly
      .then((todo) => this.setState({ ...todo })) 
      .catch((error) => console.error(error))
  }

  render() {
    return (
      <div>
        <pre>{JSON.stringify(this.state, null, 2)}</pre>
      </div>
    )
  }
}

export default TodoDetailContainer

Typescriptに変換すれば、コンポーネントの中身を完全に理解することができるのではないでしょうか?

  1. Fetch APIの前提

getTodo(id:string) は引数idがデータ型であり、応答データが Promise <TodoData> である必要があるとわかります。 IDを「数値」または「文字列」として渡す必要があるかどうかを気にしなくてよくなりますね。さらに、 TodoDataを使用してコンポーネントのStateを正しく設定していることがわかります。
・フロントエンドとバックエンドのエンジニアはリクエストとレスポンスのデータの内容を理解できます。この種のノウハウはドキュメントを残すことで共有できますが、ドキュメントをうまく残して運用することは簡単ではありません。ただし、上記を見ると、API関数モジュールを読み取っていなくても、コンポーネントが正しく機能することを読み取れると思います。
・Typescriptの静的解析は、コードのミスを教えてくれます。 APIが変更された場合はインターフェースを更新するだけで、Typescriptが間違っている内容を知らせてくれます。

  1. 2番目に注意することは、コンポーネントが理解しやすいことです。

・「PropType」ライブラリを使用する必要はありません!コンポーネントに「propTypes」を設定する代わりに、Typescriptの型システムを使用できます。
・また、Stateにも同じく型システムを使用できます!コンポーネント StateTodoDataのエイリアスとして定義できました。つまり、誰かがインターフェースを更新すると、このコンポーネントのStateの型が動的に変更されます。さらに、コンパイラはそのインターフェースの変更に間違いがあれば、コンポーネントのStateが正しく設定されていないことを教えてくれます。

クラスコンポーネントよりもReact Hooksを使いたい場合は、前の例をReact Hooksバージョンに変換したものを以下に示します。

注意

  • 以降では、Reactは16.8.0以下のバージョンの利用を前提としています
import React, { useState, useEffect } from 'react'
import { getTodo } from '../mock-api'


const TodoDetailContainer = props => {

  const { match } = props;

  const [todo, setTodo] = useState(null);

  useEffect(() => {
    const runEffect = async () => {
      const id = match.params.id
      try {
        const todo = await getTodo(id)
        setTodo(todo)
      } catch (e) {
         console.error(e)
      }
    }
    runEffect()
  }, [match.params.id, setTodo])

  return (
    <div>
      <pre>{JSON.stringify(todo, null, 2)}</pre>
    </div>
  )
}

export default TodoDetailContainer
import React, { useState, useEffect } from 'react'
import { TodoData, getTodo } from './mock-api'

//Define Props interface
interface Props {
  match: {
    params: {
      id: string
    }
  }
}

type State = TodoData

const TodoDetailContainer: React.FC<Props> = props => {
  const { match } = props;

  const [todo, setTodo] = useState<State>({ id: 0, done: false, priority: 0, text: '' });

  useEffect(() => {
    const runEffect = async () => {
      const id: string = match.params.id
      try {
        const todo = await getTodo(id)
        setTodo(todo)
      } catch (e) {
        console.error(e)
      }
    }
    runEffect()
  }, [match.params.id, setTodo])

  return (
    <div>
      <pre>{JSON.stringify(todo, null, 2)}</pre>
    </div>
  )
}

export default TodoDetailContainer
続きを読む

Zealsの新たな開発拠点『Zeals Garage』はなぜ生まれたのか?

f:id:neuneu39:20191122112053j:plain こんにちは。Zeals開発チームのPMで、今回のオフィス増床の責任者を務めた阿久津です。

去る11月6日、Zealsの新しい開発拠点『Zeals Garage(以下Garage)』がオープンしました。

zeals.co.jp

本日はこちらのGarage増築の背景や中の様子、実際の使い勝手などをエンジニア目線で紹介したいと思います。

  • Garageを作るに至った背景
  • エンジニアが考えるエンジニアのためのオフィス
    • 開発チームの課題洗い出し
    • 開発チームのあるべき姿の議論
      • 全ては熱意の「共有」からはじめ、アウトプットも「共有」する
      • 失敗を恐れず「挑戦」し、集中してやりきる
      • 志を同じくする仲間と共に「腕を磨き」、達成感を味わう
    • テクノロジードリブンでZealsのバリューを実現するためのオフィス
      • あらゆることを共有できる空間
      • 集中かコミュニケーションかを選べる執務環境
      • メンバー同士で学び合うことができる環境
    • 名前に込められた想い
  • エンジニアの想いが形になったオフィス
    • 全体の図面
      • ペアプロ専用スペース
      • 集中できるスペース
      • 広くなった執務デスク
  • 開発メンバーからの声
  • 最後に

Garageを作るに至った背景

2019年9月13日に、Zealsはロゴおよびサービス名の変更を行い、新たな一歩を踏み出しました。

zeals.co.jp

事業の拡大を加速させていく過程で、Zealsが抱えるプロダクトの課題レベルは徐々に高くなっています。例えば大規模なトラフィックを捌いたり、ログをRDB外に切り出していく、といったことが必要になっています。

tech.zeals.co.jp

これらの課題を解決し事業をより前に進めていくために、Zealsの開発チームはより高い技術力とプロフェッショナル精神を持った開発者集団に進化していく必要があります。エンジニアの人数も順調に増えオフィスが手狭になったことと重なり、エンジニアのための開発拠点オープンPJが動き始めました。

エンジニアが考えるエンジニアのためのオフィス

今まで何度かオフィス移転を経験してきたZealsですが、エンジニア主導のオフィス移転はもちろん初めて。

以前からオフィス移転の際にお世話になっているヒトカラメディア様協力の元、Garageの構成やデザインなどの検討を開始しました。

オフィスの方向性を大きく決めることになったトピックスを中心にご紹介します。まずは、「開発チームの在り方」から検討をはじめました。

  • 開発チームの課題洗い出し
  • 開発チームのあるべき姿の洗い出し
  • テクノロジードリブンでZealsのバリューを実現するためのオフィス

開発チームの課題洗い出し

f:id:neuneu39:20191122110350j:plainf:id:neuneu39:20191122110356j:plain

20人弱のエンジニア全員で貸し会議室を3時間貸し切り、開発チームの継続するべき点や改善するべき点、今後の開発チームの理想像についてプレスト行いました。 開発チームのマインドやプロダクトへの思い、開発フローまでいろいろな角度から、改めてZealsの開発チームについて振り返る貴重な時間となりました。

振り返りを通して出てきた課題として、エンジニアが開発に集中できる時間が限られ、コードを書く時間が短くなりがちであるという課題意識が多くのメンバーから出ました。

開発チームのあるべき姿の議論

現状の開発チーム課題が棚卸しできたので、次に「開発チームは今後どのような組織であるべきか?」を改めて考えました。

抽象的な議論になるため様々な意見が出ましたが、Zealsとして大切にしている3つのバリューFROM ZEAL, BET ON PARADOX, UNITED WILLを体現できる組織であることが第一。そして、そこにテクノロジーで事業を引っ張っていくという意志を込めるために、技術が先導する テクノロジードリブン な開発を行える組織を目指すのが良いのではないかという結論に至りました。

f:id:neuneu39:20191122120402p:plain

大まかな方向性が決まったので、開発チームがテクノロジードリブンでバリューを実現できるためには、どういった環境が必要か? ということを改めてチームで議論し、その結果以下の3つに集約しました。

全ては熱意の「共有」からはじめ、アウトプットも「共有」する

失敗を恐れず「挑戦」し、集中してやりきる

志を同じくする仲間と共に「腕を磨き」、達成感を味わう

テクノロジードリブンでZealsのバリューを実現するためのオフィス

理想とするチームの像や環境が絞り込まれましたので、その後はエンジニア全員で、 理想とするオフィスを画像イメージやモノで当てはめることで形にしていきました。

各メンバーが思う理想のオフィスに必要な機能を、実際の写真や言葉で形にしていくこの過程は楽しく、あっという間に時間は過ぎました。

f:id:neuneu39:20191122120525p:plain

この議論を経て、オフィスに必要な機能としては、以下の3つに集約しました。

あらゆることを共有できる空間

集中かコミュニケーションかを選べる執務環境

メンバー同士で学び合うことができる環境

ここからは、数ヶ月のうちに開発チームの理想のオフィスが図面になり、着工となりました。

名前に込められた想い

Zealsの新しい開発拠点「Zeals Garage」という名前には開発チームの想いが込められています。

アップルやグーグルと行った名だたるIT企業も最初はガレージからのスタートでした。そんな事実から、新しい開発拠点を「イノベーションを生み出し、ワクワクするモノづくりが行われる空間」にしていこう、というメンバー達の意気込みが込められています。

blog.btrax.com

エンジニアの想いが形になったオフィス

11/6(水)ついにGarageがオープンしました。エンジニアの視点でGarageを紹介したいと思います。

全体の図面

GarageはMTG、執務室等、一般的なスペースからペアプロや集中専用のスペース等、エンジニアの思いが込められたスペースで構成されています。 簡単にですが、特徴的なスペースやポイントを紹介したいと思います。 f:id:neuneu39:20191124220504p:plain

ペアプロ専用スペース

こちらは、ペアプロ専用のスペースです。

従来はスペースの関係上なかなか行われてこなかったペアプロですが、今では毎日の様に誰かがペアプロを行っていて、日々学びを実感してくれています。

f:id:neuneu39:20191125154552j:plain

集中できるスペース

一人で集中したい時のための空間です。集中スベースの角は、あっという間に人気の仕事場になりました。 f:id:neuneu39:20191125154432j:plain

広くなった執務デスク

一人あたり横幅1400mmの執務デスクです。モニターを2台配置しても十分なスペースが確保できます。 f:id:neuneu39:20191125154520j:plain

開発メンバーからの声

f:id:neuneu39:20191122112454j:plain

エンジニアからは、 開発に集中できる時間が増え、生産性が上がった という声を多数上がりました。

実際に、スクラムを導入しているチームではベロシティが1.5倍近く向上しているチームもあります。休憩時間になると自然と入口のMTGスペースに人が集まり、コミュニケーションが始まる姿を見ると、これから何かが起こりそうというワクワクを感じます。

続きを読む

第3回、Zeals開発合宿に行ってきました!@湯河原

f:id:zeals_kody:20191121213718j:plain

どうも!味わい深いエンジニア kody です!Zealsでは第3回となる開発合宿に行ってきました!

Zealsでは、過去に2度の開発合宿を行っております。

tech.zeals.co.jp

tech.zeals.co.jp

3度目の実施となった今回の合宿はこれまでと異なり、Zealsの社員メンバーだけでなく、普段お世話になっている業務委託や社外の方なども巻き込んで(任意参加)参加してもらう、新しいスタイルで合宿を企画してみました。

このような企画にした意図として、普段リモートのみで稼働してる方や、Slackではよく見かけるけど(出社の頻度が少なく)実際に話したことがない方も混ぜて行うことで、より有意義なコミュニケーションが生まれ結果チーム力が上がるとのではないか、という考えがありました。

それでは、当日の模様をお伝えして参ります。

  • 合宿のしおり(スケジュール)
    • 合宿の会場
  • 開幕!
    • テーマ一覧
  • 開発中の様子
    • 1日目
    • 夜ご飯
    • 2日目
    • 最終プレゼンテーション
  • 反省
    • よかったこと
    • 改善したいこと
  • さいごに

合宿のしおり(スケジュール)

実際の合宿は以下のようなスケジュールで進みました。特に変わったことをするわけではなく、ひたすらに集中して黙々と作業する時間が続きます。笑

f:id:zeals_kody:20191121203418p:plain

合宿の会場

今回お世話になったのは、開発合宿の宿といえば...でおなじみ「おんやど恵」さん。 開発合宿用にプランが用意されており、設備も開発用に整えられているので特に準備せずに気軽に合宿を始められるのでおすすめです!

以下のリンクから予約できるので、開発合宿の開催を検討中の方はぜひ。

www.onyadomegumi.co.jp

(※人数次第では早めに予約しないと取れない可能性があるのでご注意ください)

開幕!

f:id:zeals_kody:20191121212206j:plain

今回の合宿では、各自で自由にテーマを決めて開発を進めていく形を取りました。

参加者の開発テーマ一覧はこちら!機械学習からインフラまで様々なテーマに取り組みました!

テーマ一覧

- Quattro Luncher(”Quattro Lunch”という、社内のシャッフルランチをいい感じに仕分けるアプリケーション)
- PWAを使ったwebサービスの開発
- TwitterAPIとJavaScript(Node.jsやReac)を用いたアプリケーション開発
- Golangの勉強(とりあえずTour of Go)
- CLI化ツールの習得&コードスコア自動スクレイピングのCLIツール化
- KPI管理シート自動更新スクリプト開発
- CI/CDを修正してのDXの向上
- k8sを使ったイベント駆動マイクロサービス作成
- automlでCV予測
- 自然言語処理を用いたbot開発
- React Native with Expo で スマホアプリ作成
- Face detection
- Golang × LINE bot
- Prophetを用いたデータ量増加予測
- Pythonコードに Annotation を追加

開発中の様子

1日目

スタート直後の様子です。諸事情あり、遅れて来る方もいたので、開始時点では10名程度のメンバーが集まっていました。 f:id:zeals_kody:20191121212421j:plain

開発の息抜きに、近くに公園があるので体も動かせます。(これで運動不足も解消👍)

f:id:zeals_kody:20191121212709j:plain

なんと、地方にいるインターン生も遅れて参加してくれました!!(写真左) f:id:zeals_kody:20191121212825j:plain

15時を過ぎ、部屋のチェックイン可能になったので各々好きなところで自由に開発します。

部屋でゴロゴロする人やリラクゼーションスペースを活用する人も・・・ f:id:zeals_kody:20191121222857j:plain

開発できる場所が会議室だけではないので、気分転換できて良いですね。 ほぼ全員集合できたのが、 18:00 を過ぎたあたりでした。

夜ご飯

f:id:zeals_kody:20191121212937j:plain

さて、お楽しみの夜ご飯(お酒)の時間です。笑

夜ご飯(お酒)をほどほどに楽しんだ後、各々自由に過ごします。

f:id:zeals_kody:20191121213025j:plain

夜ご飯の後も黙々と開発する人が予想より多くいて感激しました。 メンバーによっては、かなり夜遅くまで開発をしていた人もいましたね。 f:id:zeals_kody:20191121213222j:plain

2日目

あっという間に、2日目の朝です。

旅館の朝ごはん、量が多くてもなぜだか食べれちゃいますよね(?)。

f:id:zeals_kody:20191121213301j:plain

僕はコーヒー片手に、一人寂しく朝のお散歩を。 自然が隣にある環境は控えめにいって最高でした。

f:id:zeals_kody:20191121222454j:plain

最終プレゼンテーション

15時からは最終プレゼンテーションでした。 各々がこれまで開発した内容を、まとめて発表してもらいました!

f:id:zeals_kody:20191121213559j:plain

各自が熱の入ったプレゼンテーションを実施してくれたので、盛り上がりました!

また、普段業務で使わない技術を使うメンバーも多かったので、知らない・使ったことのない技術について知る良いきっかけとなりました。

反省

よかったこと

  • 普段関わりの少ないメンバーを巻き込むことで、チームの一体感が増した
  • 技術力の高い業務委託のメンバーからの学びがあった
  • 様々な技術領域での発表があったので、ノウハウ共有の機会となった

改善したいこと

  • 人数が増えてきたので、最終発表の時間が伸びてしまう
    • チーム開発にする等の工夫をしていきたい
  • 場所やメンバーによっては、wi-fi の速度が遅くなることがあった

さいごに

開発合宿も3回目の開催となりましたが、改めて技術への投資としてサポートをしてくれるエンジニア以外のメンバーに感謝ですね。今回の合宿で得た経験をプロダクト開発に活かして、さらに事業を加速させていきたいと思いました。

1泊2日で普段とは違った場所で開発に集中できることで良い成長に機会になったのと、普段業務だと関わらないメンバーと顔合わせて時間を過ごすことで結束力も高まったと思います。

ということで、Zealsでは文字通り老若男女様々な個性のあるメンバーが集まり、チーム一丸となって開発していることがご理解いただけたかと思います(伝わってなければすいません)。一緒に日本をぶち上げたい、熱い思いを持っているエンジニアの方お待ちしておりマウス

hrmos.co

続きを読む

Redash 分析環境のGKE移設&ver.3から7へのアップデート手順を公開

f:id:zeals-engineer:20191114231808p:plain

こんにちは、分析基盤を担当している鍵本です。
本日は Redash サーバを GKE に移設して序にバージョンアップまでしちゃいました というお話をしようと思います。

  • 背景
  • 移設時のポイント
    • GCP リソース作成のコード化
    • バージョン管理をしやすくするための GKE 化
    • SSL化
  • 移設作業の詳細
    • GCP リソース作成
    • GKE 環境への Redash のデプロイ
      • マニフェストファイルの作成
      • デプロイ
    • データのリストア
      • 現行サーバのデータ取得
      • ローカルの Docker 環境での起動
      • Docker 環境へのリストア
      • バージョン 4 へのアップグレード
      • バージョン 5 へのアップグレード
      • バージョン 7 へのアップグレード
      • 本番環境へのデータ移行
  • 移行後に発覚した問題
    • 日本語での検索
    • クエリの進捗状況
  • まとめ
  • 最後に

背景

Zealsでは、ユーザーの行動ログなどに紐付いた日々のKPIを確認するために Redash を、BizDevのメンバー中心に利用しております。
弊社サービス『Zeals』 は元々Microsoft Azure 上で運用していたため、Redashも当然ながらAzure上に仮想マシンを立てて構築していました。導入は1年半ほど前のことで、バージョンはなんと 3 でした。とても古いですね。

tech.zeals.co.jp

その後ZealsをGCPに移行したことから、「Redash も移行しよう」「序でだからバージョンアップもしよう」という話になりました。
実際の移行作業は2019年10月11日に完了しております。このとき安定版最新バージョンが 7 だったので、一気に 4 ランクアップさせました。

github.com

移設時のポイント

今回の移設作業で注目すべき点は以下の通りです。

  • GCP リソース作成のコード化
  • バージョン管理をしやすくするための GKE 化
  • SSL化

GCP リソース作成のコード化

VPCネットワーク、静的外部IPアドレス、GKEクラスタといった GCP のリソースは一度作った後に触る機会はさほど多くありませんが、冪等性を担保するためにそれらをコード管理するようにしました。

qiita.com

バージョン管理をしやすくするための GKE 化

Redash バージョン4 まではオンプレのサーバーか仮想マシンにインストールすることを想定したスクリプトが用意されていましたが、その後は Docker 上に構築することを前提としたスクリプトに置き換わりましたので、GKE に構築するのが適切ではないかと考えました。

SSL化

Zealsでは、これまでHTTP通信で接続しておりました。KPIに利用するデータの内容はマスキングされたものを利用しているとはいえ、この状態は非常によくありません。そのため、今回のバージョンアップから HTTPS のみのアクセスに変更しております。

移設作業の詳細

GCP リソース作成

以下のリソースを構成管理ツール Pulumi を使って行いました。

  • VPCネットワーク
  • サブネット
  • 静的外部IPアドレス
  • GKEクラスタ
  • DNS (Redash 用の A レコード登録のみ)
  • Cloud Armor

Pulumi のコードについては別の機会にご紹介しようと思います。

www.pulumi.com

GKE 環境への Redash のデプロイ

Redash バージョン 7 の環境を以下の手順に従って GKE に構築します。

マニフェストファイルの作成

スクラッチからマニフェストファイルを作成するのは大変なので、 kompose というツールを使って docker-compose.yml から作成しました。

github.com

バージョン 7 の場合には setup/docker-compose.yml がありますので、まずはこれを用いてローカルのDocker 環境で起動できるように修正しておきます。次にこの docker-conmpose.yml を作業ディレクトリにコピーし、

kompose convert

を実行すると、必要なマニフェストファイルが生成されます。

これをもとにして必要な修正を加えることで、マニフェストファイルの作成コストを最小限にしています。主な修正箇所は以下の通りです。

  • 環境変数を ConfigMap に移動
  • 必要なリソースの定義
  • nginx 用のマニフェストの削除
  • Ingress の追加
  • ManagedCertificate の追加
  • cloud_sql_proxy をサイドカーとして起動する設定を追加
  • cloud_sql_proxy 用 Secret の追加

たとえば IngressManagedCertificate のマニフェストを紹介すると以下のようになります。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress
  namespace: analytics
  annotations:
    kubernetes.io/ingress.global-static-ip-name: [LB用静的外部IP名]
    networking.gke.io/managed-certificates: [ManagedCertificate名]
spec:
  rules:
  - host: [Redash用URL]
    http:
      paths:
      - backend:
          serviceName: [Redashサービス名]
          servicePort: 5000
apiVersion: networking.gke.io/v1beta1
kind: ManagedCertificate
metadata:
  name: [ManagedCertificate名]
spec:
  domains:
  - [Redash用URL]

tech.zeals.co.jp

デプロイ

cloud_sql_proxy 用 Secret ファイルの data には Cloud KMS を利用して暗号化された credential 情報が入っています。このままでは Secret として登録できないので、デプロイ前に復号化しておきます。

kubesec decrypt -i redash/secrets/cloudsql-client-service-account.yaml

あとはマニフェストを適用するだけとなります。

kustomize build redash/overlays/prd | kubectl apply -f -

ここで redash/overlays/prd には production 環境用の kustomization.yaml が配置されており、その中の resources に必要なマニフェストファイルが読み込まれるよう定義されております。

データのリストア

Redash ではバージョンアップをした時にデータを整合的にするためのツールとして manage コマンドを用意してくれています。しかし残念ながらバージョン 3 から 7 へは一気に変換することができませんでした。冷静に考えたら無茶苦茶な話ですよね。そこで以下のように順を追ってやっていくことになりました。

現行サーバのデータ取得

pg_dump コマンドを使って Redash データベースのダンプを取ります。

sudo -u postgres pg_dump redash | gzip -c > redash.dump.gz

ローカルの Docker 環境での起動

ローカルの Docker 環境にバージョン3の環境を作ります。

mkdir -p ~/workspace
cd workspace
git clone -b v3.0.0 https://github.com/getredash/redash.git
cd redash
vi docker-compose.production.yml
diff --git a/docker-compose.production.yml b/docker-compose.production.yml
index f0b9812d..f1b746cd 100644
--- a/docker-compose.production.yml
+++ b/docker-compose.production.yml
@@ -7,7 +7,7 @@
 version: '2'
 services:
   server:
-    image: redash/redash:latest
+    image: redash/redash:3.0.0.b3147
     command: server
     depends_on:
       - postgres
@@ -23,7 +23,7 @@ services:
       REDASH_WEB_WORKERS: 4
     restart: always
   worker:
-    image: redash/redash:latest
+    image: redash/redash:3.0.0.b3147
     command: scheduler
     environment:
       PYTHONUNBUFFERED: 0
@@ -38,8 +38,8 @@ services:
     restart: always
   postgres:
     image: postgres:9.5.6-alpine
-    # volumes:
-    #   - /opt/postgres-data:/var/lib/postgresql/data
+    volumes:
+      - /home/kagimoto/workspace/postgres-data:/var/lib/postgresql/data
     restart: always
   nginx:
     image: redash/nginx:latest
mkdir -p /home/kagimoto/workspace/postgres-data
docker-compose -f docker-compose.production.yml run --rm server create_db
docker-compose -f docker-compose.production.yml up -d

http://localhost/ にアクセスして、Redashのログイン画面が出ることを確認します。

Docker 環境へのリストア

既存のスキーマを一旦削除します。

docker exec -i redash_postgres_1 psql -U postgres -c 'drop schema public cascade; create schema public' 

次に Redash データベースを作成します。

docker exec -it redash_postgres_1 bash
su - postgres
createuser redash --no-superuser --no-createdb --no-createrole
createdb redash --owner=redash
exit
exit

先程取得したデータをリストアします。

zcat ~/tmp/redash.dump.gz | docker exec -i redash_postgres_1 psql -U postgres redash

docker-compose.yml を編集して、現行に合った環境変数を設定し再起動します。

docker stop $(docker ps -q)
docker-compose -f docker-compose.production.yml up -d

RedashのURL を /etc/hosts に 127.0.0.1 として登録し、http://[RedashのURL]/ にアクセスして現行と同じものが表示されることを確認します。

バージョン 4 へのアップグレード

まずローカルリポジトリをアップグレードします。

docker stop $(docker ps -q)
git checkout -b v4.0.0 refs/tags/v4.0.0
vi docker-compose.production.yml

イメージのバージョンを4に変更します(差分はオリジナルからのもので、上記からの追加差分ではありませんのでご注意ください)。

diff --git a/docker-compose.production.yml b/docker-compose.production.yml
index f0b9812d..8358b5f4 100644
--- a/docker-compose.production.yml
+++ b/docker-compose.production.yml
@@ -7,7 +7,7 @@
 version: '2'
 services:
   server:
-    image: redash/redash:latest
+    image: redash/redash:4.0.1.b4038
     command: server
     depends_on:
       - postgres

manage コマンドでデータベースの修正をしてコンテナを起動します。

docker-compose -f docker-compose.production.yml run --rm server manage db upgrade
docker-compose -f docker-compose.production.yml up -d

http://[RedashのURL]/ にアクセスして画面下部に Redash 4.0.1+b4038 と表示され、バージョンが上がっていることを確認します。

バージョン 5 へのアップグレード

同様にローカルリポジトリをアップグレードします。

docker stop $(docker ps -q)
git checkout -b v5.0.0 refs/tags/v5.0.0
vi docker-compose.production.yml

イメージのバージョンを 5 に変更します(差分はオリジナルからのもので、上記からの追加差分ではありませんのでご注意ください)。

diff --git a/docker-compose.production.yml b/docker-compose.production.yml
index f0b9812d..8358b5f4 100644
--- a/docker-compose.production.yml
+++ b/docker-compose.production.yml
@@ -7,7 +7,7 @@
 version: '2'
 services:
   server:
-    image: redash/redash:latest
+    image: redash/redash:5.0.0.b4754
     command: server
     depends_on:
       - postgres

manage コマンドでデータベースの修正をしてコンテナを起動します。

docker-compose -f docker-compose.production.yml run --rm server manage db upgrade
docker-compose -f docker-compose.production.yml up -d

http://[RedashのURL]/ にアクセスして画面下部に Redash 5.0.0+b4754 と表示されていることを確認します。

バージョン 7 へのアップグレード

docker-compose.production.yml を退避して、ローカルレポジトリをアップグレードします。

cp docker-compose.production.yml ../
git checkout docker-compose.production.yml
docker stop $(docker ps -q)
git checkout -b master remotes/origin/master
vi setup/docker-compose.yml

※ 現在は master がバージョン 8 になっているので、v7.0.0 のタグを checkout する必要があります。

diff --git a/setup/docker-compose.yml b/setup/docker-compose.yml
index aea6369b..832b5e45 100644
--- a/setup/docker-compose.yml
+++ b/setup/docker-compose.yml
@@ -4,7 +4,7 @@ x-redash-service: &redash-service
   depends_on:
     - postgres
     - redis
-  env_file: /opt/redash/env
+  env_file: /home/kagimoto/workspace/redash/env
   restart: always
 services:
   server:
@@ -36,10 +36,10 @@ services:
     image: redis:5.0-alpine
     restart: always
   postgres:
-    image: postgres:9.5-alpine
-    env_file: /opt/redash/env
+    image: postgres:9.5.6-alpine
+    env_file: /home/kagimoto/workspace/redash/env
     volumes:
-      - /opt/redash/postgres-data:/var/lib/postgresql/data
+      - /home/kagimoto/workspace/postgres-data:/var/lib/postgresql/data
     restart: always
   nginx:
     image: redash/nginx:latest

env ファイルに必要な環境変数を全て定義しておきます。その後 manage コマンドでデータベースの修正をしてコンテナを起動します。

docker-compose -f setup/docker-compose.yml run --rm server manage db upgrade
docker-compose -f setup/docker-compose.yml up -d

http://[RedashのURL]/ にアクセスして右上のアカウント名をクリックして、メニューの一番下に Version: 7.0.0+b18042 と表示されていることを確認します。

本番環境へのデータ移行

ローカル環境の PostgreSQL サーバから Redash データベースのダンプを取得し、一部内容を修正します。

docker exec -i setup_postgres_1 pg_dump -U postgres redash > redash.dump
sed -e s/redash$/postgres/ -e 's/redash;$/postgres;/' < redash.dump > redash_v7.dump

これは Docker 環境を前提とするバージョン 5 からは、postgres データベースに Redash 用のテーブルを作成し、それに対して postgres ユーザでアクセスするように変更されたからです。上記はダンプデータに含まれているテーブルの所有者情報等を sed で書き換えているというわけです。

本番にリストアします。

kubectl exec -it --namespace=analytics $(basename $(kubectl get pod -o name -n analytics -l 'io.kompose.service==postgres')) -- psql -U postgres -c 'drop schema public cascade; create schema public'
cat redash_v7.dump | kubectl exec -it --namespace=analytics $(basename $(kubectl get pod -o name -n analytics -l 'io.kompose.service==postgres')) -- psql -U postgres postgres

/etc/hosts に登録した RedashのURL を削除し、 http://[RedashのURL]/ にアクセスして、先程ローカル環境で確認した状態と同じものが見えることを確認します。

移行後に発覚した問題

日本語での検索

バージョン 7 に移行してみたところ、検索窓でクエリやダッシュボードを日本語で検索しても想定した検索結果が得られないことがわかりました。これはバージョン 8 で修正されたようです。この記事を執筆中に正式リリースされたようなので、すぐにアップデートしようと思います。

クエリの進捗状況

バージョン 3 の場合には、誰の実行した adhoc クエリが実行中で、誰のが待ち状態なのかが管理画面からわかったのですが、どのバージョンからかはわかりませんが、そのような情報が得られなくなったようで管理者としては不便な感じを受けております。もしかしたら何か手立てがあるのかもしれません。ご存じの方がいらっしゃったら教えて下さい...!!

まとめ

今回は Redash を GKE 環境に移行し、バージョンアップしたというお話を紹介しました。定期的にバージョンアップをしていかないと、あとで苦労する ことを思い知らされた感じがします。でも GKE に移行したので、今後は気軽にバージョンアップできるんじゃないかと(勝手に)期待しています。

続きを読む

gRPC(protobuf)をモノリシックなRailsアプリケーションに導入する

f:id:zeals-engineer:20191112233421p:plain

こんにちは!
Railsエンジニアをやっているtakakudaです。

今回のエントリーでは、Zeals(旧fanp)サービスへのgRPCを導入を紹介できればと思います。
目次は以下のようになります。

  • そもそもgRPCとは
  • 導入経緯
  • 導入
    • proto fileにデータ構造を定義する
    • protocコマンドを用いてRubyコードを生成する
    • 生成されたRubyコードをlib配下に置き、config/initializers配下でStubを作成する
    • 共通化したいメソッド呼び出し部分をprotobufで生成されたメソッドに書き換える
  • Railsへ導入時にハマったこと
  • gRPCを導入してみて
  • 最後に

そもそもgRPCとは

gRPCはRPCを実現するためにGoogleが開発したプロトコルの1つで、インターフェイス定義言語のもとになるメッセージ交換形式としてProtocol Buffersを利用できます。gRPC上のアプリケーションでは、別マシン上にあるアプリケーションのメソッドをローカルオブジェクトのように直接呼び出すことができ、分散アプリケーションおよびサービスの作成を簡単にできます。

f:id:takakudakei:20191108193526p:plain

https://www.grpc.io/docs/guides/

導入経緯

簡単にZealsの機能紹介をすると、サービスは大きく分けて以下の3つから構成されています。

[1] チャットボットの管理用Webアプリケーション

[2] チャットボットへのメッセージ送受信サービス

[3] 1と2から共通処理を切り出したマイクロサービス群

もっと詳しく知りたい方は、以前公開しました以下の記事をご覧ください。

tech.zeals.co.jp

今回は、上記の構成の中でも[3]にあたる、「チャットボット管理用のWebアプリケーション」と「チャットボットへのメッセージ送受信サービス」で共通して発生する処理をAPIとして切り出すために利用したgRPCをテーマとしています。

2つのサービスで共通する処理をサービスごとにRuby, Pythonと異なる開発言語で実装してるため、

  • 機能追加や修正の際に、それぞれのアプリに同じ実装を行わなければならない
  • 挙動に差異があった場合、その差異がどちらの実装のバグによるものかを特定するのに時間がかかる

といった問題が発生しており、サービス全体見た場合のメンテナンスコストが高くなってきたため、共通処理部分をGoのAPIとして切り出すことを決定しました。

導入

以下では、実際にZealsで使用している「メッセージの配信対象となるユーザーをフィルタリングする処理」を例に解説していきます。

またprotobufによって生成されたコードについてですが、今回はRailsに導入する部分について紹介したいのでサーバー側の実装については触れず、クライアント側のコードについて紹介させていただきます。
導入手順としては以下のようになります。

  1. proto fileにデータ構造を定義する
  2. protocコマンドを用いてRubyコードを生成する
  3. 生成されたRubyコードをlib配下に置き、config/initializers配下でStubを作成する
  4. 共通化したいメソッド呼び出し部分をprotobufで生成されたメソッドに書き換える

proto fileにデータ構造を定義する

まずは.protoという拡張子のファイルで、

  • Service:RPCサービスのインターフェイスを定義することができる 
  • Message:メッセージのフォーマットを定義することができる

をそれぞれ書いていきます。

/proto/cosmos.proto

syntax = "proto3";


import "entities/common.proto";
import "entities/entities.proto";
import "entities/message.proto";

// CosmosService is microservices composed of a suite of small and lightweight services.
service CosmosService {
  // Filter filters a list of end users related with a given chatbot ID by using a given filter ID.
  // End users is filtered by the following conditions.
  // 1. Whether the attribute ID of the end user matches filter's one.
  // 2. Whether inflow date and time of the end user matches filter's one.
  // 3. Whether the chatbot has permission to send a message to the end user.
  rpc Filter(FilterRequest) returns (stream e.EndUser) {}
}

// FilterRequest
message FilterRequest {
  // The ID of the chatbot that will be used to collect end users related with it. And then the set of end users will be filtered.
  int64 chatbot_id = 1;
  // The ID of the filter to be used to filter a set of end users.
  int64 filter_id = 2;
}

syntaxは2019年11月現時点で主流versionである3を指定します。

簡単に説明するとCosmosServiceはFilterRequestという型を受け取ると、FilterRequestの条件によってフィルタリングされたユーザーをresponseとして返すということを定義しています。

developers.google.com

protocコマンドを用いてRubyコードを生成する

gem install grpc-tools

Protocol Buffersの定義から自動でコードを生成するのに必要なツールをinstallします。

https://rubygems.org/gems/grpc-tools/versions/1.25.0

grpc_tools_ruby_protoc -I /proto \--ruby_out=lib --grpc_out=lib ../proto/cosmos.proto

コマンドを実行することでProtocol Buffersの定義をもとにRubyコードが生成されます。

https://grpc.io/docs/tutorials/basic/ruby/

生成されたRubyコードをlib配下に置き、config/initializers配下でStubを作成する

自動生成されたコードはlib配下に置きます。

Protocol Buffersから生成されたコードを実行するためにはStubを作成する必要があるので、 config/initializer にStubを作成する設定fileを作成します。

stub = CosmosService::Service.rpc_stub_class.new(localhost:38080, :this_channel_is_insecure)

共通化したいメソッド呼び出し部分をprotobufで生成されたメソッドに書き換える

生成されたコードを、Rubyで実装しているチャットボット管理画面のコードへ組み込みます。

# requestを用意して
req = FilterRequest.new(chatbot_id: chatbot_id, filter_id: filter_id)
# Stubを使ってサーバー側のメソッドを呼び出します
end_users = stub.filter(req)
# ストリーミングで返ってくるので1つずつ取り出し、end_userのidの配列として返します
end_users.map(&:id)

Railsへ導入時にハマったこと

Rails側での実装を終え、いざステージング環境にて動作確認します。 動作確認の際に、gRPCのdependencyであるgoogle-protobufをビルド済み共有ライブラリとしてインストールすると、正常に動作しないという問題にぶつかりました。

その際の環境は、以下の通りです。

  • ruby:2.5.5-alpine
  • grpc (1.23.0)

ここが僕が大きくハマったポイントです…。
Zealsではkubernetesを本番環境で運用しており、その際に利用するDockerのimageとしてAlpine Linuxを使用していました。

この問題を調査すると、どうやらalpineのimageでprotobufを利用しようとすると発生している問題らしく、本家のprotocolbuffers/protobufでもissueが上がっていました。Alpine LinuxだとRubyライブラリに不足があり、ld-linux-x86-64.soがないというエラーです。

github.com

対応としては上記のissueで議論されている内容を参考にしました。

具体的には、gRPCのdependencyであるgoogle-protobuf を BUNDLE_FORCE_RUBY_PLATFORM=1 でソースからビルドするworkaroundを取るように、以下のようにDockerfileを編集しました。

mv Gemfile Gemfile.orig && \
sed -e /grpc/d Gemfile.orig > Gemfile && \
bundle install && \
cp Gemfile.orig Gemfile && \
BUNDLE_FORCE_RUBY_PLATFORM=1 bundle install

やっていることは、最初のbundle installではgRPCを省いた状態で実行し、2回目のbundle install時にgRPCのみで実行できるようにしています。 一見二度手間のようなことをやっていますが、目的はbundle installでかかる時間を少しでも短くするためです。

gRPCを導入してみて

  • 機能追加や修正の際に、それぞれのアプリに同じ実装を行わなければならない
  • 挙動に差異があった場合、その差異がどちらの実装のバグによるものかを特定するのに時間がかかる

この問題が解決し、今では修正もデバックも容易になりました。

現在は上記のイチ機能しかまだ切り出せていないのですが、これを機に巨大なモノリシックになりつつあるRailsアプリを切り出していき、メンテナンス性や拡張性をマネジメントしやすいプロダクトに成長させていきます。

続きを読む

Kubernetes Meetup Tokyo#24で「ZealsでのCI/CDパイプライン構築事例」について登壇しました!

https://connpass-tokyo.s3.amazonaws.com/thumbs/16/f1/16f13ae16413921089f49e9bb5087352.png

こんにちは!
普段はGolangでMicroservicesを開発しているぱんでぃーです!

今回のエントリーでは『Kubernetes Meetup Tokyo #24』でLT枠として登壇してきたレポートをお届けします!

k8sjp.connpass.com

続きを読む