Wiz テックブログ

Wizは、最新のIoTやICTサービスをお客様に届ける「ITの総合商社」です。

サポート終了迫る!CentOS7 を AlmaLinux8 へ移行してみた

f:id:wiz-akakura:20220218165105p:plain こんにちは、インフラエンジニアの赤倉です。

昨年末にCentOS8のサポートが終了したばかりですが、CentOS7のサポート期限が2024年6月末に近づいてきています。

そこで今回は、CentOSの次にくるディストリビューション候補の中からAlmaLinux8への移行を試してみたいとおもいます。

移行環境

移行元CentOSの環境は弊社でよく使っているLaravel構成としました。

Laravelは初期インストール+認証設定まで完了した環境を用意しました。

移行ツール

今回の移行ではAlamaLinuxから提供されている「Elevate」を使います。
手順は公式のクイックスタートガイドを参考にしています。

Elevateは他にも下記ディストリビューションへの移行をサポートしています。

移行の流れ

  1. 最新のCentOSアップデートをインストールして、再起動します。

     $ yum update -y
     $ reboot
    
  2. elevate-releaseプロジェクトリポジトリとGPGキーを使用してパッケージをインストールします。

     $ yum install -y http://repo.almalinux.org/elevate/elevate-release-latest-el7.noarch.rpm
    
  3. アップグレードするOSのleappパッケージと移行データをインストールします。

     $ yum install -y leapp-upgrade leapp-data-almalinux
    
    • 下記のオプションに変更することにより、移行先のディストリビューションを選択できます。

      • leapp-data-almalinux
      • leapp-data-centos
      • leapp-data-oraclelinux
      • leapp-data-rocky
  4. アップグレード前のチェックを開始します。

     $ leapp preupgrade
    

    ここで移行要件を満たしていない場合は下記のエラーメッセージが出力されます。

     ============================================================
                         UPGRADE INHIBITED                      
     ============================================================
    
     Upgrade has been inhibited due to the following problems:
         1. Inhibitor: Possible problems with remote login using root account
         2. Inhibitor: Detected loaded kernel drivers which have been removed in RHEL 8. Upgrade cannot proceed.
         3. Inhibitor: Missing required answers in the answer file
     Consult the pre-upgrade report for details and possible remediation.
    

    レポートファイル(/var/log/leapp/leapp-report.txt)を確認して修正対応します。

    弊社環境の場合、下記の通り対応しています。

    1. Inhibitor: Possible problems with remote login using root account

    sshのrootログインを許可する。

     $ echo PermitRootLogin yes | sudo tee -a /etc/ssh/sshd_config
    

    2. Inhibitor: Detected loaded kernel drivers which have been removed in RHEL 8. Upgrade cannot proceed.

    CentOS8で廃止されたカーネルモジュールを読み込まないようにする。

     $ rmmod pata_acpi
     $ rmmod floppy
    

    3. Inhibitor: Missing required answers in the answer file Consult the pre-upgrade report for details and possible remediation.

    公式ガイドに記載のあったコマンドを実行する。

     $ leapp answer --section remove_pam_pkcs11_module_check.confirm=True
    

    もう一度アップグレードのチェックコマンドを実行してエラーがないことを確認します。

     $ leapp preupgrade
    
  5. アップグレードを開始します。

     $ leapp upgrade
    
    • 途中、パッケージ重複エラーでプロセスが終了することがあります。

        Error: Transaction test error:
      file /usr/lib64/.libcrypto.so.1.1.1k.hmac from install of openssl-libs-1:1.1.1k-5.el8_5.x86_64 conflicts with file from package openssl11-libs-1:1.1.1k-2.el7.x86_64
      

      その場合は、対象のパッケージを削除して再度アップグレードを開始します。

        $ yum remove openssl-libs-*
      
    • 弊社環境では約5分でアップグレードが完了しました。また、その間もWebサービスは継続していました。

  6. アップグレード完了後、サーバを再起動します。

     $ reboot
    

    弊社環境では約15分で再起動が完了しました。

  7. AlmaLinuxにアップグレードされていることを確認します。

     $cat /etc/redhat-release 
     AlmaLinux release 8.5 (Arctic Sphynx)
    

移行後の問題

  • なぜかphp-xmlのみ消えていたので、再度インストールしました。

      $ dnf install https://rpms.remirepo.net/enterprise/remi-release-8.rpm
      $ dnf module install php:remi-7.4
      $ dnf module enable php:remi-7.4
      $ dnf install php74-php-xml
    

Webサイト(Laravel)確認

上記の問題を対応すれば、移行後の環境でもLaravelは正常に稼働していました。 f:id:wiz-akakura:20220224101806p:plain

最後に

「Elevate」 アップグレード前の問題を修正するのに時間がかかりましたが、思っていたより簡単に移行することができました。サーバの再起動に時間がかかるので実施の際はWebサービスの停止を予定しておく必要はありそうです。

インフラエンジニアの皆様も、CentOS7移行の際にはElevateお試しください!

株式会社Wizではエンジニアを募集しています!

↓↓↓興味ある方はぜひご覧ください!↓↓↓

