RareJob Tech Blog

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

USM(User Story Mapping)をやってみた

はじめに

お久しぶりです。改善したいマンの三上です。
世間は新型ウィルスで混乱していますが、レアジョブのブログは元気に更新中です!
私事ですが、少し前に丸2日間の研修を経てLSM(Licensed Scrum Master)の資格を取得しました。

レアジョブではアジャイル開発を推進しており、各チームやプロジェクトで積極的に採用されています。
そんな中で弊社のメインプロダクトである「レアジョブ英会話」のリプレース案件を実施することになりました。
この超大型案件を進めるにあたり、まず始めに何から始めたらいいのか考えていく中で
まずはプロトタイプ開発をしよう!スクラムでやってみよう!
という運びになり、まずはユーザーストーリーマッピング(USM)をやってみたので
今回はその記事を書こうと思います。

USMとは?

スクラム開発においてプロダクトの価値を決めるのはプロダクトバックログです。
プロダクトバックログはいくつかのPBI(プロダクトバックログアイテム)が優先度の高いものから順番で並んでいます。
PBIはユーザーストーリー形式になっていて、誰にとって、どんな価値があるのか表現されている必要があります。

ユーザーストーリーは以下の形式にします。

WHO = ○○として
WANT = △△したい
WHY = なぜなら✗✗だからだ

実際に例を上げると、

WHO = レアジョブ英会話の会員として
WANT = 日本語も話せる講師とレッスンしたい
WHY = なぜなら分からない時に日本語で質問したいからだ

のように書きます。
上記は例ですが、ユーザーストーリーの粒度は大小様々になりえます。
このようなユーザーストーリーを優先度が高く直近のスプリントで着手するものほど、より詳細なストーリーにしていく必要があります。

(ちなみにストーリーの詳細化・見積もりの事をリファインメントと呼びます。)

USMは上記のようないくつかのユーザーストーリーで構成されます。
そのユーザーストーリーをナラティブフローと呼ばれる物語の流れに沿って左から右へ時系列順に並べていきます。

USMを作成するメリットはプロダクトの全体像を俯瞰して見れることです。
個人的に、この複数のサブストーリーを経て、全体としてのストーリーが出来上がる感じが
RPG感があって楽しいです。笑

というわけでやってみた

f:id:rarejobmikami:20200323024258j:plain

ざっくり以下のような形で進めました。

Timebox

3時間

Agenda

  1. 目的説明
  2. ユーザーストーリーマッピング説明
  3. 現状の機能概要のおさらい
  4. ペルソナの共有
  5. ユーザーのアクションを書き出す(全員)
     時系列に並べていく。
     同じ目的はグループにまとめる。
     注:ストーリーとして書く。〇〇として△△したい
     注:ストーリーをタスクに分解してはならない
     「青い付箋」にユーザーのアクションを書き出す
     「オレンジ付箋」にアクションのグループをまとめる
  6. 改善ポイントや欲しい機能を書き出す
     「赤い付箋に」ユーザーの気持ちになって欲しい機能を書き出す
  7. 整理(優先順に並び替える)
  8. 最優先で作りたいものを決める(MVPの確定)
     MVP(Minimum Viable Product)とは、まず最初にリリースしたいものになります。
     今回はMUST(必ず必要)、WANT(出来れば欲しい)の2つに分類し、MUSTをMVPとします。
     ※余裕あれば次に達成したいことを決める
  9. プロダクトバックログに組み込む

参加者は開発チームはもちろんPO(プロダクトオーナー)に当たる企画チーム、CTOまでバラエティに富んだメンバーで行いました。

完成したUSMはこちら

f:id:rarejobmikami:20200326204520j:plain

オレンジ色の付箋がエピックと呼ばれる単位になります。
例としては「ユーザー登録」、「検索」、「レッスン予約」のような粒度です。

また上部に貼ってある付箋ほど優先度が高いものになります。

やってみて良かったこと

大きく以下の2つ。

・ペルソナを明確にした
・プロトタイプ開発のスコープが明確だった

です。これは企画チームに感謝です。

上記の写真でプロジェクターに映し出しているのが、
レアジョブ英会話の重要ペルソナを言語化したものになります。
ここが明確だと何が良いかというと、ユーザーストーリーを考えるにあたって
最も重要なのがユーザー視点に立つことです。

これは簡単なようで難しいです。なぜなら人それぞれ求めることが違うので当然意見が分かれるときがあります。
またエンジニアあるあるだと思うのですが、
パッと要件を聞いた時に、すぐ実装方法を考えてデータ構造とかアーキテクチャとかを想像しちゃうと思うんです。

それはそれで重要なんですが、今回の場では不要です!

そんな時に役に立つのがペルソナの存在です。
意見が別れたり悩んだときには、ペルソナの気持ちになってその都度振り返ることが出来て、円滑に議論を進めることが出来ます。

また、プロトタイプ開発のスコープが明確だったことにより、どこまでやる・やらないの判断にあまり時間を取られずに進行することが出来ました。

最後に

このUSMを経て、プロダクトバックログを作成し、無事にスプリントが開始することができました。
またスプリント開始時にやったことについても次の記事で書きたいと思います。

それでは、また。

マイクロサービスのロギングベストプラクティスとGoの実装の場合

こんにちは、プラットフォームチームの池田と申します。初投稿です。

プラットフォームチームではマイクロサービスアーキテクチャの構成を採用し開発を進めています。
どんな構成でも忘れてはいけないのがロギング。いわゆる非機能要件の1つで地味な存在ですが、サービス運用を支える上で非常に重要です。

