Zeals TECH BLOG

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

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

続きを読む

Rails環境でセキュリティ向上のため、Brakeman gemを導入&脆弱性対策を実施しました

Brakemanのロゴ

こんにちは、ZealsでバックエンドエンジニアとしてRailsを使って開発をしている鈴木です。
Zealsでは、セキュリティ対策の一環としてBrakemanを使用しております。

今回はなぜ、Brakemanを導入することに至ったかの理由とBrakemanの導入、実際に活用してうまくいったノウハウを紹介させていただきます。
Railsのセキュリティ対策について調べている方や、Brakemanをどのように活用していくか調べている方の参考になれば幸いです。

  • そもそもBrakemanとは
  • Brakemanを導入した経緯
  • Brakeman活用までの流れ
    • 1.Brakemanの実行結果をエクスポートし、脆弱性の調査および相談
    • 2.脆弱性についての調査
    • 3.脆弱性の対応についてチームで方針を決定
    • 4 話した優先度や対応方針に従って修正
  • 今後やっていきたいこと
  • さいごに

そもそもBrakemanとは

Brakemanとはソースコードに、SQLインジェクションなどの脆弱性がないかを解析してくれるライブラリです。 SQLインジェクションなどの有名な脆弱性だけでなく、解析してくれる項目が20種類以上もあります。

github.com

これだけ解析をしてくれるため、URLを設定する際に危険なサイトのURLを登録できてしまう脆弱性に気づくことができました。現在のサービスの運営上では問題になっていませんが、将来的に大きなインシデントとなる可能性があります。

もちろん、 コードレビューだけでは気づけない箇所も指摘してくれますので、セキュリティに強いアプリケーションを開発していく上では必須のツールです。

実行についても、以下のようにコマンド一発で解析してくれるため非常に簡単です。

brakeman

実行すると、以下のように解析した結果を表示してくれます。

もちろん、Brakemanは他のGemと同様に簡単にインストールできます。
グローバルにGemをインストールする場合は、以下のコマンドを実行してください。

gem install brakeman

Gemfile にを用いる場合は以下のように記述してください。

group :development do
  gem 'brakeman', :require => false
end

Brakemanを導入した経緯

Zealsはクライアントである企業様の大切な情報を扱うアプリケーションです。 そのため、セキュリティ対策を万全にする必要があります。

今後より多くの機能を追加し事業を成長させる上で、セキュリティ強化は重要なテーマとなります。 そうなった場合に、数万行のコードをエンジニアがすべて解析することは、現実的ではありません。

そういった脆弱性の診断を効率的にするために、今回Brakemanを導入しました。

Brakeman活用までの流れ

1.Brakemanの実行結果をエクスポートし、脆弱性の調査および相談

最初のフローとして、Brakemanを用いた脆弱性の調査、相談をしていきます。

実行結果を見るために、その都度コマンドラインで実行するしていくのは非効率的です。
Brakemanには解析した結果を、HTMLなどにエクスポートしてくれる便利機能があります。

解析結果をエクスポートするためには、以下のコマンドを実行するだけOKです。

brakeman -o brakeman.html

Zealsでは、エクスポートされたHTMLを資料として、脆弱性の調査や相談を社内外のエンジニアと共同で行いました。

脆弱性を調査した箇所の一例としては、冒頭に説明した「URLを設定するところで危険なサイトのURLを登録できてしまう」という脆弱性です。こちらに関しては、コードレビューの時点では気づくことができませんでした。

詳細な内容としては、「アプリケーションの公開範囲が将来toC向けに広がった場合、不正なデータを登録される可能性がある」というものです。

エクスポートしたHTMLをブラウザで開くと、このような画面が表示されます。

ここでの指摘内容の例として、以下のようにBrakemanが指摘してくれています。

  • RemoteFollowControllerのcreateメソッドで、ユーザーが入力した値をもとにリダイレクトする可能性がある
  • そのため、オープンリダイレクトにつながってしまう可能性がある

tooljp.com

2.脆弱性についての調査

Brakemanは、現在は脆弱性でなくとも、将来的に脆弱性となりえる箇所まで指摘してくれます。

脆弱性と指摘されたコードについても、何かの理由があってそのような実装になっている場合もありますので、その背景も調査してドキュメントに残しておくと、後から振り返ることができ非常に便利です。

ドキュメントがあることで第三者への脆弱性報告の資料として使えたり、実装方針について相談する資料にもなり、とても役に立ちました。

3.脆弱性の対応についてチームで方針を決定

調査した脆弱性ごとに対応方法を選択し、修正していく方針となりました。

しかし、すぐに修正可能な方法を採用すると、処理に時間がかかりサービスが使いづらくなる脆弱性対策が存在します。 そのため、「脆弱性対策として処理時間をどれぐらいかけて良いか」をもとに対応方法を考え、チーム内で相談と調査をしていきました。

先に調査を行ったことで、脆弱性対策によって処理が遅くなることが事前にわかり、実装の手戻りを防いだり、工数の見積を正確にした上で対策を進めることができたという点が非常に良かったですね。

4 話した優先度や対応方針に従って修正

チーム内で決定した優先度や対応方針に従って、プロダクトコードに修正を加えます。

Brakemanは、アプリケーション内でのユーザー入力の操作内容をもとに脆弱性を判断しているため、問題ない仕様でも指摘される場合があります。
そのような場合は、Brakemanの指摘を無視するコマンドを実行します。

以下のコマンドを実行すると、警告を無視する設定が可能です。

brakeman -I 

上記のコマンドを実行すると、以下の実行画面が表示されます。

最初に、警告を無視するignore設定をするファイル名を何にするか質問されます。

特にこだわりがなければ、デフォルトのままで問題ないのでEnterを押します。
次に、どの対話形式で進めていくかを確認されますが、最初は全件対応で問題ないので1 を入力してください。

さらに、挙げられた脆弱性ごとに、内容を無視するかを確認されます。
主に実行するコマンドは以下のコマンドです。

  • n :ignoreをする際、ノートにignoreする理由を書くことが可能です
    • iでもignore可能ですが、あとから見返すとignoreした理由が不明になるため、nの使用を推奨します
  • s:何もせずにスキップします
  • u:ignoreを取り消します

今後やっていきたいこと

CIを使ってBrakemanを実行していきます(Zealsでは、CIにCircle CIを利用しています)。

当然ですが、一度Brakemanの対応が終わった後にも、新しい脆弱性が生まれる可能性があります。

新しい脆弱性が生まれないようにするため、CIツールでBrakemanを実行し継続的に脆弱性を確認したほうが良いでしょう。
Zealsでは今後、初動の脆弱性対応の完了後、 CircleCIを用いたBrakeman運用をしていく予定です。

circleci.com

続きを読む