careers.012grp.co.jp

継続は力なり!心理的安全性を高めるための取り組み

f:id:rainymoment0616:20220202165247p:plain

こんにちは、フロントエンドエンジニアの柳田です。

今までは、技術的なインプットが大半を占めていたのですが、半年前くらいから組織づくりについてのインプットを意識して行うようになりました。(技術的インプットが減ってきているので、がんばらないと…!)。

最近読んでいる本で、心理的安全性の大切さを改めて感じたので、今回はその本の紹介と、弊社で行っている心理的安全性を高める取り組みをご紹介したいと思います。

最近読んでいる本

最近、私が読んでいる本は、産業・組織心理学者の山浦一保さん著の『武器としての組織心理学 人を動かすビジネスパーソン必須の心理学』です。

組織心理学とは

組織のトラブルの原因を突き止め、うまくいっている集団に共通する「リーダーシップ」や「人間関係」を明らかにする学問

だそうです。

組織とは色んな能力のメンバーが集まっていて、個々のメンバーの能力を最大限に引き出すことはなかなか難しいことです。

リモートワークになり、今まで当たり前だった働き方から大きく変わった今、それはより難しくなっています。

この本には、組織をまとめるマネジャーの立場の人たちに、組織心理学の観点から組織のパフォーマンスを高めるためヒントが多く書かれてあります。

心理的安全性が高いことで組織はどう変わる?

この本の中では、心理的安全性とは下記の意味で展開されています。

個人がリスクテイクしても大丈夫な職場だと信じているということ

よく、心理的安全性の意味を「組織のメンバーが仲良しであること」という内容と捉えがちですが、そうではありません。

組織のメンバーが「失敗しても大丈夫!」「成功するか不安だけど、何かあったとしても助けてくれる!」と感じられるような環境であることが、本当の心理的安全性なのですね。

ざっくりとした要約になりますが、心理的安全性の高い・低いというのは、

  • 上司やメンバーとの良好な関係性
  • 職場が支援的であること
  • 学ぶ姿勢を持った個人や組織であること

の3点が影響・関与しており、このような状況が充実した環境になることで、メンバーは「意見を言ってもいいんだ!」「失敗しても良いんだ!」と安心感が生まれ、組織のパフォーマンスの向上につながっていく、といった内容でした。

この本を読んで、心理的安全性の大切さを再認識することはできましたが、心理的安全性を良くするための施策を実行しすぐに改善するということは、なかなか難しいものです。

ですが、弊社には心理的安全性を高くするべく、様々な施策を企画・実行するチームがいるのです。

We are Creative Team

エンジニアが所属しているクリエイティブチームの心理的安全性向上を目的として【We are Creative Team】、略してWCTというチームが存在しています。

このチームの前衛が、以前このテックブログでも紹介した【雑談会】を行っていた集まりになります。

現在は、私を含めた5名で定期的に様々な施策を展開しています。

今回は、WCTが定期的行っている施策を一部ご紹介します。

Creative Night

4月・8月・12月の年3回開催している、クリエイティブチームの交流会になります。

全員リモートワークなので、バーチャルオフィスを利用しての開催になります(クリエイティブチームでは普段からバーチャルオフィスを利用して業務を行っています)。

多くのメンバーと話をできるように、時間を決めてメンバーをシャッフルしたり、ミニゲームなどを盛り込んで行っています。

特に4月は、新年度で新入社員が入ってくる時期でもありますので、大々的に行います。

ちなみに去年は、参加者みんなで同じ食事を食べながらの会でした。

f:id:rainymoment0616:20220202111641p:plain
2021年4月のCreative Nightの様子

Bar Creative

毎日業務終了後にオープンする、コミュニケーションの場になります。

普段のバーチャルオフィスとは異なる環境で、読書会やもくもく会、期間限定で他事業部も巻き込んで交流会など、雑談してもよし、勉強会してもよし、な自由なスペースとなっています。

ウィンセッション

ウィンセッションは、エンジニアチームで現在試験開催中のものになります。

ウィンセッションとは、メンバーのがんばったことを発表しあい、お互いに褒め合う会のことです。

月に2回、金曜日の終業時間前に集まり、1週間で取り組んだことやがんばったことを振り返りながら、他メンバーに共有します。

今後、クリエイティブチーム全体で開催することを目標としています。

f:id:rainymoment0616:20220202121428p:plain
ウィンセッションの様子

最後に

皆さん、いかがでしたでしょうか?

もちろん、WCTだけでなく各職種チームでも、心理的安全性を高めるための施策を行っています。

このような施策は、一度行って終わりではなく、定期的に実行してこそ効果が出てくるものです。

少しずつではありますが、組織のパフォーマンスを最大化すべく、組織全体で心理的安全性の向上に取り組んでいきたいと思っています。

こんなチームのいる会社で一緒に働いてみませんか?

株式会社Wizではエンジニアを募集しています!

↓↓↓興味ある方はぜひご覧ください!↓↓↓

careers.012grp.co.jp

【Laravel】L5-Swaggerを導入しつつBasic認証も設定する