直近でマイクロサービスにおけるロギングの構成を調査し、プラットフォームチームでメインで採用しているGo言語での実装を検証しました。
今回の記事ではそのまとめを紹介します。

目次

ロギングベストプラクティス for マイクロサービス

マイクロサービスにおけるロギングの方針に関して記載している日本語の記事が少なく感じたので、はじめに調べた結果のまとめを記載します。

リクエストにユニークなIDを付与し紐付けができるようにする

マイクロサービスでは、あるサービスAがサービスBを呼びさらにサービスCを呼ぶといった形になるので、呼び出しチェーンにユニークなIDを与えることで調査の見通しが良くなります。

このとき、アプリケーションがHTTPレスポンスなどでエラーを返す場合にもリクエストのユニークIDを入れると良いようです。そうすることで、問題が発生し際ユーザが受け取ったエラーとユニークIDを素早く紐付けて調査を開始することができます。

ユニークなIDをどこでどのように生成かということも重要なポイントでしょう。原則としてはユニークIDでの追跡範囲のエントリーポイントとなる箇所で生成します。そしてそういった箇所で利用されるロードバランサのサービス(AWSではELB)やKongなどのAPI Gatewayミドルウェアプラグイン的にCorrelation ID(ユニークID)生成の機能を提供しているので、それを利用するのが一般的なようです。そういった機能では、生成したユニークIDをオリジナルのHTTPヘッダーへ挿入します。

ログは一箇所に集める

上述での各サービスが出力するユニークID付きのログを横断的に調査するために、各サービスのログを一箇所に集中させることが次に重要になります。

このとき、アプリケーションがPush型として能動的にHTTPリクエストなどを使って集約場所へ登録するのではなく、ローカルストレージのファイルやAmazon Elastic File Systemといったクラウドのストレージに一旦預けた後に、LogstashFluentdといったツールで集約場所へ連携することが望ましいとのことです。そうすることで、アプリケーションからログ集約という役割を切り離すことができます。

以下の図はここまでの2つのポイントを踏まえた構成の一例です。Amazon ELBがCorrelation ID(ユニークID)を生成しマイクロサービスサービス間で伝搬され、出力されたログはCloudWatch Logs Subscriptionの機能でElasticsearch Serviceへと集約させています。

f:id:ochataro:20200316183408p:plain

ログデータを構造化する

マイクロサービスではログに持たせるフィールドは柔軟にしておきたい一方で、サービス共通でロギングデータのパースができるようにもしておきたいです。

そこでサービス共通でログデータのフォーマットを合わせましょう。JSONやLTSVといった構造化の形式を統一させることで持たせるフィールドも柔軟になり、共通で必須なフィールドを容易にパースすることができます。

ログに有益な情報を持たせる

マイクロサービスアーキテクチャにおいてログ情報として持つことが望ましいフィールドが以下になります。

どのサービスでも共通で持つのが望ましいフィールド

リクエストのエントリーポイントとなるサービスで持つのが望ましいフィールド

  • リクエスト元のIPアドレス
  • ユーザが利用したブラウザ名またはモバイルのOS名
  • HTTPレスポンスコード

Go言語での実装例

上述のベストプラクティスの内容を踏まえて、Go言語の場合の共通ロギングライブラリとアプリケーションのサンプルを作っていきます。

はじめに、重要なのは具体的にどういったフィールドやフォーマット、出力先にするかをチーム間で議論し合意した上でそのルールを決めることでしょう。

ここでは、ロギングルールを決定できているという前提で進めていきます。

今回の記事ではソースコードすべて載せられていませんが、後述のサンプルAPIサーバを実行し、下記のようにユニークIDを想定したHTTPヘッダー付きでリクエストすると、

curl -H 'X-Transaction-ID:00A-abcdef-99B' http://service-hostname/v2/sample

APIサーバから以下のようなJSON形式のログが標準出力へ出力されます。(フォーマッティングは後付しました。)

{
   "level":"error",
   "msg":"SearchSample panic!",
   "request-id":"00A-abcdef-99B",
   "service-name":"MicroService01",
   "stack":"goroutine 6 [running]...スタックトレースが続く...",
   "time":"2020-03-15T23:39:58+09:00"
}

マイクロサービス共通で利用するロギングライブラリ

上述したマイクロサービスのロギングの共通ルールは共通ライブラリとしてサービス(チーム)間で共有されるべきです。

package logger

import (
    "github.com/sirupsen/logrus"
)

const (
    // XTransactionID はユニークIDのためのHTTPヘッダーのキー名です。
    XTransactionID = "X-Transaction-ID"
)

var (
    // Log は本パッケージのグローバルインスタンスです。
    Log = defaultLogger()

    // ServiceName は各アプリケーションのビルド時に ldflags を利用して埋め込まれます。
    // 下記はビルドの例です。
    // go build -ldflags "-X path/microservice-logging/logger.ServiceName=MicroService01"
    ServiceName = "not-set"
)

// Logger は公開インターフェースです。
type Logger interface {
    Debug(xTxID interface{}, msg string, fields ...Field)
    Info(xTxID interface{}, msg string, fields ...Field)
    Error(xTxID interface{}, msg string, fields ...Field)
}

// NewLogger は Logger インターフェースのコンストラクタです。
// 基本として、マイクロサービスのアプリケーションはこれを利用せずに Log インスタンスを利用することがルールです。
// 開発をする場合や調査をする場合などで Config を設定しこのメソッドを呼び出す。
func NewLogger(conf *Config) Logger {
    return newLogger(conf)
}

