オールアバウトTech Blog

株式会社オールアバウトのエンジニアブログです。

未経験でエンジニアとして新卒入社して感じたこと

f:id:allabout-techblog:20200323120018p:plain 今年も例年に引き続きオールアバウトの新卒1年目エンジニアが投稿する企画「テックブログ新卒週間2020」を開催します。

今回はオールアバウトのメディアを運営・開発するグループに所属している @rynkd08 がお送りします。

普段の業務ではLaravelを用いた開発を行っております。 同期入社の中で唯一未経験で入社した私がこの1年でどのようなことを感じ、気をつけているかについてお話しさせていただきます。

前置き

私は大学では分子生物学を専攻していました。 パソコンを使うのはレポートを書くときぐらいでほとんど触ったことがなかったです。 就職活動をし始めて3ヶ月ほど経った2018年の6月ごろにエンジニアになろうと決めてMacを購入し、とりあえずProgateを始めました。

そんなこんなで入社したのでどのくらいのレベルで入社することになったかはお分かりになると思います。

2019年5月〜6月 エンジニア研修

この期間弊社では新卒のエンジニア向けに研修が行われました。ネットワークの基礎やSQLを学んだり、最後の2週に渡ってプロダクト開発を学んだりしました。詳しい研修の内容はこちらの記事を見ていただければと思います。

新卒目線で語る!オールアバウトの新卒エンジニア研修

困っていたこと

Progateをやってただけでは実務レベルは到底追いつけない

Progateをある程度クリアしていたので大丈夫と思っていたのですがそうは行かなかったです。 基礎的なfor文の書き方や配列などはわかっていたのですがその知識を使えませんでした。

業務では配列等もかなり複雑なものになるため理解するまでにすごく時間がかかりました。

フレームワークを使うのでその知識がないと最初の段階でちんぷんかんぷんになる

フレームワーク特有の書き方(MVCモデルなど)が理解できず、 Laravelの基本的なことは教えてはもらったものの頭でわかっただけで身につくにはほど遠いという感じでした。

やって良かったこと・気をつけていたこと

ProgateをやってたおかげでHTMLとCSSはある程度わかった状態になっている

ProgateのおかげでHTMLとCSSについてはある程度できるなという印象でした。 2週に渡るプロダクト開発でも主にフロント周りを担当し、自分ができそうなデザインなどをやることでプロダクトに携わっていました。

2019年6月〜10月 配属

研修も終わりいよいよ配属となりました。未経験で入社しているのをチームのみなさんが知っていたおかげですごく細かく丁寧に説明したり話したりしてくれました。 それでもわからない単語がたくさん出てきていたのを覚えています。

困っていたこと

わからない用語だらけ

配属当初は何のことを言っているのかがわからないことが多く、大変でした。 それが会社固有の言葉なのか一般的に使われているものなのかがわからなかったです。

あとで調べても会社固有のものだともちろん調べてもわからず困り果てることも、、、

そのような時その場で質問することはもちろん、後述する夕会のおかげで解決することができました。

やって良かったこと・気をつけていたこと

わからない時は一度何がわからないかを自分の中で整理してから質問する

これはエンジニアなど職種関係なく社会人歴が浅い人が気をつけるべきことだと感じています。最初の頃はわからないことが山ほどあります。 自分が何をわかっていて何がわかっていないかわからない時も多々あります。 そのような時にただわかりませんと言っても相手を困らせてしまうだけです。

何がわからないかわからない時は理解したことを書き出してみます。 そしてここまではわかりましたと相手に伝えることで相手はじゃあこの部分をもう少し詳しく話さないといけないなと感じてくれます。 わからないことがわからない場合、わかったことを書き出してみるのも一つの手です。

とにかくメモをとる

わからないこと、わかったこと、教えてもらったこと全てを出来るだけメモするようにするといいと思います。 同じく未経験でエンジニアとして入社した先輩は1年にA4ノート2冊分ぐらいはメモをとっていました。

エンジニアとしてメモを取るべきオススメの内容はコマンドです。 何度も打つコマンドに関しては自然と覚えると思います。 しかしたまに打つコマンド(例えばKubernetesのポッドに乗り込むコマンドなど)は必要な時に覚えていないことが多いのでメモしておくと楽です。 (私はこの部分をおろそかにした結果、何度もコマンドを聞くことに....)

メモは手書きでもPCでもどちらでもいいと思いますがコマンドはPCの方がそのまま貼り付けれるので便利だとは思います。

ホワイトボードなど図に書いて説明してもらう

gitの一連の流れはホワイトボードに書いて説明してもらいました。 その図をもう一度自分でも書き起こすことで流れを理解できるようになったと思います。

f:id:allabout-techblog:20190515181844j:plain

そのほかにも設計の段階で話していることがわからなかった時はホワイトボードにまとめて書いてもらうことで視覚的に理解することができるようになりました。

夕会でその日わからなかったことを質問する時間を設ける

私が所属していたチームでは毎日30分ほど夕会を行っていました。 業務中には質問しづらかったことをこの時間に一括ですることでその日の疑問点を解消することができました。

私はたまにしかしていなかったのですが疑問点を解消した後、自分の言葉でまとめ直してアウトプットを行えばさらに知識が定着すると思います。

業務時間後にテスト駆動開発の勉強をする

テスト駆動開発を行っていたのですが配属したての時はそもそもテストとは何を書くものかということがわからなかったです。

業務の中で行うものをやっていただけではテスト駆動開発の基礎が身につかないと感じたため業務時間後に基礎的な内容で身につけようと考えました。

これを行うことでテスト駆動開発のリズムをつかめることができました。

業務終了後にも関わらず付き合ってくれたメンターの方には感謝しております。

2019年10月〜2020年3月 組織体制変更

2019年10月から組織体制が大きく変わりました。 その影響もあって開発のやり方が少し変わり最初の1ヶ月ほどは戸惑いを感じながら仕事していました。