f:id:hnak0210:20220124170246p:plain:w730

こんにちは、バックエンドエンジニアの中嶋です。

昨今のプロダクト開発ではOpenAPIを用いたスキーマ駆動で行うことが多くなってきました。

もちろん規模や目的によって何を導入すべきか、あるいはスプレッドシートやチャットでの共有で済ませてしまうかは検討が必要ですが、今回はその中の選択肢のひとつとして、LaravelプロジェクトへのL5-Swagger導入とBasic認証の設定について簡単に紹介したいと思います。

1. 前提

確認環境

  • Laravel 8.67
  • L5-Swagger 8.0.9

OpenAPIとSwaggerとL5-Swagger

OpenAPIは「RESTful APIの仕様を記述するフォーマット」で、
SwaggerはSmartBear社が提供する「OpenAPIを便利に扱うためのツール」の一つです。

What is OpenAPI? Swagger vs. OpenAPI | Swagger Blog

ざっくり、要は「どんなURIAPIがあってどんなリクエストとレスポンスなのかを定義したり共有できるもの」と言えましょう。

そして今回紹介するL5-Swaggerは、Swaggerの機能をLaravelプロジェクトで使えるようにしたライブラリです。

github.com

Laravelのプロジェクトの中でスキーマを定義すれば「/api/documentation」にアクセスすることで以下のようなページが生成&表示でき、APIの実行例を示したりPostmanのように実際にリクエストを送信することもできます。

f:id:hnak0210:20220124161344p:plain

2. L5-Swaggerの導入

Composerでインストールする

LaravelプロジェクトにおいてComposerでインストールします。
今回はLaravel8系を使用しています。

composer require "darkaonline/l5-swagger"

JSONスキーマを作成する

スキーマはPHPDocにアノーテーションで記述する方法とJSONファイルを直接記述する方法があります。

●アノーテーションの例

提供されているサンプルの記法に倣ってController等のPHPDocにアノーテーションを記述していきます。

/**
 * @OA\Get(
 *      path="/projects/{id}",
 *      operationId="getProjectById",
 *      tags={"Projects"},
 *      summary="Get project information",
 *      description="Returns project data",
 *      @OA\Parameter(
 *          name="id",
 *          description="Project id",
 *          required=true,
 *          in="path",
 *          @OA\Schema(
 *              type="integer"
 *          )
 *      ),
 *      @OA\Response(
 *          response=200,
 *          description="successful operation"
 *       ),
 *      @OA\Response(response=400, description="Bad request"),
 *      @OA\Response(response=404, description="Resource Not Found"),
 *      security={
 *         {
 *             "oauth2_security_example": {"write:projects", "read:projects"}
 *         }
 *     },
 * )
 */

記述し終わったら

php artisan l5-swagger:generate

を実行することでSwaggerドキュメントが生成されます。

なおドキュメント生成については.envファイルに、

L5_SWAGGER_GENERATE_ALWAYS=true

を記載すれば、ロードするたびに自動で実行してくれるように設定することもできます。

github.com

JSONの例

プロジェクトの/storage配下に/api-docs/api-docs.jsonを作成し、OpenAPI Specificationの記法にて記述していきます。

{
  "/pets": {
    "get": {
      "description": "Returns all pets from the system that the user has access to",
      "responses": {
        "200": {          
          "description": "A list of pets.",
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/pet"
                }
              }
            }
          }
        }
      }
    }
  }
}

JSONで直接定義する場合は特にコマンド実行は不要です。

なおスキーマはブラウザ版のSwaggerEdditerで編集・記述したり、エラーがないかの検証をすることができます。

https://editor.swagger.io

画面で表示してみる

スキーマが作成できたら/api/documentationにアクセスします。

プロジェクトにファイルが含まれているので、ローカル環境でも検証サーバーの環境でもアクセスできるのが確認できると思います。

開発の初期段階で仮のレスポンスを返すControllerを定義してサーバーに置いておけば、フロントエンドと共有できるモックサーバーとしても使えそうですね。

3. Basic認証を設定する

作成したAPIドキュメントは便利に使えるのですが、このままでは世界中にSwaggerドキュメントを公開した状態になってしまうのでBasic認証を設定していきます。

LaravelにおいてBasic認証を設定する方法としては、

の3つになるかと思います。

標準の「auth.basicミドルウェア」はDBを使用する形式であることとフレームワークのAuthライブラリに依存してしまうこと、またBasic認証のために他の外部ライブラリに依存したくないという考えから、今回は独自ミドルウェアを作成して設定していきます。

ミドルウェアを作成

適当な分かりやすい名称でミドルウェアを作成します。

php artisan make:middleware OriginalBasicAuthMiddleware

App/Http/Meddleware配下にファイルが生成されるので、中身を実装していきます。 今回の例では実行環境によってenvファイルに定義したユーザー名とパスワードを適用するように設定しています。

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class OriginalBasicAuthMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        if (config('app.env') === 'local' || config('app.env') === 'testing') {
            return $next($request);
        }

        $username = $request->getUser();
        $password = $request->getPassword();

        if ($username == config('app.basic_auth_username')
            && $password == config('app.basic_auth_password')) {
            return $next($request);
        }

        header('WWW-Authenticate: Basic realm="plase user and passwoard!"');
        header("HTTP/1.0 401 Unauthorized");
        abort(401);

        return $next($request);
    }
}