func defaultLogger() Logger {
    return newLogger(NewConfig()) //NewConfig() で共通ルールに基づくデフォルト設定がされる。
}

type logger struct {
    *logrus.Logger
    config *Config
}

func newLogger(config *Config) Logger {
    var l = logrus.New()
    {
        l.Level, _ = logrus.ParseLevel(config.minLevel.String())
        l.Formatter = config.formatter
        l.Out = config.out
    }
    return &logger{
        Logger: l,
        config: config,
    }
}

func (l *logger) Debug(xTxID interface{}, msg string, fields ...Field) {
    ...
}

func (l *logger) Info(xTxID interface{}, msg string, fields ...Field) {
    ...
}

func (l *logger) Error(xTxID interface{}, msg string, fields ...Field) {
    ...
}

上記の共通ロギングライブラリにおいて実装のポイントをピックアップすると以下になります。

  • Goのロギングのライブラリには現時点(2020年3月時点)で、最も多くのGitHubスター数のあるlogrusを利用
  • グローバル変数(Log)をエクスポートしそれだけを利用させるようにしている
  • ログのフィールドに入れるサービス名(ServiceName)はアプリケーションのビルド時に組み込むようにしている
  • インターフェースのメソッドの引数にユニークID(XTransactionID)を指定する

これらのポイントはあくまでも一例に過ぎません。どのように実装するかはチームでのコーディングポリシーなどに依存するでしょう。

続いて以下はロギング設定のコードです。デフォルト設定はマイクロサービスのルールとして決定した内容になります。

package logger

import (
    "io"
    "os"
)

// Config は Logger 設定の構造体です。
//
// formatter: Format type of logging, TEXT or JSON
// out:       io.Writer of the logger output
// minLevel:  Minimum level to out
type Config struct {
    formatter Formatter
    out       io.Writer
    minLevel  Level
}

// ConfigOption はFunctional Options Patternで
// Configのフィールドを設定するための関数型です。
type ConfigOption func(*Config)

// NewConfig は *Config のコンストラクタです。
//
// マイクロサービスのルールである各デフォルト設定
// Formatter: JSON
// Out:       STD OUT
// MinLevel:  Info
func NewConfig(options ...ConfigOption) *Config {
    config := &Config{
        formatter: Formatters.JSON,
        out:       os.Stdout,
        minLevel: Levels.Info,
    }
    for _, option := range options {
        option(config)
    }
    return config
}

func WithMinLevel(minLevel Level) ConfigOption {
    return func(c *Config) {
        c.minLevel = minLevel
    }
}

func WithFormatter(formatter Formatter) ConfigOption {
    return func(c *Config) {
        c.formatter = formatter
    }
}

func WithOut(out io.Writer) ConfigOption {
    return func(c *Config) {
        c.out = out
    }
}

上記の例では

  • ログデータはJSON形式 (formatter: Formatters.JSON)
  • ログの出力先は標準出力 (out: os.Stdout)

というルールをデフォルト設定としています。(脱線ですがGoのio.Writer, io.Readerはとても良いものです。)

これらの設定に関してもチームが持つインフラ構成や方針に影響されるものです。

共通ロギングライブラリを利用するアプリケーションコード

上記の共通ロギングライブラリを利用するアプリケーションの実装例は以下のようになります。

ここではGinフレームワークを利用したAPIサーバを想定しています。Ginのミドルウェア内でプログラムpanic時にスタックトレース付きでロギングします。

import (
    "fmt"
    "net/http"
    "runtime"

    "github.com/gin-gonic/gin"

    "path/microservice-logging/logger"
)

var (
    defaultStackSize = 4 << 10 // 4 kb
)

// Recovery はGinフレームワーク で利用するミドルウェアです。
// 下層からの panic をリカバリーしロギングとHTTPレスポンスセットを実施します。
func Recovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if r := recover(); r != nil {
                err, ok := r.(error)
                if !ok {
                    err = fmt.Errorf("%v", r)
                }
                var (
                    stack  = make([]byte, defaultStackSize)
                    length = runtime.Stack(stack, true)
                )
                logger.Log.Error(
                    /* Request-ID */ c.Request.Header.Get(logger.XTransactionID),
                    /* msg */ err.Error(),
                    /* Additional fields */ logger.F("stack", fmt.Sprintf("%s ", stack[:length])),
                )
                c.AbortWithStatusJSON(http.StatusInternalServerError, &gin.H{
                    "errors": []string{"unexpected error"},
                })
            }
        }()
        c.Next()
    }
}

func main() {
    router := gin.New()
    {
        router.Use(
            Recovery(),
        )
    }
    ...
}

おわりに

ベストプラクティスな方針を知ることができましたが、どうやって実現するかはどうしても開発チームごとに工夫しながら作っていく必要があると思います。日々精進です💪

また、今回のロギング実装の例ではパフォーマンスに関する考慮が不足しています。

  • メモリアロケーションがより効率的なzapというライブラリに切り替える
  • リクエストをさばいた最後にまとめて出力するようバッファを利用する

といった改善案が考えられます。
今後こういった面でも検証する予定です。

それでは (^_^)/~

参考

Nuxtで環境に応じた環境変数をいい感じに設定する

APP/UXチームに所属しております、フロントエンドエンジニアの田原です。
前回の記事から時間が空きましたが、弊社ブログ2回目の登場です。よろしくお願いします。