この頃に担当していた業務としては、

  • 構造化データを導入する
  • フィード配信先を追加する
  • AIコメントシステムを導入する

などメディアのことを行っていました。

この頃になると細かい仕事が多かったこともあって業務に関して困ったことは初めの半年ほど多くはなかったです。 最初の頃には流れが全くわからなかったgitも一連の流れをスムーズに行うことができるようになりました。(2月頃になってもmasterプッシュしてしまったことがあるのでこれからも精進していきます)

困っていたこと

クラウド周りがわからない

研修の時に一通り会社で使用しているクラウド環境のことを説明してもらいましたがそれまではあまり触れる機会がなかったです。 そのため触れる機会になった時や他の業務でその知識がいるという時にわからないことが多く、研修資料をちょこっと見直すこともありました。

業務効率が悪い

タイピングスピードが遅いのはありますが、そのほかでも遅いと感じることが多々ありました。 ショートカットキーを使いこなせていなかったり、マウスを多く使うのでホームパッドとキーボードの手の行き来が頻繁だったり、、、

慣れている方はほとんどマウスを使わずキーボードを使っていました。

やって良かったこと・気をつけていたこと

わからない時はわからないと言うこと

半年過ぎたあたりから業務にも慣れてきてできることも格段と増えてきました。 しかしまだまだわからないことが多いのでその時に変なプライドを持たず素直に質問することが大事です。

タイピングの練習

タイピングの速度が基本的に遅かったのでタイピングの練習を毎日行い、スコアをスプレッドシートで管理しました。 コードと日本語では少し違うかもしれませんがタイピングスピードは早くなったと感じています。

まとめ

最初の半年ほどは本当にわからないことしかなく、毎日必死でした。 ただその時でもわからない時はわからないと素直に伝え、メモをとり、メモをとったことを自分の言葉でアウトプットする、ということを続けていると 半年ほど経った時にようやく点が繋がる感覚が出てくると思います。

何度も言っていますがわからないことだらけなのでその時にわからないと素直に言うことが大事だと感じました。 一度聞いたことを二度と聞かないようにすることも大事ですがわかるまで聞くことの方が大切だと思います。

既存batchのPHP、Laravelのアップグレードとcronjob化したお話

f:id:allabout-techblog:20200323115646p:plain オールアバウトの新卒1年目エンジニアが投稿する企画「テックブログ新卒週間2020」を開催します。

本記事はオールアバウトの運営・開発を担うグループに所属している@k_takamatsuからお送りします。 現在、オールアバウトとグループ会社のオールアバウトナビ両方で業務を行なっています。 基本的にどちらもLaravelを使ったWebアプリの開発が主です。

本日はオールアバウトナビで行なった、システムのバージョンアップとCronJob化についてお話しします。

既存batchシステムのPHP、Laravelバージョンアップ

現在オールアバウトナビ(以降ナビ社)は、「カジュアルに知性をアップデート」をコンセプトにした「SNS配信型ウェブメディア」のcitrusを運営しています。 citrus-net.jp

こちらのサービスに関するbatchシステムを改修する際に、PHPのバージョンが古くて使用できないpackageが出てきたため、バージョンアップをする必要がありました。 また、現状batchはGCPVMインスタンスで動いているのですが、この点に関して料金と何よりインスタンス自体の保守・運用などのコストが掛かる、という事からbatchをサーバレス化したいという話が上がりました。 元々ナビ社ではKubernetesでwebアプリを運用していたことから、batchシステムをCronJob化することになりました。

このような経緯から、私はこのbatchシステムのバージョンアップとCronJob化を行う事になりました。 しばらくはPHPとLaravelのバージョンアップについてお話をします、CronJobについては後ほど記載します。

既存システムの構成を把握

実際に作業を始めるにあたり、まずは現状batchシステムがどう動作しているのか知る所から始めました。

調査した結果現状の構成は以下の通りです。

古くからあるシステムという事と、社内の閉ざされた環境で動作していたため長い間アップグレードは行われていない事がわかりました。 また、LaravelもLTSの2世代前と古かった為、PHPだけでなくLaravelもアップグレードしようという事になりました。 PHPは7系に、Laravelは6系に移行する事となります。

既存システムの動きとアップデート後のシステムの動きをどう担保するか

PHPとLaravelのバージョンアップやりますって話になりましたが、実は入社するまでフレームワークを使用した開発経験がありませんでした。 またPHPに限らず、プログラム言語のアップデート対応などの知識・経験もありません。 そのため、バージョンアップ対応に自信がなく最悪バグを残したままリリースしてしまうのではないか?といった不安が当初強かったです。

また、今回はPHPのバージョンアップ、Laravelのバージョンアップ、CronJob化を一気に対応してリリースする方針となり、 それぞれ動作の担保をどのように確保するか、また問題が生じた際の切り分けをどうすればいいかという点で凄く悩みました。

色々考えた結果、動作の担保としてUnitテストを書くのが一番だと思ったのですが、既存のソースを見る限りUnitテストを入れるとなると多少リファクタする必要がありそうでした。 ですが、今回はそこまで時間を掛ける事ができないためUnitテストの導入は断念しました。 その代わり静的チェックのPHPStanを使用する事で最低限の担保を行う事にしました。 ※本当はPHPccを使用したかったのですが、サポートが終了しているためPHPStanを用いる事としました。

また、問題を切り分けるためにローカル環境に現状の動きを再現するDockerコンテナを作成し、そのコンテナ内で言語・フレームワークのアップグレードを段階的に対応する事にしました。 CronJob化については手元でPHP・Laravelの移行が完了した後に対応する事で切り分けました。

既存システムの動作をDockerコンテナで再現

まずは現状と同じ動作を行う環境を作成する必要があります。

各種設定値やcomposerで管理しているpackageなど、それぞれ調査した後にDockerfileを作成します。

ベースイメージは公式のDockerHubからphp:5.5-cliを落としてきて、それぞれ必要なモジュールをインストール、アプリイメージを作成していきます。 一から環境構築など行なった事がなかったため、この環境作りが個人的にかなり苦戦しました。