kernel.phpに追記

ミドルウェアが作成できたら、kernel.phpにも忘れずに追記して有効にしておきます。今回はルートミドルウェアとして設定します。

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'org.basic.auth' => \App\Http\Middleware\OriginalBasicAuthMiddleware::class, // 追加
    ];

ルーティング設定を変更

最後に、適用させたいルートにミドルウェアを適用させます。 これにはvendorのconfigファイルをpublishする必要があります。

php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"

/config/.phpが作成されるので、「Route Group options」の項目に作成したミドルウェアを設定します。

   〜略〜
            /*
             * Route Group options
            */
            'group_options' => [
                'middleware' => ['org.basic.auth'] // 追加
            ],
        ],
   〜略〜

アクセスしてみる

認証を設定した環境においてブラウザアクセスしてみると、きちんとBasic認証を求められるように設定できました。

f:id:hnak0210:20220124174042p:plain:w350

あとはenvファイルに設定したユーザー名とパスワードを入力すればSwaggerドキュメントを表示することができます。

まとめ

今回はLaravelにおけるL5-Swaggerの導入とBasic認証の設定方法について紹介しました。
なおスキーマの定義方法は、アノーテーションJSONかymlファイルか、Stoplightを使うか等、日々議論していたりします。

これからもフロントエンドはSPAでバックエンドはAPIを提供するという構成は多いと思いますので、 チームメンバーとも話し合いながらより良い開発手法を模索していきたいと思います。


さいごに..

Wizではみんなで良いものを楽しく作るべく、積極的にエンジニアを募集しております。

↓↓興味がある方はぜひご覧ください!

careers.012grp.co.jp

【Laravel/TDD】100日後でも死なないテスト駆動開発のやり方〜Laravel8編〜

f:id:t-yone3:20211206131321j:plain こんにちは、Wizプロダクト開発チームの米山です。

なんだか名前負けしそうなタイトルですが(笑) 今回はTDD(テスト駆動開発)の、主にテスト手法について書いてみようと思います。

TDDは、弊社の開発手法にも取り入れており、通常のMVCモデルのソースコードの他、PHPUnitで動かすためのテストコードも作成しています。

テストコードの書き方は人それぞれなのですが、私が約半年TDDを実践してみて「だいたいこんな感じで良いかな」というようなスタンダードモデルができてきたので紹介したいと思います。

setUpBeforeClassとsetupは必ず作成する

setUpBeforeClassは全テストケースで最初に1度だけ、setUpはテストケース毎に動作するメソッドです。

私の場合は、public/index.phpで行っている操作(例えば、定数「LARAVEL_START」の定義など)をsetUpBeforeClassで行い、setUpでテストケース実行前にDBクリアを行うなどをしています。

public static function setUpBeforeClass(): void
{
    if (!defined('LARAVEL_START')) {
        define('LARAVEL_START', microtime(true));
    }
}

public function setup(): void
{
    parent::setUp();

    $this->clearDB();
}

メソッド名にケース番号とケース内容を入れてわかりやすくする

この辺は個人のやり方で異なる部分ですが、私の場合は「テストケースがいくつあって、何番目のケースなのか」がわかるよう、テストケースメソッドにはケース番号と確認したいことを入れています。

例えば

/**
 * @test
 */
public function Case1_Getするとステータス200が返る(): void
{
    // 実際のテストコード
}

こんな感じです。 こちらについては、別途「php-code-coverage」というライブラリも併用している関係で、あるコードはどのテストケースで通ったのかを視覚的に確認する時にも有効な手段です。

テストデータはFactoryを有効活用する

この記事の執筆時点のLaravelバージョンは8.67なのですが、Laravel8系になってFactoryはクラスとして定義できるようになりました。

各Modelクラスに「HasFactory」トレイトをuseすることで使用可能になります。

単に値を自動生成するだけでなく、テストケースによって固定値を入れておきたいケースもあると思います。

どちらでも対応できるよう、デフォルトの「definition」とは別にメソッドを作っておきます。