(隙あらば自分語り) 私は映画が好きなのでよく映画館に観に行くのですが、
最近はコロナの影響もあり外出を極力控えているので、そうこうしている間に観たい映画の劇場公開が終わってしまうのだろうなという寂しさで胸がいっぱいになる日々を過ごしております。 (直近ではミッドサマーを観に行こうと思っていましたが、、、諦めました)

ちなみに誰も興味がないとは思うのですが一番好きな映画はインファナル・アフェア - Wikipedia(3部作)とダークナイト - Wikipediaです。(一番と言っておきながら4本も上げていますが、どうしても順位がつけられないので)

インファナル・アフェア好き!という方と中々巡り合った事がないので、これを機に好きになる方が増えると嬉しいです。

さて、前置きが長くなってしましたが、今回の本題に入らせて頂きたいと思います。
過去の記事でも紹介しておりますが弊社ではフロントエンドフレームワークにVue.js(以下、Vue)を採用し、開発をおこなっております。

Vueを使った開発経験ある点もあって、最近、新たにスタートしたProjectにおいてはNuxt.js(以下、Nuxt)を採用する運びとなりました。
※前回の記事は↓↓ rarejob-tech-dept.hatenablog.com

今回はNuxtで開発をおこなっていくにあたり、先に設定しておくと楽だなと思ったProject内での環境変数の取り扱いについてご紹介させて頂きます。

目次

環境変数の扱いについて

Nuxtを利用してProject(App)の準備を行うにあたり、create-nuxt-appコマンドで環境の準備をおこないますが、その際に.envファイルが自動で生成されるので、 このファイル内でApp内で利用する環境変数を定義し読み込めるようにすることが多いと思います。

とはいえ、開発環境が多岐にわたってくると(local/dev/prod)初期設定時からの.envファイルのみでは扱いが少し面倒になってくるかと思います。

実現したかった事

  • 環境毎対応する.envファイルを定義しておき、Nuxt Appのdeployやbuildのscriptコマンドに応じて、それぞれの環境変数がApp内で参照出来るようにしたい。
  • 環境毎の設定については可読性や拡張性についても考慮した作りとしたい。
  • SSR対応のProject(App)ではない為、クライアントサイドからの参照ができれば良い。

対応について

1. dotenv(@nuxtjs/dotenv)をProject Appに追加

create-nuxt-appで生成したApp(Project)に対して、dotenv(@nuxtjs/dotenv)を追加します。 github.com

※dotenvとは カレントディレクトリに置かれた.envファイルを読み込み、そこに記述されたキー&バリューのペアをprocess.env 経由で参照できるようするnpm moduleです。 Nuxtで生成されたAppに対しては以下のコマンドでnuxt専用のものを追加します。

yarn add --dev @ nuxtjs / dotenv #またはnpm install --save-dev @ nuxtjs / dotenv
2. nuxt.config.jsに設定を記載

追加したdotenv moduleを利用する為にnuxt.config.jsのmoduleの項目に@nuxtjs/dotenvを追記します。

nuxt.config.js

︙
modules: [
  '@nuxtjs/dotenv'
 ︙
]
3. 各環境に対応するenvファイルを設定

Project Rootに各環境毎に対応する.envファイルを設置します。

config/

config
├── .env.dev
├── .env.local
└── .env.prod

各々の.envファイルにApp内で利用したい環境変数を記載します。

e.g. env.local

BASE_API_ENDPOINT_URL="https://hogehoge/local"

e.g. env.dev

BASE_API_ENDPOINT_URL="https://hogehoge/dev"

e.g. env.prod

BASE_API_ENDPOINT_URL="https://hogehoge/prod"

※変数は複数定義してOK

4. package.jsonのscriptsでENVに各々の環境の文字列を渡すように指定する

package.json

"scripts": {
    "dev": "ENV=local HOST=0.0.0.0 PORT=3000 nuxt",
    "build-local": "ENV=local nuxt build",
    "build-dev": "ENV=dev nuxt build",
    "build-prod": "ENV=prod nuxt build",
  },

※ENVに.envとして指定したファイル名(今回の例ではlocal/dev/prod)を渡している。

5. nuxt.config.jsに設定を追加し、config/配下の.envファイルが読み込まれるように対応する

nuxt.config.js

const envPath = `config/.env.${process.env.ENV}||'local'`
require('dotenv').config({ path: envPath })


export default {
  ︙
  dotenv: {
    filename: envPath
  }
}

【補足説明】 * 4で設定したscriptsでENVに文字列を渡している為、nuxt.config.js内においてprocess.env.〇〇で文字列を取得できる。 * envPathに各.envファイルへのPathが代入され(config/.env.local等)、これによりdotenvを使って3で定義した各.envファイルへの参照が可能に * dotenvのpathにenvPathを指定する。(ファイル名をオーバーライドする為の設定)

6. 型エラー解消

1〜5までの対応が終わっていれば、App内(.vue/.ts等)の各ファイルからprocess.env.〇〇で.envファイルに定義した環境変数に対して参照を行うことができます。

︙
created() {
  process.env.BASE_API_ENDPOINT_URL // scriptsで渡した引数の環境に応じた値が参照可能
}

ただし、process.env.〇〇という形ではApp内のどこからでも呼べてしまうのは統一性が無い&参照の際のprocess.envに型を入れたいので型の指定とplugin化を以下の様に行います。(型指定を行わない場合、Nuxt側で持っているdefaultの型がprocess.envにあたる為、想定した型で参照できない場合があります)

// environments.ts等(plugin file)