ネットの情報などを利用して記述していたのですが、次から次に襲いかかってくるエラーに心打たれ・・・それでも何度もtry & errorを繰り返してようやく動作するコンテナが立ち上がりました。 同じようにデータベース用のDockerfileも作成し、テスト用環境のデータベースからデータを取得してデータの準備も完了。 バッチとデータベースのコンテナはdocker-compose.yamlで管理する形にします。

これで無事既存システムの動きの再現を行う事ができました。 知らない事だらけだったので、この時点でかなり勉強になりました。

PHPのバージョンアップ

既存の動作をする壊していい環境を作れたので、ここからは思う存分変更を加えてPHP5.5から7系へのバージョンアップを行います。 ※壊していい環境を自分で作る事で、心理的安全を確保できたのはかなり良かったです

先ほど作成したDockerfileで使用していたベースイメージをphp:5.5-cliから7系に変更。 これでコンテナを立ち上げてPHPのバージョンを確認すると、無事php7に変更されていることが確認出来ました。 これでPHP7への切り替えは終了です。ここから実際に非推奨となった関数であったり、消されてしまった関数だったりの対応を行う必要があります。

ここからは先程述べたとおりPHPStanを用いて修正を行なっていきます。 まずは出来上がっているLaravelの環境にPHPStanを組み込んでいきます。

PHPStanの導入

導入は至って簡単で、composer.jsonに書くだけで入ってくれます。

インストールされたら、実際にチェックコマンドを実行。 /app# vendor/bin/phpstan analyse -l 0 ./app

すると沢山のエラーが出力されました。

119/119 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
------ ------------------------------------------------------------------
  Line   Console/Commands/〇〇〇〇〇〇Command.php
 ------ ------------------------------------------------------------------
  46     Call to static method info() on an unknown class Log.
  56     Call to static method info() on an unknown class Log.
  80     Call to static method beginTransaction() on an unknown class DB.
  93     Call to static method rollback() on an unknown class DB.
  94     Call to static method Error() on an unknown class Log.
  97     Call to static method commit() on an unknown class DB.
  98     Call to static method info() on an unknown class Log.
  102    Call to static method rollback() on an unknown class DB.
  103    Call to static method Error() on an unknown class Log.
  117    Call to static method info() on an unknown class Log.

こんなエラーが延々と出力されて、合計198件のエラーが検出されます。 うわー、これ手に負えないやつじゃね?って思って居たのですが、よくエラーを見てみるとunknown classがたくさんあり、かつそのclassはLaravel独自のclass名である事に気づきました。

ネットで調べてみると、Laravel 5 IDE Helper Generatorなるものがあり、LaravelのIDE補完を可能にすると上記のエラーの類は消えることがわかりました。 なのでLaravel 5 IDE Helper Generatorを導入します。

Laravel 5 IDE Helper Generatorの導入

これも同様にcomposerでインストールを行います。 composer require --dev barryvdh/laravel-ide-helper ~2.0 ※Laravel5以上からは2.0を指定するようです

インストールが完了したらProviderに登録します。

'providers' => array(
    // ↓ 下記を追記 ↓
    'Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider',
),

php artisan ide-helper:generateを実行し、_ide_helper.phpが生成されれば終了です。

この_ide_helper.phpの中に、Laravel独自のクラスが補完されているため、PHPStan実行時に先ほどのファイルを読み込ませると良いようです。

PHPStanで補完のファイルを読み込ませる

PHPStanは実行する際に、設定ファイルを使って実行する事ができます。

そこで、以下のファイルを作成 phpstan.neon

parameters:
    level: 0
    autoload_files:
        - _ide_helper.php
    paths:
        - ../../app

実行時に設定ファイルを指定 vendor/bin/phpstan analyse -c vendor/bin/phpstan.neon これで実行結果が変化するはずです。

実際に5系から7系にあげた後、静的チェックツールで確認

Laravelのide_helperを読み込ませた結果、テストのエラー結果が変わりました。

------ -----------------------------------------------
  Line   Console/Commands/〇〇〇〇Command.php
 ------ -----------------------------------------------
  131    Undefined variable: $hoge
 ------ -----------------------------------------------
 ------ --------------------------------------------------------
  Line   Console/Commands/〇〇〇〇Command.php
 ------ --------------------------------------------------------
  154    Caught class App\Console\Commands\Exception not found.
 ------ --------------------------------------------------------
 ------ --------------------------------------------------------
  Line   Console/Commands/〇〇〇〇Command.php
 ------ --------------------------------------------------------
  160    Caught class App\Console\Commands\Exception not found.
 ------ --------------------------------------------------------
 ------ --------------------------------------------------------
  Line   Console/Commands/〇〇〇〇Command.php
 ------ --------------------------------------------------------
  166    Caught class App\Console\Commands\Exception not found.
 ------ --------------------------------------------------------
 ------ --------------------------------------------------------
  Line   Console/Commands/〇〇〇〇Command.php
 ------ --------------------------------------------------------
  168    Caught class App\Console\Commands\Exception not found.
 ------ --------------------------------------------------------

198件あったのが合計24件まで削除されました。 これでようやく対応できるエラー量となりました。

実際に検知されたエラーとしては、使用されていない変数が残っていたり、非推奨関数が使われているよってエラーだったり、こんな書き方出来ないよってエラーでした。 1つ1つエラーを解消しに行ったら対応する事ができ、最終的に静的チェックでエラーの検知を無くす事ができました。

想像していたよりも、PHP5から7への対応は比較的安易に終了する事ができたので良かったです。 ※一応不安だったので、非推奨になった関数や無くなった関数など検索しましたが、残っていませんでした。

Laravelのバージョンアップ

想定していたよりも、PHP7への移行は比較的スムーズに終わったためLaravelのアップグレードに入ります。

Laravelはアップグレードガイドを公式が出してくれています。