class ItemFactory extends Factory
{
    protected $model = Item::class;
    public function featureTestSetup(array $attributes = []): ItemFactory
    {
        $replaceData = collect($attributes);
        return $this->state(fn (array $attributes) => [
            'id' => $replaceData->get('id') ?? $this->faker->randomNumber(),
            'name' => $replaceData->get('name') ?? $this->faker->name(),
        }
    }
}

こうしておけば、引数$attributesに入っているものは固定値を使うことができますし、直接テストには関係ないが、DB上必要な項目はランダムにfakerを使って値を生成できます。

TestCaseとTestDataの責務を分ける

Laravelのテスト、すなわちPHPUnitには「DataProvider」という機能があります。

これを利用すると、テストケースメソッドの開始時にテストデータが入った状態(メソッドの引数にある状態)でテストが開始できます。

が、私は敢えてこれを使わずにコードを書きます。

理由はテストケースとテストデータ作成のコードを両方1つのクラスに書き込むことで、コードが肥大化しメンテナンスしづらくなるからです。

テストケースにはテストケースだけを書いておく。データ作成は別のところでやる。

私はこれをトレイトを使って実現しました。

例えば

class GetItemsTest extends TestCase
{
    /**
     * @test
     */
    public function Case1_Getするとステータス200が返る(): void
    {
        // 実際のテストコード
    }
}

というテストコードがあったとして、DataProvidorを使うと

class GetItemsTest extends TestCase
{
    /**
     * @test
     * @dataProvider case1_data
     */
    public function Case1_Getするとステータス200が返る($data): void
    {
        // 実際のテストコード
    }

    public function case1_data(): array
    {
        return [
            // ここにデータを書く
        ];
    }
}

となるのですが、これをトレイトに置き換えると

class GetItemsTest extends TestCase
{
    use ItemTestData;

    /**
     * @test
     */
    public function Case1_Getするとステータス200が返る(): void
    {
        $this->createTestData();

        // 実際のテストコード
    }
}

というコードにすることができます。

「なんで便利な機能(DataProvider)があるのに使わないんだ?」とギモンを持つ方もいるかもしれません。

コードを分けることは前述した通り、メンテナンスしやすくすることが目的ですが、トレイトにすることでいくつかのテストで共有して利用することが可能です。

例えば、上の例では「ItemTestData」というトレイトを使い、テストデータを作成しますが、他のケースでもこの「ItemTestData」を利用したければuseすることで使用可能になります。

テストケースごとにDataProviderを書く必要はありません。

ここまでのまとめ

いかがだったでしょうか。テスト駆動開発といっても人によって、あるいは言語によって様々なスタイルがあると思いますが、今回はLaravel(PHPUnit)でのやり方を紹介しました。

  • setUp系のメソッド(初期化処理)は必ず行う
  • テストケースメソッド名はケース番号とやりたいことを書き、わかりやすくする
  • データ作成はFactory機能を使って柔軟に作成
  • DataProviderではなくtraitを使ってテストデータを作成する

1つできれば使いまわして量産することも可能です。参考になれば幸いです。


Wizではエンジニアを募集しております。

興味のある方、ぜひご覧下さい。

careers.012grp.co.jp

突撃!隣のリモート環境 ~推しキーボード編~

こんにちは!フロントエンドの松本です。

現在、Wizで働くエンジニアチームは殆どが在宅勤務をしております。 社内Slackの雑談チャンネルでは、ガジェットやリモート環境をシェアするチャンネルがあったりと、リモート環境のアップデートに力を入れているメンバーも少なくないです。

今回は突撃!隣のリモート環境 第1弾として、エンジニアの皆様に推しキーボードを教えてもらいました。

それではさっそくご紹介します。

【Vortexgear Tab75 茶軸】 KDMさん

f:id:yukiji_03:20211118154737j:plain

推しポイント

  • 無線
  • 84キーというちょうどいいサイズ感
  • 複数台とペアリングできる

【Niz Plum82】 SSKさん

f:id:yukiji_03:20211118155035j:plain

推しポイント

  • Win、Mac両対応
  • スイッチ : 静電容量無接点
  • キーキャップがCHERRY MX互換
  • APC機能で反応ポイントを調節できるので地味に良いです
  • サイズ感と打鍵音がスコスコという音で気に入ってます

【Keychron K8】 MTIさん

f:id:yukiji_03:20211118155232j:plain

推しポイント

  • Win、Mac両対応
  • 有線 無線切り替え可能
  • 軸も変更可能(赤、青、茶)
  • ホットスワップ対応モデルなので簡単にスイッチを交換できる
  • 別売りのパームレストが木製でかっこいい!

【Magic Keyboard(JIS)】 YKさん

f:id:yukiji_03:20211118155521j:plain

推しポイント

【HHKB HYBRID Type-S】 タコライスNさん

f:id:yukiji_03:20211119165438p:plain

推しポイント

  • 言わずもがな
  • 指が喜んでます

【7V】sevenium777さん

f:id:yukiji_03:20211119165622j:plain

推しポイント

  • やわらかい打鍵感
  • 低音で心地よい打鍵音
  • スムーズなスイッチ
  • 重い(重さは正義!)

logicool G512】DSKさん

f:id:yukiji_03:20211124101330j:plain

推しポイント

  • 光ってるw
  • うるさ過ぎない

推しじゃないポイント