export type export type EnvironmentVariables = {
  // nuxt default property ------------
  NODE_ENV: string
  browser: boolean
  client: boolean
  mode: 'spa' | 'universal'
  modern: boolean
  server: boolean
  static: boolean
  APP_NAME: string
  // nuxt custom propety --------------
  BASE_API_ENDPOINT_URL: string
}

export cont environments: EnvironmentVariables = {
  // nuxt default property ------------
  NODE_ENV: process.env.NODE_ENV!,
  browser: process.browser!,
  client: process.client!,
  mode: process.mode!,
  modern: process.modern!,
  server: process.server!,
  static: process.static!,
  APP_NAME: process.env.APP_NAME!,
  // nuxt custom propety --------------
  BASE_API_ENDPOINT_URL: process.env.BASE_API_ENDPOINT_URL!
}

export defalut (context: any, inject:(name: string, v: any) => any) => {
  inject('environments', environments)
}

nuxt.config.js

︙
plugins: ['@/plugins/environments']

準備したplugin file内で適当な名前でinjectしnuxt.config.jsにもpluginの宣言を行います。 また、TypeScript側で環境変数が設定されていることを検知させるためにtypes/enviroments.d.tsを設定します。

import Vue from 'vue'
import { EnvironmentVariables } from '@/plugins/environments'

declare module 'vue/types/vue' {
  interface Vue {
    $environments: EnvironmentVariables
  }
}

最終的にApp内でthis.$enviroments.process.env.〇〇という様に指定した環境変数を参照することができます。

参考にさせて頂いたサイト

dotenvについて
process.envの型指定について

まとめ

APIに対してのRequest basePathが環境によって異なることに気づいたタイミングでこの対応を行いましたが、最初にこの辺りについて対応しておいた方が楽だったなぁっと思い、備忘録も兼ねて内容を残しました。

editor formatのチーム間の設定やaxios-modulesとの付き合い方やstoreの設計、その他にもCSS-moduleやstorybookの設定周辺等、Nuxtで環境を作っていく上でこんな感じで作っていくといいかも思える様な設定などについても、改めてご紹介できればと思っております。

最後まで読んで頂きありがとうございました。

Ansible の cron モジュールで環境差分を減らしてみる

ご無沙汰してます

先日、大好きな女性アーティストである aiko の楽曲がサブスクリプションで提供されて狂喜乱舞しております DevOps チームのおくさんです
好きな aiko の曲は「ジェット」の再録音 ver. です
これまでライブには 3 回程しか行けていませんが、一度もライブで「ジェット」を聴いたことはないです・・・

さて、前回記事を書いてから約半年経過し、久々の執筆となります

前回は AWS の CloudWatch に関する記事を書きましたが、今回は Ansible の利用において便利だなと思った「cron モジュール」について書いてみようと思います

rarejob-tech-dept.hatenablog.com

目次

Ansible の cron モジュール

Ansible では、サーバやネットワーク機器等の設定に関する構成管理を行うことができますが、行う操作によってはモジュールという形で提供されています
たとえば、yum でパッケージをインストールするには「yum モジュール」を利用して各種パッケージのインストール等を行います

「cron モジュール」は文字通り cron 設定を管理するためのモジュールで、以下のドキュメントに利用方法が記載されています
こちらのモジュールを利用することにより、cron 設定の管理を Ansible にて行うことが可能になります

docs.ansible.com

やってみよう

cron モジュールのドキュメントに Examples がありますので、これを参考にしつつ動かしてみましょう

今回はサンプルとして、以下のような Playbook を作成してみました
設定内容は、毎日 0:00 にroot ユーザで date コマンドを実行するものとなります

test_cron.yml

---
- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'

なお、今回利用する Ansible のバージョンは以下です

/work/ansible # ansible --version
ansible 2.9.2

では、作成した Playbook を実行してみましょう

/work/ansible # ansible-playbook -i inventory.ini test_cron.yml

PLAY [test] ********************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************
ok: [x.x.x.x]

TASK [Test] ********************************************************************************************************************************************************************************************************
changed: [x.x.x.x]

PLAY RECAP *********************************************************************************************************************************************************************************************************
x.x.x.x              : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

サーバの crontab を確認すると、無事に root ユーザで cron の設定が行われたことを確認できます

[root@x.x.x.x ~]# crontab -l -u root
#Ansible: Test
0 0 * * * date

環境にあわせて cron の有効無効を変更する

さて、本題です

Ansible を利用する際には、本番環境や開発環境でなるべく同じ Playbook を利用したい場合があると思います
しかし、「本番環境では cron を動かしたいが、開発環境では cron を動かしたくない」といったように、環境によって行うべき挙動に差が生まれることも多々あると思います

その場合、cron モジュールにおいては、「disabled パラメータ」を利用することにより環境の差分を吸収することが可能です
disabled パラメータはドキュメントに以下のように記載されています

disabled 
boolean

Choices:
no ←
yes

If the job should be disabled (commented out) in the crontab.
Only has effect if state=present.

したがって、本番環境で cron を有効にする場合は「disabled: no」、開発環境で cron を無効にする場合は「disabled: yes」とすることにより、開発環境のみ cron のコメントアウトを行うことができます
これにより、同一の Playbook を利用して環境によって挙動を変更することができます

せっかくなので、上述で利用した Playbook を変更して試してみましょう
本番環境で cron を動かしたいときは以下のような Task になります

- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'
        disabled: 'no'

また、開発環境で cron を動かしたくないときは、以下のような Task になります

- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'
        disabled: 'yes'