readouble.com

基本的にはこのアップグレードガイドに沿って行えば問題ないはずです。 今回のシステムに関してはLaravel5.1とかなり前のバージョンからのため、過去のアップグレードガイドを跨ぎながら進めました。

とは言え初めてLaravelのバージョンアップを行うため、ネットで手順などを調べてみました。 すると以下のように書いてありました。

  1. composer.jsonを修正して、ライブラリのバージョンを上げる
  2. composer updateしてみる
  3. 「composer.jsonに書いてあるライブラリはこのバージョンだと依存関係を解消できない」みたいなエラーが出る
  4. packagist.orgでバージョンと依存関係を調べつつ1.に戻る

ひとまずこのやり方に沿って進める事とします。

packageの依存関係

composer.jsonで指定しているバージョンを6.0に変更

"require": {
        "laravel/framework": "6.0.0",

実行してみると以下のようにエラーが発生

Updating dependencies (including require-dev)
Your requirements could not be resolved to an installable set of packages.
  Problem 1
    - Installation request for laravel/framework 6.0.0 -> satisfiable by laravel/framework[v6.0.0].
    - hogehoge v2.0.0 requires illuminate/support ~5.0.17|5.1.*|5.2.*|・・・v5.6.9

とあるpackageの2.0.0はLaravel5.0から5.6までしかサポートしてませんよって怒られます。 Packagistで確認しても、そのように記述されてました。

packageによっては、単純にバージョンを上げればインストールできる場合もありますが、稀にLaravel6.0に対応していない or PHP7.3に対応していないpackageがあります。 それらに関しては代用できるpackageを探してインストールする作業が必要でした。

今回は大規模にアップグレードするため、殆どのpackageを更新しなければならず・・・結構骨の折れる作業でした。 もちろんですが、使用するpackageを変更すると既存のソースコードも変更しなければならず。 早々に壁にぶつかって泣きそうでした。

artisan コマンドが使えない

無事にcomposer updateが終わっていざ、aritsanコマンドを使ってみようかと思ったのですが上手くいきませんでした。 これはLaravelアップグレードによる弊害で、アップグレードガイドを元に整えていくと無事実行されます。

その他の原因として、cacheが邪魔して動かないといった事がありました。

発生したエラーの内容を見ると以前の設定のキャッシュが残っているのが原因でした。 そのためキャッシュを削除したら上手くいくだろうと思い、実行することに。 - php artisan config:clear - php artisan cache:clear

共にエラーが発生して削除できません。

仕方ないのでbootstrap/cache配下の4つのファイルを削除して再度実行するとうまくいきました。 - packages.php - services.php - config.php - routes.php

根本の原因はこのファイルなので、このファイルを生成しないように変更します。 composer.jsonで"php artisan optimize"が走っていたのが問題でした。 以下のように削除してcomposer update -> php artisan listを実行すると正常に動作しました。

"scripts": {
        "post-install-cmd": [
            "php artisan clear-compiled",
            "php artisan optimize"
        ],
        "post-update-cmd": [
            "php artisan clear-compiled",
            "php artisan optimize"
        ],

"scripts": {
        "post-install-cmd": [
            "php artisan clear-compiled"
        ],
        "post-update-cmd": [
            "php artisan clear-compiled"
        ],

これでcomposerのupdateもできるようになったしartisanコマンドが使えるようになったので、一旦アップグレード作業を停止します。

Laravel6になったためPHPStanで動作を静的にチェック

静的チェックを行なって、今の段階でエラーがないか確認をすることにしました。

Laravel6になり内部構成も変更されているため改めてide-helperを生成します。 生成後PHPStanを実行結果、見つかったerrorは17件。 内容としては既存のpackageのエラーであったり、アップグレードによるLaravelのエラーでした。 しっかりLaravelでのエラーも捉えれているので安心ですね。

このエラーを解消していけば、Laravel6系へのアップグレード対応が完了します。

PHPStanでエラーを潰す

チェックして検知されたエラーを、Laravelのドキュメントで公開されているアップグレードガイドを元に対応していきます。

途中packageの変更に伴い、処理を書き換える必要も出てきましたが、なんとか無事にPHPStanのエラーを潰す事ができました。

PHPStanでのチェックが通ったので実際に各コマンドを実行してみる

ここまでの対応で、PHP7.3へのアップグレード・Laravel6.0へのアップグレードが一応完了しました。

実際にBatchで実行されているartisanコマンドをそれぞれ実行して動作確認をしていきます。 事前に必要なテストデータをそれぞれ作成、実行した結果と現在VMインスタンスで稼働しているbatchシステムの実行結果を比較していって動作の確認を行っていきます。

この際に現在使われていないコマンドなどが複数残っており、不要な処理とそれに関連するファイルは削除してリポジトリの掃除も行っていきます。 そうやって全部のコマンドが正常に動作するのを確認して、PHPとLaravelのアップグレード対応は無事完了しました。

VMインスタンスで動作しているbatchをGKEのpodで動かす(CronJob化)

ここまで大きな対応として3つあるうちの2つが完了しました。

残りは3つ目のCronJob化となります。 普段業務でDockerであったり、kubernetesであったり使用しているのでGKEに関してはなんとなーくわかっていたのですが。 CronJobに関しては全く知らない状態でした。

なので、まずはCronJobって何なのか調べるところからスタートです。 具体的に参考になったのは以下の記事でした、ありがとうございます。

cloud.google.com

cstoku.dev

実際にCronJobってどんな物か把握できたら、作業に取り掛かり始めます。

CronJobのyamlファイル作成

Googleが提供してくれているCronJobのドキュメントを参考にyamlファイルを作成していきます。

CronJobは基本的に1yamlファイルにつき1jobを実行します。 そのため、batchで動かしているコマンドの数ぶんCronJobのyamlファイルを書く必要がありました。

実際に作成したファイルの一部分です

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hogehoge-ranking
spec:
  schedule: "0 * * * *" # UTC時間で実行される
  successfulJobsHistoryLimit: 0
  failedJobsHistoryLimit: 0
  jobTemplate:
    spec:
      template:
        metadata:
          labels:
            name:hogehoge-ranking
        spec:
          containers:
          - name: hogehoge-ranking
            resources:
              requests:
                cpu: 〇〇
                memory: 〇〇
              limits:
                memory: 〇〇
            env:
              - name: TZ
                value: "Asia/Tokyo"
            image: GCRにあるImageのパス
            command: ["php", "artisan", "hogehogeRanking"]
          restartPolicy: Never
      backoffLimit: 1

ドキュメントなどを見て各種設定項目を追加していきました。 実行時間がUTC時間なのは気をつけなければいけません。 実行終了したpodを何世代分残すであったり、それぞれのjobを並行実行させていいのかダメなのかなど、結構考慮しなければいけない点が多かったです。 設定項目に関しては、先程紹介した記事に詳細がありますので、気になる方はそちらをご覧ください。

GKE上にデプロイしてみる

こんな感じでyamlファイルを作成したら実際にKubernetes上にデプロイして動作の確認を行います。 弊社ではCIツールとしてCircleCIを採用しています。 ※詳細は以下の記事に載っています

allabout-tech.hatenablog.com

そのためCircleCIのconfig.ymlで作成したCronJobのyamlファイルをapplyするよう修正します。 ここで問題になるのが、今までCircleCIでデプロイしていたのはVMインスタンス上でありGKE上にデプロイするような処理ではないということです。

CircleCIの設定

CronJobの確認を行う前にCircleCIのconfig.ymlを修正する必要がありました。 こちらに関しては詳細は省きますが、それぞれ必要な項目を設定して最終的に以下のようなフローにしました。

f:id:allabout-techblog:20200319110139p:plain

STGとPROに関してはそれぞれworkflowsでマージされたブランチを元に制御しています。

デプロイされた後の動作確認

まずはkubectl get cronjobでCronJobができているか確認

kubectl get cronjob

NAME                      SCHEDULE    SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hogehoge-ranking   0 * * * *             False           0              <none>                    6m59s

できている事を無事に確認できました。 jobが実行された後にjobの確認もできます。

kubectl get job

NAME                      COMPLETIONS   DURATION   AGE
hogehoge-ranking   1/1                        6m11s           6m11s

無事にjobも実行されて完了していることがわかります。 実行結果を確認しに行ったら、問題なく動作している事が確認できました。

Kubernetes Loggingでログを取得できない

CronJobの設定項目でKubernetes Loggingでログを取得できるから実行した後のpodは残さない方針に決めた、と書きました。 しかし、その大事なログが取得できない事がわかり対応する必要がありました。

そもそもDockerの標準出力としてログを確認できない

Kubernetes Loggingで確認できないという話だったので、まずは手元で動かしているDockerコンテナのログとして出力できるか確認してみました。

$ docker-compose logs batch

ログ自体は出力されるのですが、それはコンテナを立ち上げた際のログが出力されるだけで、jobで実行した際に出力しているログが表示されません。 なぜだって頭を悩ませるのですが、そもそもLaravelバージョンアップに伴ってログの設定やってないって事に気付いたのでLaravelをいじる事にしました。

Laravelでログを標準出力するまで

そもそもLaravel5.1とLaravel6.xではログの設定の仕方が異なります。 Laravel5.6からログの設定はconfig\logging.phpで設定する事になっているため、まずはファイルの作成から。

以下参考

github.com

ここでlaravelはデフォルトで標準エラーは用意されているのがわかります。

'stderr' => [
    'driver' => 'monolog',
    'handler' => StreamHandler::class,
    'formatter' => env('LOG_STDERR_FORMATTER'),
    'with' => [
        'stream' => 'php://stderr',
    ],
],

今回出力するログは開始だったり終了だったりがわかるようなログだったため、標準エラーではなく標準出力としてログを吐きたいのです。 そのためstdoutに吐くような設定を追加する必要があります。 それに関してはこちらの記事を参考にしました、ありがとうございます。

qiita.com

そんなこんなでstdoutを作成、デフォルトで標準出力を使用するように設定してLaravelの設定は終了です。

相変わらずDocker logsで表示されない

意気揚々とバッチのコマンドを実行して、Docker logsを実行! それでもログは出力されていません。

laravelの方でログが取得できているか確認しに行くと、ちゃんと今日の日付でログファイルが作成されていました。 その為Laravelでの標準出力はうまくいっている、Dockerの方で出力をキャッチできていない事がわかりました。

これに関しては、php.iniの設定が必要だったりDocker for Mac故の罠なども多々あり色々つまづくのですが今回は割愛します。

Kubernete Loggingでログが表示される!!

それぞれ必要な設定を行った後に実際にCronJobをデプロイして、podが実行されたとにログビューアを見に行くと・・・ ありました!しっかりKubernetesがログを拾ってくれていました。

I 2020-03-06T03:00:28Z [2020-03-06 12:00:28] production.INFO: ranking command start.

I 2020-03-06T03:00:28Z [2020-03-06 12:00:28] production.INFO: ranking command end.

これにて、ようやくKubernete Loggingで標準出力したいって目標は達成できました。

実際にCronJobとして処理が実行されるか確認

一通り作業が終わったら、今度はテスト環境にdeployして動作を確認します。 KubernetesUTC時間のため、CronJob.yamlファイルでの時間指定にだけ気をつけてdeploy。

それぞれ実行時間になった際に正しく動作している事を確認する事ができました。 あとは本番用のCronJob.yamlファイルとCircleCI対応を行ってdeployしたら終わりとなります。

本番のリリースはもう少し後なので、それが終わるまで気を抜かずにやりきりたいと思います。

終わりに

今回の仕事では主に以下の作業を行っています。

PHP・Laravelのバージョンアップ、Dockerアプリイメージの作成、CronJob化、CircleCIのフロー作成など。 今までの業務内容ではやる事の出来なかった内容だったので、かなり満足しています。 それも、新卒1年目で全ての作業を任せてもらえたのは大きな経験となりました。

今回の仕事を通して大事だなって思ったのは、初めての業務内容で不安がある際にその不安点をどれだけ取り除けるか工夫する事です。 私の場合手元の環境で本番と全く同じ動作を再現できた事、静的チェックツールを用いてエラーを検知する事で心理的安全を作りました。 そのおかげで、環境さえ作ってしまえばゴリゴリ変更を加えていけたので本当に大事だと感じました。

また、弊社では開発部とSREと別れていて基本的にアプリイメージの作成等はSREが行っていました。 それを、今回アプリイメージの作成、各種設定などインフラ寄りの作業も行う事ができ、自分の知見が広がった事を実感しています。 WEBアプリケーションエンジニアとしてこの辺りの知識も当然のように求められると思いますので、機会を見つけて今後も学んでいきたいです。

Laravel.shibuya #6がオールアバウトで開催されました

こんにちは!ララベラーの @y_hideshi です。 去年に参加したPHPカンファレンスからご縁があり、1月24日 Laravel.shibuyaにて会場提供させていただきました。

​今回、私はスタッフとして参加の予定でしたが、つい話を聞きたくてセッションにも参加させていただきました! 本記事では、スタッフ&参加者として当日の空気や盛り上がりをレポートさせていただきます!

続きを読む

【ログ物語】第2章 ~ログ/収集の仲間~

こんにちは!オールアバウトでデータエンジニアをしている@ondaljhです。

「 データエンジニアってどんなことやってるの?」という方々にも、こちらを読めばデータエンジニアの業務の一片がわかるようになる! 連載企画「ログ物語」の第二回をお届けいたします。

バックナンバー

今回の構成

第2章.ログ/収集の仲間

  • 1.ログ収集
  • 2.業務データ

第2章.ログ/収集の仲間

1.ログ収集

話す内容としては、All Aboutメディアを含む、PrimeAd(以下、PA)のメディアパートナーサイトからTreasure Data(以下、TD)までどの経由でログが収集されるかが対象になります。

ログ収集の概要

前回の記事で既にお見せしたことがある図になりますが、ログ収集の概要を簡略に表すと下記の図の通りになります。

f:id:allabout-techblog:20200218111541p:plain
ログの収集

ログの転送と集約・フィルタは両方ともfluentdを使っていますが、転送(いわゆるFowarder)サーバーと集約・フィルタ(いわゆるAggregatorとProcessor)サーバーをそれぞれ用意し、各自の役割を担っています。

ログ転送(Fowarder)

All Aboutメディアを含む、PAのメディアパートナーサイトからのログは各サイトに仕込んでるイメージトラッキングやリダイレクターによってログ転送サーバーに転送されます。ログ転送サーバーに転送されたログは、fluentdエージェントによって、ログ集約・フィルタサーバーにアクセスログとして転送されます。基本的にサーバーのスケールアウトが必要な時以外は設定変更が発生しません。 ログ転送サーバーは、各サーバーから発生したログをログ集約・フィルタサーバーに右から左へ流すだけなので、サーバーインスタンスAWSの汎用インスタンスを複数使っています。

ログ集約・フィルタ(AggregatorとProcessor)

ログ集約・フィルタサーバーではアクセスログをfluentdの機能を使ってフィルタリングやデータの簡単な正規化等を行ってからTDまで転送します。新しいログパターンが発生したり、新しいフィルタを適用したい場合には設定変更が必要になります。ログ転送サーバーとは異なり、一つ一つのログに対してfluentdの処理を行っているため、サーバーインスタンスAWSのコンピューティング最適化インスタンスを複数使っています。

上記のように、ログ転送サーバーとログ集約・フィルタサーバーは役割も異なり、設定変更の発生頻度も異なります。また、役割によってサーバーへの負荷も異なります。従って、PrimeAdでは下記の観点でログ収集サーバーを各役割ごとに分けています。

  • サーバーの役割の明確さ
  • 設定変更の頻度による障害リスクの軽減 (設定変更が必要なサーバーのみ設定変更を行う)
  • 負荷に対するメンテナンスのしやすさ (負荷が高まった役割のサーバーをスケールアウトする)

ログ転送とログ集約・フィルタの構成図

ここまで見てきたログ転送サーバーとログ集約・フィルタサーバーを図で表すと、下記のようになります。

f:id:allabout-techblog:20200225110817p:plain
fluentdサーバー構成

上の図では、TD以外にもBigQueryが登場しています。BigQueryは下記の役割を担っています。

  • データウェアハウスとして、広告配信の機械学習や目標クリック到達判定で利用するためのリアルタイム集計用の生ログを格納。

学習やリアルタイム集計等は別の機会があれば説明することにしますので、この場では「そのようなものがあるんだ~」程度で問題ありません。

fluentdとは?

Fluentd is an open source data collector for unified logging layer. 一言で言うと、各種ログをデータとして収集するオープンソースになります。 集計チームではログの転送及び集約・フィルタでもう既に何年も前から使っている状況です。 2018年12月時点での利用バージョンは下記になります。

ログ転送 : 0.12.12
ログ集約・フィルタ : 1.0.2
ログ集約・フィルタサーバーではマルチスレッドが使いたかったので、2018年から1.0.2を使っています。

簡単な説明

fluentdはRubyとCで実装されていて、Rubyのデーモンとして動きます。 従って、設定ファイルもRuby処理ができるようになっています。 そして、fluentdは数多くのプラグインが提供されていて、ログの収集でも下記のようなプラグインを使っています(プラグインの説明は省きますが…)。

fluent-config-regexp-type (1.0.0)
fluent-logger (0.7.1)
fluent-plugin-bigquery (1.2.0)
fluent-plugin-extract_query_params (0.1.1)
fluent-plugin-forest (0.3.3)
fluent-plugin-record-modifier (1.1.0)
fluent-plugin-record-reformer (0.9.1)
fluent-plugin-rewrite (0.1.1)
fluent-plugin-rewrite-tag-filter (2.1.0)
fluent-plugin-td (1.0.0)
fluent-plugin-td-monitoring (0.2.3)
fluentd (1.0.2)

fluentdは設定ファイルにディレクティブを書くことで設定できるし、設定した内容通りに動作します(これは当たり前ですねw)。 基本的なディレクティブは下記になります。

ディレクティブ名 概要
<source>~</source> 入力プラグインの指定を行う
<filter>~</filter> フィルタリングプラグインの指定を行う
<match>~</match> 出力プラグインの指定を行う

実際にTDへログを転送する処理では数多くのディレクティブ処理が行われています。 下はそのソースの一部を抜粋したものになります。(一部マスキング済み)

#-------------------
# ログ転送サーバーからログの受け取り
#-------------------
<source>
  @type forward
</source>

#-------------------
# 不要行及びブラックリスト削除(共通処理)
# 共通処理の場合、ユーザエージェントは「ua」として渡される
#-------------------
<filter raw.**>
  @type grep
  <regexp>
    key uri
    pattern /hogehoge.gifuga.php
  </regexp>
  <exclude>
    key ua
    pattern bot|Bot|BOT
  </exclude>
</filter>

#-------------------
# uriから特定パラメータを取得する
#-------------------
<match raw.**>
  @type extract_query_params
  key uri
  remove_tag_prefix raw.log
  add_tag_prefix after.check.xxxtag
  only xxx, v
</match>

#-------------------
# xxxパラメータがあるかないかによって分岐
#-------------------
<match after.check.xxxtag>
  @type rewrite_tag_filter

  # xxxパラメータがある場合 - 英数字のみOK
  <rule>
    key xxx
    pattern ^[0-9a-zA-Z]+$
    tag decoding.xxxtag
  </rule>
</match>

Fluentd v0.12 Filter プラグインの使い方と作り方等を参照したので、興味ある方は一読してみてください。

2.業務データ

集計処理を行うためには意図したログなのかどうか等を判定するためのデータが存在し、集計側ではそのデータを「集計としてのマスタ」という意味で、「マスタデータ」と呼んでいます。が、その正体は「業務データ」になりますので、ここでは「業務データ」として説明させていただきます。

※正規化については前回の記事で軽く書いていますが、「第3章.ログ/二つの塔」にて詳しく説明させていただきます。

業務データとは

上記でも少し話しましたが、各種ログが意図したログかどうかを判定する材料になります。
例えば、All Aboutの一つの商品であるタイアップ記事の場合、掲載期間がありますが、既に掲載期間が過ぎた場合でも記事として残しています。
しかし、掲載期間が過ぎたタイアップ記事に対しては、掲載期間が終わった時点でもう集計は不要になります。
そのようなタイアップ記事データを集計しないように、ある業務データと結合して、掲載期間内のログだけを集計する等の処理を行います(「第3章.ログ/二つの塔」で詳しく説明しますが、この処理も正規化の1つになります)。

業務データの処理フロー

集計としての業務データは各種フロントデータに依存しているため、各種フロント側からデータを取得します。
フロント側のデータをそのまま使うパターンもありますが、集計用途に合わせて加工してから使うパターンがもっとも多いです。
取得して加工まで終わったデータは、TDへアップロードされ、そこから実際のログデータと結合されて利用されます。
文言だけだとわかり辛いので、簡単な図で出しますと、下記の感じになります。

f:id:allabout-techblog:20200225114222p:plain
業務データ取得及びアップロード

この処理はバッチサーバーからTDのEmbulk機能を使ってMySQLから直接TDへのアップロードを行うようになっています。
Embulkは大量データの転送を楽にしてくれる技術で、業務データのアップロードでは下記サイトを参考にしました。
Bulk Import for MySQL

まとめ

PAのメディアパートナーサイトから生成されたログをどうやって収集し、TDまで蓄積しているかを紹介させていただきました。また、ログデータが意図したログデータであるかどうかを判定する等に使ってる、業務データの処理についても簡略に説明させていただきました。次回はこのように収集されたログデータをBIツールや人が使えるようにするための処理である、正規化と集計について書いていきたいと思います。

【ログ物語】第1章 ~ログ物語の始まり~

こんにちは!オールアバウトでデータエンジニアをしている@ondaljhです。

オールアバウトはコンテンツマーケティングプラットフォームPrimeAdというサービスを提供しています。 今回のブログでは本サービス運営上でのログにまつわるお話を4回の連載企画でお届けします。

前提

本題に入る前に、今回の連載記事はADNWの中でも、効果測定のためのログやデータの集計に関する内容がメインになります。まずはどのような目的で、どのような方を対象に書いていくのかを明記したいと思います。

連載記事の目的

データエンジニアってどんなことやってるの?と思ってる方々に、実務者が今までの経験に基づいて説明することで、データエンジニアの業務について少しでも理解していただくことを目的としています。 一言でデータエンジニアといっても、ビジネス毎に扱ってるアーキテクチャやデータは異なるため、細かいところは現場毎で異なりますが、データパイプラインを構築する上での基本的な概念はそれほど変わらないと思うので、ADNWのデータエンジニアとして書いていきます。また、集計データはADNWの機械学習にも使われていますが、今回の連載では割愛させていただきます。

閲覧してほしい対象読者

  • データエンジニアって、何やってる人?と思ったことがある方
  • データパイプラインについて興味ある方
  • ADNWのログ・集計について興味ある方

持ってたほうがいい知識

  • ADNWの基本知識
  • RDBMSの基本知識
  • SQLの基本知識

上記知識がなくても読むには問題ないと思いますが、専門用語などが出てくるかと思いますので、専門用語の説明がない場合にはご了承ください。

全体構成

第1章.ログ物語の始まり

  • 1.PrimeAdでのデータパイプラインの概要
  • 2.ログのライフサイクル
  • 3.ログの種類

第2章.ログ/収集の仲間

  • 1.ログ収集
  • 2.業務データ

第3章.ログ/二つの塔

  • 1.正規化
  • 2.集計

第4章.ログ/データの帰還

  • 1.データ可視化
  • 2.レポートツールへのデータ連携
  • 3.データマート

これから「第1章.ログ物語の始まり」が始まります!

第1章.ログ物語の始まり

1.PrimeAdでのデータパイプラインの概要

PrimeAdとは

オールアバウトが提供しているコンテンツマーケティングプラットフォームで、約100のメディアと提携した広告配信からレポーティングまでをサービスしているプラットフォームです。 詳細はPrimeAdを参照ください!

データパイプライン

一般的にデータパイプラインとは、データ分析基盤、また機械学習基盤にとって、要望を満たすデータを収集、整形、準備、提供する一連のプロセスを指します。PrimeAdでのデータパイプラインも似たようなもので、PrimeAdでのADNWから発生するログの収集からデータの正規化、集計、可視化までがパイプラインによってつながっています。

2.ログのライフサイクル

PrimeAdでのライフサイクルを図式化すると下記のようになります。

f:id:allabout-techblog:20200225114059p:plain
ログのライフサイクル

一見するととても複雑に見えるかもしれません。 ですが、ところどころ切り取ってみれば難しい話ではないので、一つ一つ説明していきます。 ※各プロセスの詳細は第2回以降からになりますので、今回は概略だけ書かせていただきます。

ログの誕生

最初の方にも書いてありますが、ここで扱ってるログはあくまでも「ADNWでのログ」となります。 ご存じの方も多いかと思いますが、ログにはいろんな種類があり、種類ごとに生成されるタイミングが異なります。 基本的にはビーコンとして、ブラウザ上で発生するアクションデータがログとして生まれます。

例えば下記のようなものです。
PV(Page View) : ブラウザであるページが表示されたタイミング
Imp(Impression) : ブラウザであるページが表示される際に、ADNW等を経由して広告が表示されたタイミング
Click : ブラウザであるページからイメージリンクやテキストリンク等がクリックされたタイミング

ログの収集

f:id:allabout-techblog:20200218110929p:plain
ログの収集

PrimeAdではログ収集の際に、fluentdを使っています。各ウェブサーバーへアクセスが発生するたびに、アクセスログをポーリングしていたfluentdが検知し、ログ転送サーバーへ転送します。ログ転送サーバーはさらにログをログ集約・フィルタサーバーに転送し、ログ集約・フィルタサーバーはそのログをデータウェアハウス(以下、DWH)の一種であるTreasureData(以下、TD)へ転送します。 この辺の詳細は第2回で書いていきます。

ログの正規化と集計

f:id:allabout-techblog:20200117190002p:plain
ログの正規化と集計

正規化とは

データ等々を一定のルールに基づいて変形し、利用しやすくすること。 非常に多くの分野で使われている言葉で、分野によって意味も大きく異なります。 ※出典 Wikipedia

例えば、あるウェブページに対して、普段はURLにトレイリングスラッシュがついてるが、何らかの理由でURLにトレイリングスラッシュがなかった場合、トレイリングスラッシュを付ける等を指します。
正規化前のURL : https://allabout.co.jp/gm/gc/475094
正規化後のURL : https://allabout.co.jp/gm/gc/475094/

これにより、正規化前のログでは下記のURLは別々のものとして扱われていましたが、正規化後のログでは同じURLとして扱うことができるようになります。

https://allabout.co.jp/gm/gc/475094/
https://allabout.co.jp/gm/gc/475094

集計

正規化されたデータをベースに様々な切り口で数値の合計値をMySQLへ格納しています。 ここで言ってる切り口は、「ディメンション」を指してます。また、合計値は「指標」を指してます。 ディメンションと指標について、グーグルアナリティクスではこのような説明をしています。

  • ディメンション : データの属性です。たとえば、ディメンション「市区町村」はセッションの性質を表し、「横浜」、「川崎」などセッションが発生した市区町村を指定します。ディメンション「ページ」は、閲覧されたページの URL を表します。
  • 指標 : データを定量化したものです。指標「セッション」はセッションの合計数です。指標「ページ/セッション」は、セッションあたりの平均閲覧ページ数です。

PrimeAdではADNWの効果測定として様々なディメンションで各種データを集計しています。

正規化と集計の詳細は第3回で書いていきます。

集計データの活用

f:id:allabout-techblog:20200225114131p:plain
集計データの活用

集計処理で個別のログではなくなったデータを活用する段階です。 ADNWの効果測定として集計されたデータは直接BIツールで参照されたり、PrimeAdのウェブレポートとして活用されるため、関連システムへ連携されたりします。 この辺の詳細は第4回で書いていきます。

ここまでがPrimeAdでのログのライフサイクルになります。

3.ログの種類

第1回の最後として、PrimeAdのADNWとしてのログの種類を簡単に紹介します。

種類 概要
広告リクエス ユーザーがサイトへアクセスし、ADNWが呼ばれたことの情報を持つログ
広告インプレッション 実際に表示された広告の情報を持つログ
広告クリック ユーザーがクリックした広告の情報を持つログ
広告コンテンツPV 実際に表示された広告コンテンツ記事の情報を持つログ
読了率 ユーザーが広告コンテンツページをどこまで読んだかの情報を持つログ
滞在時間 ユーザーが広告コンテンツページへ滞在した時間の情報を持つログ

各ログの詳細情報をここに記載するのはできませんが、上記以外にも色々の種類のログを収集しています。 ※PV、CV等のADNW用語については割愛させていただきます。

まとめ

連載の第1回として、PrimeAdでのデータパイプライン、ADNW上でのログのライフサイクル、ログの種類などを簡略に紹介させていただきました。次回は集計で使われる業務データについて、また、ログ収集についてもう少し具体的に書いていきたいと思います。