  • 有線
  • テンキーあり

最後に

こうやってズラッと並べると購買欲が上がってきてしまいます。
打鍵感がいいとコードを書くのが楽しくなりそうですね。

Wizではエンジニアを募集しております。
興味のある方、ぜひご覧下さい!

careers.012grp.co.jp

Apacheのデータをrubyで整える

こんにちは。バックエンドエンジニアの河内です。

先日、Apacheのログデータの解析結果をレポートするタスクが入ってきました。われわれの業務の大半はプロダクト開発なのですが、その中には、実際に要件を策定しコードを書いていく純粋な開発タスクもあれば、その開発のための技術調査(スクラムであれば「スパイク」でしょうか)もあります。

前者はなんとなく工数を把握したうえであとは実装していく…といった感じですが(それでもスケジュール管理は欠かせません)、後者は暗中模索でおこなわなければならないタフなタスクです。

また、それとは別に、開発中の案件とは関係なく不具合などの調査が入ってくることがあるかと思います。なんとなくやることは見えていて、あとはどれだけ速くこなすか…といったタイムトライアル的なタスクです。今回のログデータ解析タスクもそんな分類になるのかな、と思っています。

作業概観

さて、ログを調べた結果、今回解析が必要なデータは以下の5行と導けました。*1

IPアドレスなど、一部の情報は適当にマスクしてます。下記の内容をlogsというファイル名で保存します。

000.000.000.000 - - [02/Nov/2021:15:41:14 +0900] "POST /awesome HTTP/1.1" 200 5009 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/181.0.401558652 Mobile/15E148 Safari/604.1"
000.000.000.000 - - [04/Oct/2021:20:04:09 +0900] "POST /awesome HTTP/1.1" 200 4868 "https://xxx.dev/awesome" "Mozilla/5.0 (Linux; Android 11; SO-03L Build/55.2.D.0.447; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/94.0.4606.61 Mobile Safari/537.36 Line/11.17.1/IAB"
000.000.000.000 - - [04/Oct/2021:19:49:50 +0900] "POST /awesome HTTP/1.1" 200 4870 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Safari Line/11.17.0"
000.000.000.000 - - [04/Oct/2021:18:11:32 +0900] "POST /awesome HTTP/1.1" 200 4865 "https://xxx.dev/awesome" "Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1"
000.000.000.000 - - [03/Oct/2021:01:33:15 +0900] "POST /awesome HTTP/1.1" 200 4866 "https://xxx.dev/awesome" "Mozilla/5.0 (Linux; Android 9; SO-04J) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Mobile Safari/537.36"

前回、別のメンバーが同様の対応をおこなったときのフォーマットは以下のとおりだったそうなので、それに従います。

・アクセス日時: 2021-01-01 00:00:00
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.8
    ブラウザ: Google
    ブラウザのバージョン: 181.0.401558652
    デバイス: iPhone

あとは文字列を拾って特定フォーマットに落とし込めばよいのですが…対象は複数行あります。目で追って手で拾う…というのは不正確ですし疲れるので避けたいです。

また、こういうタスクは一回やりおおせても、おかわりとして同じような作業依頼が再びやってくるのが常です。

せっかくテキストは規則的に並んでいることですし、ちゃちゃっとコードを書くことにします。こういうときに自分はrubyを使うことが多いです*2

今回のようなタスクを何度かこなしているので、rbenvで特定バージョンのrubyが動作する環境が用意されています(今回の例はruby 2.6.3p62)。

作業ディレクトリの中は、以下のような感じに配備しました。logsは先ほどの5行のログが書かれたファイルで、処理をおこなうreport.rbがあり、処理後にレポート内容がREPORT.txtに吐き出されるようにします。

./
  logs
  report.rb
  REPORT.txt(report.rbにより生成される)

コードを書いていく

ログをパースする

それでは、report.rbにコードを書いていきます。Apacheログのパーサーライブラリを使い、以下のように書きました。ログフォーマットは、Apacheのものをそのまま流用できるようです。

require 'apachelogregex'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)
    puts result; exit # 出力確認
end

コードで一行目パースしたところで止めてますが、こんな感じで取得できます。

{"%h"=>"000.000.000.000", "%l"=>"-", "%u"=>"-", "%t"=>"[02/Nov/2021:15:41:14 +0900]", "%r"=>"POST /awesome HTTP/1.1", "%>s"=>"200", "%b"=>"5009", "%{Referer}i"=>"https://xxx.dev/awesome", "%{User-Agent}i"=>"Mozilla/5.0 (iPhone; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) GSA/181.0.401558652 Mobile/15E148 Safari/604.1"}

日時を読みやすくする

Apacheの日時をrubyでフォーマットする…誰かやってそうですよね? されてる方がいらっしゃったので拝借します。

require 'apachelogregex'
+ require 'time'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

+    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")
    puts datetime; exit # 出力確認
end

日時も読みやすくなりました。

2021-11-02 15:41:14

ホスト名を取得する

require 'apachelogregex'
require 'time'
+ require 'resolv'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")

+     host_name = Resolv.getname(result["%h"])
    puts host_name; exit # 出力確認
end

サンプルではIPアドレスが適当ですが、実際のIPでアドレスではホスト名も取得できました。

ユーザーエージェントから各種情報を取得する

ユーザーエージェントはとくに目で見ての作業はめんどうなので、ライブラリに任せましょう。

require 'apachelogregex'
require 'time'
require 'resolv'
+ require 'user_agent_parser'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")

    host_name = Resolv.getname(result["%h"])