違いは disabled パラメータだけであり、このままだと冗長になってしまうので、変数を利用して切り替えましょう
変数を利用することにより、1 つのタスクで動作の切り替えが可能になります

- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'
        disabled: "{{ DISABLED }}"

それでは、変更した Task を利用して Playbook を実行してみましょう

今回はサンプルなので、extra-vars でサクッと変数に値を代入して Playbook を実行します
まずは DISABLED 変数に yes を代入します

/work/ansible # ansible-playbook -i inventory.ini test_cron.yml -e DISABLED=yes

PLAY [test] ********************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************
ok: [x.x.x.x]

TASK [Test] ********************************************************************************************************************************************************************************************************
changed: [x.x.x.x]

PLAY RECAP *********************************************************************************************************************************************************************************************************
x.x.x.x              : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

実行した結果、cron をコメントアウトができました

[root@x.x.x.x ~]# crontab -l -u root
#Ansible: Test
#0 0 * * * date
[root@x.x.x.x ~]#

今度は、DISABLED 変数に no を代入します

/work/ansible # ansible-playbook -i inventory.ini test_cron.yml -e DISABLED=no

PLAY [test] ********************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************
ok: [x.x.x.x]

TASK [Test] ********************************************************************************************************************************************************************************************************
changed: [x.x.x.x]

PLAY RECAP *********************************************************************************************************************************************************************************************************
x.x.x.x              : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

実行した結果、cron 有効にすることができました

[root@x.x.x.x ~]# crontab -l -u root
#Ansible: Test
0 0 * * * date

まとめ

ジョブについては各種 OSSクラウドベンダーのサービスを利用して管理することも多いと思いますが、まだまだ各サーバ内で cron として実行している環境も多いと思います
Ansible を利用することでも cron の管理は可能ですので、 この記事がどなたかのお役に立ちますと幸いです

また、Ansible で提供されているモジュールは非常に便利なので、各種ドキュメントをしっかり読みながら、今後もいろいろ探して導入検証をしてみようと思います

レイヤードアーキテクチャを導入した開発

バックエンドエンジニアをしております。久住です。
「レアジョブ英会話 スマートメソッド®コース」ではLaravelとVue.jsを使ったSPAの開発をしておりバックエンドではDDDを意識した実装を行なってます。
その中の「レイヤードアーキテクチャ」について今回は書かせていただきます。

なぜレイヤードアーキテクチャを採用しようとしたのか

よくあるMVCの問題としては以下のようなものがあると思っております。

  • Controllerにビジネスロジックを書くなど、FAT Controllerになる
  • ModelにビジネスロジックとDBアクセスが混在して、メンテナンス性が悪くなる
  • 共通のModel間で自由にコールし合い、互いの依存性が高くなる
  • 共通のModelに、特定のControllerからしか使われないような汎用性の低いメソッドができる
  • 共通のModelに、同じような役割のメソッドが複数できる

他のMVCを採用したプロダクトではこれらの問題がたびたびエンジニアのミーティングでも課題として挙げられる事がありました。

  • 修正時に影響範囲がわかりにくい
  • どこに何を実装すべきかチーム間で齟齬が出る 等

作成したアーキテクチャ

以下のような実装イメージをチームで統一しております。

  • ソースをいくつかの層に分離し、実装すべき箇所を明確にする
  • 各層の依存関係を限定的にする

f:id:daisuke6666:20200227162137p:plain

  • 各層の役割について
    • Controller・・・Requestから入力値を取得し、Domain.Serviceに処理を移譲、結果をResponseに渡す
    • Request・・・アクセス認証、入力値のvalidation、入力値のデータ型キャストを行う
    • Response・・・APIレスポンスの構築、Viewファイル
    • Domain・・・ビジネスロジックがここに集約される
      • Service・・・Controllerからデータを受け取り、Entity、ValueObjectの生成、RepositoryへEntityの取得、作成、更新、削除を移譲する
      • Entity・・・一意に識別できるIDを持ち、その属性が変化しても同じものだと判定する、もしくは同じ属性でも別のものだと判定する場合があるもの
        例:ユーザー → 名前が変わっても同じ人である、同姓同名の人がいても別の人の場合がある
      • ValueObject・・・その属性やふるまいだけを表現し、それが「どの」人なのか、「どの」ものなのかは気にしないもの
        どれかを識別するIDは持っていない。また、オブジェクト自体は不変であり、setterは絶対にない
    • Infrastructure・・・DB、他サービスAPI、Platformなどの外部アクセスを行う(Platformとはデータ基盤の事です)

f:id:daisuke6666:20200227161905p:plain

例えばDBから取得しているデータをAPIから取得するようにしたい。
となった場合infrastructureを修正すれば良いとすぐにわかります。

導入してわかったこと

  • メリット
    • どこかを修正した時に上位層を修正すれば良いので、影響範囲がわかりやすく、これが最大のメリットに感じています。
    • おかげで開発者間での実装の相談のしやすさや見積もりの精度も上がりました。
    • コードが増えてくるとどこに何をするものがあるのか不明確になる事が多いと思いますが、役割を明確にする事によって解決し変化に強いシステムが作れます。
  • デメリット
    • 開発メンバーの入れ替わり等があると共有認識を作るのが大変かもしれません。ドキュメントやサンプルコードを見る事でチーム間で共有しています。
    • 初期段階の仕組み作りは少し大変かもしれません。(Laravelのディレクトリ構造も変えたような記憶があります)

今後もメリットを増やせるように仕組みをアップデートしていこうと思います。

ちょっとしたデザインの経験談