+     user_agent_part = result["%{User-Agent}i"]
+     user_agent = UserAgentParser.parse(user_agent_part)

+     os_name, os_version = user_agent.os.to_s.split(" ")

+     browser_version = user_agent.version.to_s
+     browser_name = user_agent.family.to_s

+     device_name_family = user_agent.device.family.to_s

+     device_name = device_name_family == "iPhone" ? device_name_family : "#{user_agent.device.brand} #{user_agent.device.model}"
end

iPhoneAndroidのときの表示で違和感があったので、条件分岐してます*3

整形する

仕上げに入っていきます。フォーマットに従い出力します(ログ一行ごとのブロックで適当に仕切り線を入れて表示)。

require 'apachelogregex'
require 'time'
require 'resolv'
require 'user_agent_parser'

format = '%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"'
logParser = ApacheLogRegex.new(format)

texts = File.foreach('./logs').map do |line|
    result = logParser.parse(line)

    datetime = Time.strptime(result["%t"], '[%d/%b/%Y:%H:%M:%S %z]').strftime("%Y-%m-%d %H:%M:%S")

    host_name = Resolv.getname(result["%h"])

    user_agent_part = result["%{User-Agent}i"]
    user_agent = UserAgentParser.parse(user_agent_part)

    os_name, os_version = user_agent.os.to_s.split(" ")

    browser_version = user_agent.version.to_s
    browser_name = user_agent.family.to_s

    device_name_family = user_agent.device.family.to_s

    device_name = device_name_family == "iPhone" ? device_name_family : "#{user_agent.device.brand} #{user_agent.device.model}"

+     text = <<~"TXT"
+         ・アクセス日時: #{datetime}
+         ・IPアドレス: #{result['%h']}
+         ・IPアドレスのホスト名: #{host_name}
+         ・UserAgentからの情報
+             OS: #{os_name}
+             OSのバージョン: #{os_version}
+             ブラウザ: #{browser_name}
+             ブラウザのバージョン: #{browser_version}
+             デバイス: #{device_name}
+     TXT
end

+ File.write('REPORT.txt', texts.join("-" * (50) + "\n"))

出力結果は以下のような感じです。

・アクセス日時: 2021-11-02 15:41:14
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.8
    ブラウザ: Google
    ブラウザのバージョン: 181.0.401558652
    デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-04 20:04:09
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: Android
    OSのバージョン: 11
    ブラウザ: LINE
    ブラウザのバージョン: 11.17.1
    デバイス: SonyEricsson SO-03L
--------------------------------------------------
・アクセス日時: 2021-10-04 19:49:50
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.2
    ブラウザ: LINE
    ブラウザのバージョン: 11.17.0
    デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-04 18:11:32
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.7.1
    ブラウザ: Mobile Safari
    ブラウザのバージョン: 14.1.2
    デバイス: iPhone
--------------------------------------------------
・アクセス日時: 2021-10-03 01:33:15
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: Android
    OSのバージョン: 9
    ブラウザ: Chrome Mobile
    ブラウザのバージョン: 94.0.4606.61
    デバイス: SonyEricsson SO-04J
--------------------------------------------------
・アクセス日時: 2021-10-02 13:52:21
・IPアドレス: 000.000.000.000
・IPアドレスのホスト名: XXX
・UserAgentからの情報
    OS: iOS
    OSのバージョン: 14.7.1
    ブラウザ: Mobile Safari UI/WKWebView
    ブラウザのバージョン: 
    デバイス: iPhone

まとめ

タイムトライアル的なタスクは、ありもののライブラリや先人のナレッジ、自分の過去のメモを生かしてさくっと解決していく必要があります。一方で、一回だけでなく何度かおかわりが来ることを想定し、再現できるようにしておくとよいのかな、と思います。

最後に

Wizではエンジニアを募集しております。 興味のある方、ぜひご覧下さい。

careers.012grp.co.jp

*1:この行を特定するのにも要件からDBから特定条件でクエリを投げ、得られた日時とログデータのアクセス日時を突合しなければならない…というタスクがありました。

*2:みなさんも、手になじんだ言語があるのではないでしょうか。

*3:たとえば、iPadなどはなかったので今回は考慮してません。アプリ開発でないので、とりあえず条件を満たして早さ優先。

express(nodejs)+node.jsからmysqlに接続してデータベースを作成する

f:id:thunder_fury:20211115154050p:plain

はじめに

皆さんこんにちは、フロントエンドエンジニアのWooです。⚡️🌪

バックエンドはexpress(Node.js)を使ってMySqlに接続しクライアント側はNext(react)を使って簡単データベースを作成してみたいと思います。

自分の場合はsql文が分からないのではデータベース管理するMySQLWorkbenchを使いました。

express

expressは、Webおよびモバイルアプリケーションのための一連の強力な機能を提供する、簡潔で柔軟なNode.js Webアプリケーションフレームワークです。事実上Node.jsの標準的なWebサーバフレームワークに付けて起動だけ多くの場所で使用されているようです。

expressjs.com

MySQLWorkbenchとは

公式からの説明