こんにちは、デザインチームのキョウと申します。
最近はスパイスとカレーにはまっていて、毎日寝る前にインスタで美味しそうなカレーの投稿写真をひたすら眺めることで、日々の癒やしの時間として過ごしています。
そろそろスパイスマスターの資格を取ろうかなとも考えております(・∀・)

さてさて、本題に入ります。

web業界の流行りのビジュアルデザインとUIデザインの変革をずっと見てきましたが、自分がデザインする時によくやっていることや、使っているツールについて、ちょっと話をしようと思います。


- ひたすら参考サイトを見る

デザイナーとして働いてもうすぐ9年になりますが、新人として働き始めた頃は、ひたすら参考サイトを探して、いいと思ったデザインをひたすら真似して作るという経験は、今でも生かしています。
特にデザインのアイディアがどうしても浮かばない時や、複雑なUIはどうやって作っていくのかは悩んでいる時に、とにかく参考書のようにいろいろなサイトを見ていました。

優れたものをひたすら眺めて、考えて、いざ手を動かして作ってみると、徐々に自分のものになっていく感じがとても実感できるようになります。
そしてこの過程をやっているうちに、自分なりのアレンジもできるようになり、最終的にデザイン感性の磨きとスキルアップには繋がったと、私はそう感じています。

最近では、作る前にもう既に頭の中にイメージが湧いたり、こういう場合はこういうUIだー、この改修ならこのサイトのUIを参考にすればいいよー、みたいなことができるようになりました。

私がよく参考にしているサイトはこちらになります。

webサイト系ならここらへん:
https://muuuuu.org/
https://bookma.torch.blue/
https://1guu.jp/

バナー、チラシ系ならここらへん:
https://retrobanner.net/
http://bannermatome.com/

配色に悩んだら:
https://colorhunt.co/
https://colors.muz.li/
https://coolors.co/

そしてみんなご存知のピンタレスト(何でも参考になります):
https://www.pinterest.jp/


- このデザインは難しい、どうしたらいいかは分からない時は

最初の一歩が難しく、なかなか手を付けられないと感じているデザイナーはいると思いますが、そんな時に、とにかくめっちゃラフな状態でもいいので、紙にワイヤーを書いたり、FigmaやSketch、Adobe UXなどのUIが作りやすいツールで簡単なレイアウトを作ったり、コンテンツの整理から始まるといいと思いますね(σ・∀・)σ

もちろん会社にちゃんとしたディレクターがいて、ディレクターがキレイなワイヤフレームを書いてくれるケースも多いと思いますが、うちの会社の場合は、正式なディレクターという役割を持っている人はいないので、大きなコンテンツ改修や、新規ページを作成する際に、デザイナーはディレクターの役割として働かないといけない時は結構あります。
そういう時、やはり作業依頼者に要望を聞きながら、まず最初はコンテンツと情報の整理から、ワイヤフレームを作っていくというケースが多いです。

ワイヤフレームができたら、自分も作業依頼者も頭の中でイメージができるようになり、フィードバックのやりとりもしやすくなりますね。
そして最初からデザインガイドラインが決まっていれば、わりとデザインにすんなり入れるようになります。

もし最初から色もデザインのテイストも決まってないであれば、やはり先程申し上げた通り、参考サイトをとにかく真似して作ってみよう、そして細かいデザイン調整や、難しいパーツは後回しにしよう、と私はよくこのやり方でやっています。
そしたら、デザインのスピードもかなり上げられて、最初のデザインから何回もブラッシュアップして行くうちに、どんどんいいデザインが出来上がっていきます。


- 常識にとらわれない、時には変則も必要

デザインガイドラインは既に決まっていて、サイトで使う色やフォントも決まっている案件は多いと思いますが(特に自社の保守案件の場合)
明らかにここはこの色を使うと変だよー、でもデザインガイドライン上ではこのボタンの場合はこの色を使えと書かれているよー、この部分はデザインガイドラインに載せた色と文字サイズだとすごく見づらいよー、みたいな時は結構あると思います。
そういう時、きっちりデザインガイドラインを沿って作るより、やはりユーザー目線から見て、ユーザー層を考えた上で、見やすい、使いやすいデザインにするのが一番大事だと、私は思いますね。

もちろん、デザインガイドラインや、サイト全体の統一感からすごく離れたテイストや色を使うのは駄目だと思います。
でも、同じ色トーン、似たような構成であれば、ちょっとした変化を出したり、色を鮮やかにしたり暗くしたり、文字サイズもちょっと上げたり下げたりするなら、全然ありだと思いますね(σ・∀・)σ

そして世の中のデザインルールでは、同じサイトでは最大3-4種類のフォントスタイルと色を使わないという傾向があると感じてますが、もちろん統一感とスッキリ度は大事だと思います、自分もごちゃごちゃしたデザインより、スッキリしたデザインの方が好きです。
でも時には、どうしてもユーザーに見てもらいたい、どうしても特別感を出したい、どうしてもスペシャルなコンテンツにしたい時ってありますよね?
そういう場合、きちんとルールを守るより、時にはちょっと外れたデザインをするのもありだと思いますけどねヽ(^o^)丿


以上。
ちょっとした自分なりの経験談でした。
参考になれたら幸いでございます。