MySQL Workbench は、データ・モデリングSQL 開発、およびサーバー設定、ユーザー管理、バックアップなどの包括的な管理ツールを提供します。MySQL Workbench は WindowsLinuxMac OS X で利用可能です。

データベース自分の好みで管理ツールを導入しても良いと思います。自分は「Mac MySQL Sequel Pro」と「MySQLWorkbench」どちらかで悩みましたが「MySQLWorkbench」にしました。

sequelpro.com www.mysql.com

データベーステーブル

MySQLWorkbenchを使ってmemberスキーマを作成しidは重複できないようにして user_email user_name password三つのテーブルを用意しました。

f:id:thunder_fury:20211115143710p:plain

全体ディレクトリー構成

├── client ( Next.js基本ディレクトリー)
└── api
     ├── config
     |     └──database.js
     └── server.js

パッケージinstall

 $ npm init -y
 $ npm i mysql
 $ npm i express

npm初期化の後mysqlとexpressをインストールします。

データベースconnection

データベースのconnection処理が必要です。 mysqlをimportして自分はデータベースの情報を返してくれる共通関数して使い回しできるように書いてみました。

データベース情報はセキュリティのためenvに書いた方が良いです。

api/config/database.js

const mysql = require('mysql');

const database = () => {
  const connection = mysql.createConnection({
    host: `${process.env.MYSQL_HOST}`,
    user: `${process.env.MYSQL_USER}`,
    password: `${process.env.MYSQL_PASSWORD}`,
    database: `${process.env.MYSQL_DATABASE}`,
  });
  return connection;
};

exports.database = database;

POST API用意

/api/sign_up のルートにポストする場合「member」のデータベースに格納できるように書いています。

api/server.js

const express = require('express');
const app = express();
const port = process.env.PORT || 3090;

app.post(`/api/sign_up`, (req, res) => {
    const { email, password, user_name } = req.body;
    const params = [email, password, user_name];
    const sql = `INSERT INTO member VALUES (null, ?, ?, ?)`;
    database().connect();
    database().query(sql, params, (err, rows, fields) => {
      res.header(`Content-Type`, `application/json; charset=utf-8`);
      res.status(200).send({ reow: rows });
    });
    database().end();
  });

app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});

INSERT INTO member VALUES (null, ?, ?, ?) は上記MySQLWorkbenchから作成したテーブルと繋いでいます。(null, ?, ?, ?) は(id, user_mail, user_name, password )です。

ここまでバックエンドの処理は完了となります。

server立ち上げ

apiの直下で実行

 node ./server.js

これでAPIの使用が可能になります。

ポスト入り口用意

nodejs.org

データベースにPOSTするため簡単なフォーム作成します。

import { useState } from 'react'
import Axios from 'axios'
import { css } from '@emotion/react'
export const SignUp = () => {
  const [ userEmeil, setUserEmail] = useState(``)
  const [ userPassword, setUserPassword] = useState(``)
  const [ userName, setUserName] = useState(``)
  const submit = async () => {
    console.log(userPassword,userEmeil )
    await Axios.post(`/api/sign_up`, { 
      email: userEmeil,
      password: userPassword,
      user_name: userName
    })
      .then(res => {
        console.log(res)
      }).catch(err =>{
        console.log(err)
      })
  }
  return (
    <div css={css`
      max-width: 300px;
      width: 100%;
      margin: 0 auto;
    `}>
    <>
      <h1>Sign Up</h1>
      <div>
        <label htmlFor={`user_email`}>Mail Address : </label>
        <input
          id={`user_email`} 
          type={`email`}
          name={`user_email`}
          onChange={(e:React.ChangeEvent<HTMLInputElement>) => {
          setUserEmail(e.target.value)
          }}
        />
      </div>
      <br />
      <div>
        <label htmlFor={`user_name`}>User Name : </label>
        <input 
          id={`user_name`} 
          type={`text`} 
          name={`user_name`}
          placeholder={``}
          onChange={(e:React.ChangeEvent<HTMLInputElement>) => {
            setUserName(e.target.value)
          }}
        />
      </div>
      <br />
      <div>
        <label htmlFor={`password`}>password : </label>
        <input 
          id={`password`}
          name={`password`}
          type={`password`}
          onChange={(e:React.ChangeEvent<HTMLInputElement>) => {
            setUserPassword(e.target.value)
          }}
        />
      </div>
      <button
        css={css`
          text-align: center;
          background: black;
          color: white;
          padding: 5px;
          margin-top: 10px;
        `}
        onClick={submit}
      >
      Sign Up
      </button>
    </>
      
    </div>
  )
}

export default SignUp

このようなサブミットフィールドになります。 f:id:thunder_fury:20211115144115p:plain

データベース確認

f:id:thunder_fury:20211115145008p:plain MySQLWorkbenchを確認してみたらちゃんとデータは格納されてるのが確認できました。⚡️🌪 今回のPOST+Nodemailerと組み合わせをして自動返信メール機能を入れても🤔良さそうな気がしました。


最後になりますが、Wizではエンジニアを募集中です!

興味のある方は是非覗いてみてください!↓

careers.012grp.co.jp