そして、スパイスの力でみんなが健康な体に作れたらいいなと、願います(`・ω・´)ノ

AWS CodePipelineを用いたデプロイ

DevOps チームの うすい と申します。よろしくお願いします。 昨年11月にハーフマラソンに出てから膝に矢を受けてしまった状態が続いています。困っています。

今回は最近作ったシステムにおけるデプロイについてお話したいと思います。

前提

弊社の主だった環境は AWS 上にあります。また、ソースコード管理には GitLab を使用していて、GitLab CI も元気に稼働しています。 本システムは小規模で、リリース後のデプロイは多くないことが想定されています。

デプロイフロー

フローとしては下記のようになっています。

  1. master マージ
  2. GitLab CI で成果物を S3 にアップロード
  3. S3 にファイルが作成されたことをトリガーとして CodePipeline がスタート
  4. ステージング環境にて CodeDeploy でデプロイ
  5. 本番環境デプロイ承認のためのメール送信と Slack 通知
  6. 承認権限を持った人が CodePipeline の中で承認
  7. 本番環境にデプロイ

少し細かく見ていきましょう。

GitLab CI

GitLab CI の中でcomposer installnpm installをしています。各ツールのバージョンを簡単に固定したかったため、今回は PHP 用のイメージの中で npm をインストールしたりするのではなく、バージョンを指定したイメージを使用し作成した成果物を次のジョブに引き継ぐ方式をとりました。 .gitlab-ci.yml の概要です。

stages:
  - npm
  - composer
  - upload

npm:
  stage: npm
  image: node:12.14.1-alpine3.11
  script:
    - npm install
    - npm audit fix
    - npm run production
    - tar czf node_modules.tar.gz node_modules
  artifacts:
    paths:
      - node_modules.tar.gz

composer:
  stage: composer
  image: composer:1.9
  script:
    - composer install
    - zip -r ./${CI_PIPELINE_ID}.zip .
  artifacts:
    paths:
      - ./${CI_PIPELINE_ID}.zip

s3upload:
  stage: upload
  image: alpine:latest
  before_script:
    - apk add --no-cache python3
    - pip3 install awscli
  script:
    - aws s3 cp ./${CI_PIPELINE_ID}.zip s3://${S3BUCKET}/${APP}.zip

実際にはbefore_scriptと呼ばれる文字通りscriptの前に実行されるセクションで環境変数ファイルに秘匿情報を埋め込んだりもしています。それらの秘匿情報は GitLab のSecretに保存しています。 このようにして作成された成果物を最終的に S3 にアップロードしています。

また、これまでは nginx や php-fpm の設定ファイルは DevOps チームの Ansible のリポジトリで管理されていることが多く、開発チームで変更したい場合には

依頼&マージリクエスト作成(開発チーム) → マージ&反映(DevOps チーム) → 確認(開発チーム)

となっていましたが、開発チームで使用しているリポジトリに必要なファイルをすべて含め、CodeDeploy の中で反映を行うようにしました(後述)。

CodePipeline

CodePipeline は S3 にファイルが置かれたことをトリガーとしてスタートします。特に込み入ったことはしておらず、シンプルなパイプラインとなっています。

イメージ図

f:id:goode:20200210174116p:plain
CodePipeline

SNS との連携も上記画像の真ん中の上にあるNotifyから簡単に作成することができて、今回はパイプラインの成功スタート失敗、そして承認時に SNS に通知を送って Lambda を起動し Slack の Incoming Webhooks で通知、といったことをしていますが、AWS Chatbot でも検証中です(下記図)。Lambda 内でメッセージをパースする必要もないので、こだわりがなければ AWS Chatbot で良いかと思います。 承認部分に関してはメールでの通知も行っています。

Chatbot

CodeDeploy

今回は AutoScaling と EC2 を組み合わせたシステム構成になっているので、コンピューティングプラットフォームEC2/オンプレミス、またデプロイ設定はリリース後の修正が多くないこと、また利用される時間があらかじめ把握することができることから、時間の節約のためデプロイタイプインプレースにしました。

CodeDeployはイベントという形で順々に処理を実行していきます。

イベント

こちらのAfterInstallの中で設定ファイルのアップデートや nginx の再起動などを行っています。

また、余談ですがインスタンス起動時のユーザーデータに登録したスクリプトで CloudWatch Alarm の作成を行い、ステージング環境など夜間休日にインスタンスを停止する環境において残り続けてしまう CloudWatch Alarm は AWS Batch で削除しています。 AWS Batch のコンテナイメージはこんな感じで作って ECR に push しています。

delete-insufficient-cloudwatch-alarm.sh

#!/usr/bin/env bash

aws cloudwatch describe-alarms --state-value INSUFFICIENT_DATA --region ap-northeast-1 | grep AlarmName | awk '{print $2}' > ./result.txt

sed -i -e 's/"//g' ./result.txt
sed -i -e 's/,$//g' ./result.txt

for name in $(cat ./result.txt)
do
  aws cloudwatch delete-alarms --alarm-names ${name} --region ap-northeast-1
done

Dockerfile

FROM amazonlinux:latest
 
ENV LANG C.UTF-8
RUN yum -y install unzip aws-cli
ADD delete-insufficient-cloudwatch-alarm.sh /usr/local/bin/delete-insufficient-cloudwatch-alarm.sh
 
ENTRYPOINT ["/usr/local/bin/delete-insufficient-cloudwatch-alarm.sh"]

まとめ

今回は EC2 を選択しましたが、別のプロジェクトでは ECS(Fargate)を使用していたり他にも IaC や CI / CD の強化といったこともしています。DevOps チームのメンバーは現在絶賛募集中ですので、興味を持たれた方はご連絡いただければと思います。また、もっといいやり方や改善点を指摘してくれる方もカジュアル面談でお越しいただければと思います。