スタイル・エッジ技術ブログ

士業集客支援/コンサルティングのスタイル・エッジのエンジニアによるブログです。

2022年もアドベントカレンダー開催しました!

はじめに

こんにちは!今年スタイル・エッジ・グループに新卒入社したべぷおじです。
入社してもう半年以上経ちました。月日の流れは早いものです…

さて、我々スタイル・エッジ・グループのシステム事業部では、ナレッジを通した社員同士の交流を兼ねてアドベントカレンダーを毎年開催しています。
2022年は総勢59名でアドベントカレンダーを開催いたしました!

そもそもアドベントカレンダーとは?

アドベントカレンダーとは、本来クリスマスまでの期間に日数を数えるために使用されるカレンダーのことですが、 IT業界では12月1日から25日まで1日に1つずつ、技術やマメ知識などを記事として投稿していくイベントのことを指します。

システム事業部では

アドベントカレンダーは、前身のスタイル・エッジLABO時代から数えると、2022年で6年目になります。
今回はクリエイティブチームの合流もあり、参加人数が大幅に増えたので、多い日には1日に件もの記事が投稿されることになりました。

記事が投稿されている様子
今回は記事に関してのコメントをチャットツールの各自の分報チャンネルでやり取りすることになり、毎日たくさんのコメントで盛り上がっていました!
みなさん個性豊かな記事ばかりで毎日とても楽しかったです!

以下、個人的におもしろかった記事をいくつか紹介します。

個人的に面白かった記事の紹介

リバースエンジニアリングについて

バイナリファイルを解析している様子
この記事では、リバースエンジニアリングを活用し、バイナリファイルを解析していました。 エンジニアとして、バイナリファイルを解析できるようになりたいと思いました。

Figmaを使ってアドベントカレンダーを作ってみよう!

完成したアドベントカレンダー
この記事では、Figmaを活用して、おしゃれなアドベントカレンダーを作成していました。 デザインは今後ますます重要になると感じているので、こういったツールも使いこなせるようになりたいですね。

キングオブアドベント

システム事業部では、記事を投稿するだけでなく、全員が投稿し終わったあとにアンケートを集計し、アドベントカレンダーの中で一番よかった記事を決定します。
一番得票数の多かった記事は、キングオブアドベントとして、この上ない栄誉が与えられるのです。

今年のキングオブアドベントは、なんと史上初の2名同時受賞となりました…!
今回はキングオブアドベントを受賞した記事のうち1つを紹介します。

今年もあざざます

モデリングでキャラクターを作成している様子

モデリングでキャラクターを作成する記事で、製作の過程は思わずすごいっ…と声がもれるほどの出来でした。
自分もやってみたいと思えるような素敵な記事だったと思います…!!

もう一つの記事は紹介できませんでしたが、身近な例でRPAの利便性やその魅力が伝わるような面白い記事でした!

まとめ

アドベントカレンダーは、もちろん技術や知識披露の場という面もありますが、 普段あまり関わりのない社員同士での交流の機会にもなっています。

会社としてどんどん規模が大きくなっていきますが、こういった社員同士の繋がりやコミュニケーションは大切にしていきたいです!

最後に

☆スタイル・エッジ・グループでは、一緒に働く仲間を募集しています☆
もし興味を持っていただけましたら、採用サイト↓も覗いてみてください!
recruit.styleedge-labo.co.jp

PHPもアトリビュートでAOP!!

はじめに

こんにちは!未だにPHP8.1で登場したEnumに心躍らせているSHISOです。

さて、今回はJavaのSpringフレームワークでできるアスペクト指向プログラミング(AOP)に憧れ、PHP8.0でリリースされたアトリビュートを使用して同じようにAOPを実現させてみました。
(ちなみにPHP8.0未満であってもLaravel 5.x~8.xをお使いであれば、Laravel-Aspectというパッケージを使ってAOPできるそうなので、興味のある方は試してみてください。)

先に今回の実装サンプル一部をチラ見せすると、クラス内に個別のgetter処理を書かずとも、アトリビュートの指定だけで値参照ができるようになったりします✨

<?php
class ExampleClass extends BaseClass
{
    public function __construct(
        #[Getter] protected string $hoge,
    ){}
}
$exampleClass = new ExampleClass('fugafuga');
// protectedなプロパティなのに 'fugafuga'と出力できる!
var_dump($exampleClass->hoge);

アトリビュートとは

アトリビュートを使うと、 コンピューターが解析できる構造化されたメタデータの情報を、 コードの宣言時に埋め込むことができます。
つまり、クラス、メソッド、関数、パラメータ、プロパティ、クラス定数にアトリビュートを指定することができます。
アトリビュートで定義されたメタデータは、 実行時に リフレクションAPI を使って調べることが出来ます。
よって、アトリビュートは、 コードに直接埋め込むことが出来る、 設定のための言語とみなすことができます。

引用元|https://www.php.net/manual/ja/language.attributes.overview.php

つまり、アトリビュートとはクラスやメソッド・プロパティなどに付与できる追加情報のことです。
このアトリビュートを付与することで、本来のロジックと直接的に関係ない情報を追加できます。

各メソッドやプロパティに付与されたアトリビュートは、リフレクションAPI を使用して取得できるため、アトリビュートの有無や引数から、共通化した処理の対象として扱うか、どういった振る舞いをさせるかなどを判断させることができます。

詳しい使い方はPHPドキュメントを参照ください。

概要

今回はアトリビュートを使用し、getterとトランザクション制御を共通化(分離)してみました。

実装の流れ

  1. アトリビュートを定義
    例: #[Attribute] class Getter {}
  2. 基底クラスやトレイトに共通処理を実装
  3. マジックメソッド内にて、アトリビュートが付与されている対象にのみ(2)の処理を実行するように実装
    例: $reflectionProperty->getAttributes(Getter::class);
  4. 共通処理を付与したい関数やプロパティに、(1)で定義したアトリビュートを指定
    例: #[Getter] protected string $hoge

最終的なコード*1

getter処理を共通化する例

<?php

#[Attribute]
class Getter
{}

/** getter処理を共通化した抽象クラス */
abstract class BaseClass
{
    public function __get(string $name): mixed
    {
        // 指定されたプロパティにGetterアトリビュートが付与されている場合、プロパティの値を返す
        $reflectionProperty = new ReflectionProperty($this, $name);
        $reflectionAttributes = $reflectionProperty->getAttributes(Getter::class);
        if (!count($reflectionAttributes)) {
            return $this->{$name};
        }

        throw new Exception('アクセス不能なプロパティです。');
    }
}

class ExampleClass extends BaseClass
{
    public function __construct(
        // 参照可能にする場合は、アトリビュート#[Getter]を付与する
        #[Getter] protected string $hoge,
    ){}
}

$exampleClass = new ExampleClass('fugafuga');
  // 'fugafuga'と出力される
var_dump($exampleClass->hoge);

トランザクション処理を共通化する例

<?php

#[Attribute]
class Transactional
{
    public function __construct(public readonly bool $needTransaction){}
}

/** 仮DB処理クラス */
class DB
{
    public static function beginTransaction(){}

    public static function commit(){}

    public static function rollback(){}
}

/** トランザクション処理を共通化した抽象クラス */
abstract class BaseClass
{
    public function __invoke(mixed ...$args): mixed
    {
        if (! (method_exists($this, 'handle') && (new \ReflectionMethod($this, 'handle'))->isProtected())) {
            throw new Exception('アクセス権がprotectedのhandleメソッドを定義してください。');
        }

        // Transactional属性を取得
        $handleMethod = new ReflectionMethod($this, 'handle');
        $transactionalAttributes = $handleMethod->getAttributes(Transactional::class);
        if (!count($transactionalAttributes)) {
            throw new Exception('Transactional属性にて、トランザクションの要否を指定してください。');
        }

        $needTransaction = $transactionalAttributes[0]->newInstance()->needTransaction;
        if ($needTransaction === false) {
            return $handleMethod->invoke($this, ...$args);
        }

        try {
            DB::beginTransaction();
            $result = $handleMethod->invoke($this, ...$args);
            DB::commit();
            return $result;
        } catch (Exception $e) {
            DB::rollback();
            throw $e;
        }
    }
}

class ExampleClass extends BaseClass
{
     // トランザクション制御したい場合は、Transactional属性をTrueに
    #[Transactional(true)]
    protected function handle() {}
}

$exampleClass = new ExampleClass();
// トランザクション制御されながらhandle処理が実行される
$exampleClass();

解説

getter処理の解説

1. まずは使用するアトリビュートのクラスを宣言

<?php
#[Attribute]
class Getter
{}

2. Getterアトリビュートの有無でgetter処理を付与する処理を実装

<?php
abstract class BaseClass
{
     // 解説1:アクセス不能(protected または private)または存在しないプロパティを呼ばれた時に実行される「マジックメソッド__get」に処理を実装
    public function __get(string $name): mixed
    {
        // 解説2:リフレクションAPIを使用し、クラスから指定された名前のプロパティを取得
        $reflectionProperty = new ReflectionProperty($this, $name);

        // 解説3:プロパティに付与されているアトリビュート情報のうち、Getterアトリビュートを取得
        $reflectionAttributes = $reflectionProperty->getAttributes(Getter::class);

        // 解説4:Getterアトリビュートが付与されていれば、プロパティの値を返す
        if (!count($reflectionAttributes)) {
            return $this->{$name};
        }

        throw new Exception('アクセス不能なプロパティです。');
    }
}

参考|__get

3. 実際にgetter処理を追加したいプロパティにアトリビュートを設定

<?php
class ExampleClass extends BaseClass
{
    public function __construct(
        // 解説1:クラス内からしかアクセスできないprotected/privateプロパティに#[Getter]アトリビュートを設定
        #[Getter] protected string $hoge,
    ){}

//    // 解説2:本来ならば下記のようなgetter処理が必要
//    public function getHoge(): string
//    {
//        return $this->hoge;
//    }   
}

4. 実行するとエラーにならず値が取得できる

<?php
$exampleClass = new ExampleClass('fugafuga');
 // 'fugafuga'と出力される
var_dump($exampleClass->hoge);

トランザクション処理の解説

1. まずは使用するアトリビュートのクラスを宣言

<?php
#[Attribute]
class Transactional
{
    // 解説1:今回はトランザクション制御の有無を引数で指定できるようにする
    public function __construct(public readonly bool $needTransaction){}
}

2. アトリビュートによるトランザクション制御処理の実装
※ getterの時と同じ解説は省略しています。

<?php
abstract class BaseClass
{
    public function __invoke(mixed ...$args): mixed
    {
        // 解説1:abstractでhandleメソッドの実装を強制すると、引数が自由に指定できないので、こちらの処理で強制
        if (! (method_exists($this, 'handle') && (new \ReflectionMethod($this, 'handle'))->isProtected())) {
            throw new Exception('アクセス権がprotectedのhandleメソッドを定義してください。');
        }

        // 解説2:Transactional属性を取得
        $handleMethod = new ReflectionMethod($this, 'handle');
        $transactionalAttributeArray = $handleMethod->getAttributes(Transactional::class);
        if (!count($transactionalAttributeArray)) {
            throw new Exception('Transactional属性にて、トランザクションの要否を指定してください。');
        }

        // 解説3:Transactional属性のフラグでトランザクション制御
        $needTransaction = $transactionalAttributes[0]->newInstance()->needTransaction;
        if ($needTransaction === false) {
            return $handleMethod->invoke($this, ...$args);
        }

        try {
            DB::beginTransaction();
            $result = $handleMethod->invoke($this, ...$args);
            DB::commit();
            return $result;
        } catch (Exception $e) {
            DB::rollback();
            throw $e;
        }
        $transactionalAttribute = $transactionalAttributeArray[0]->newInstance();
    }
}

3. 具象クラスのhandleメソッドにアトリビュート付与し、実行するとトランザクション制御される。

<?php
class ExampleClass extends BaseClass
{
    // 解説1:Transactional属性とトランザクション制御の有無フラグを引数として設定
    #[Transactional(true)]
    protected function handle() {}
}

$exampleClass = new ExampleClass();
// トランザクション制御されながらhandle処理が実行される
$exampleClass();

終わりに

いかがでしたでしょうか?
アトリビュートを利用したことで、getter処理をプロパティの数だけ書いたり、同じようなトランザクション制御を何回も書く必要がなくなりました。
アトリビュートはあくまで追加情報 ではありますが、今回のように工夫して利用することで、横断的な処理をメイン実装から剥がせたりするので、実務では大いに活躍できそうです。
ぜひ皆さんもアトリビュートを使ってAOPしてみてください!

☆スタイル・エッジ・グループでは、一緒に働く仲間を募集しています☆
もし興味を持っていただけましたら、採用サイト↓も覗いてみてください!
recruit.styleedge-labo.co.jp

参考資料

*1:実行環境:PHP 8.0

流行りのテキストからの画像生成やってみました

はじめに

こんにちは、スタイル・エッジ・グループのぽんです。
今年はオフィス移転の業務に関わったりなど、バタバタしておりましたが…
ようやく、新しいオフィス周辺のお昼の探索を楽しみつつ、
新宿ど真ん中で濡れずに出勤できる喜びを日々かみしめています。

画像生成って?

今回は、ここ最近流行っているテキストからの画像の自動生成を自分の環境で試してみました!
LINEアカウントがあったり、アプリがあったり、色々と盛り上がっていますね。

組み合わせは「VQGAN+CLIP」でWindows環境で実施しています。

「VQGAN+CLIP」であれば、Google Colaboratory(https://colab.research.google.com/)でも可能なのですが
今回はあえて、ローカルPCで実施してみました!

Google Colaboratory(Google Colab)とは、環境構築をしなくても無料で
Pythonでの機械学習のプログラミングが行えるサービスです。
しかもGPUも使用できるため、GPUを積んだPCを持っていなくても実行が可能です。
オンラインベースなので、携帯からでもタブレットからでも
勿論PCからでも操作できるのは素敵ですね!
※より高性能な有料プランもあります。

今回採用した、VQGANはVQ-VAE(ベクトル量子化)を用いた、
敵対的生成ネットワーク(Generative Adversarial Networks通称GAN)です。
GANは、データから特徴を学習し実在しないデータを生成します。そしてそのデータが本物か偽物かの判定を
繰り返すことによって本物のような偽物のデータを作り上げます。

そして、CLIP(Contrastive Language-Image Pre-training)はモデルを持っているわけではなく、
インターネットから画像とテキストの組み合わせを収集することにより、
4億の画像とテキストの組み合わせをOpenAIで学習しています。
テキストのエンコーダとイメージのエンコーダ、それぞれで動作をし、
類似度が最も高い画像をピックアップします。


この2つを組み合わせることによって、入力したテキストから画像の自動生成を実現します!

環境構築と実働

Python 3.10をインストール

まずは、Windowspythonを利用できるようにします。
python
https://www.python.org/downloads/
公式サイトよりダウンロードしてインストーラにてインストールします。

インストールが完了したら、コマンドプロンプトを立ち上げ下記コマンドを実行。

python -v

バージョンがきちんと表示されれば問題ありません。

CUDAインストール

VQGAN+CLIPは、機械学習ライブラリのPyTorchを利用して動作させています。
GPUを積んでいるPCであれば、GPU利用版のPyTorchで動作させたいため…
PyTorchのインストール前にCUDAをインストールします。

CUDAは、NVIDIAGPU向けの統合開発環境です。
私の端末には以前CUDAを入れたことがあるのですが、バージョンが古くてPyTorchの要件に合わないので、
アップデートインストールを行います。

NVIDIAのサイトからCUDA11をダウンロードして、実行します。
CUDA Toolkit 11.8
https://developer.nvidia.com/cuda-downloads?target_os=Windows&target_arch=x86_64&target_version=10

環境変数にパスが追加されているか確認をします。
 システム環境変数の編集 ⇒ 詳細設定 ⇒ 環境変数をクリック

環境変数

CUDA_PATH_V11_8が追加されていることを確認。

コマンドプロンプトで下記コマンドを実行。

nvidia-smi

CUDA Version: 11.XXと表示されることを確認。
これで、無事にCUDAが入りました!

PyTorchインストール

改めまして、PyTorchインストールです。
PyTorch公式
https://pytorch.org/get-started/locally/

公式ページに行き、自分のPC環境に合わせてインストールしたい環境を選ぶと…
環境に合わせたコマンドが表示されます。

私は、condaコマンドで実行しました!
そのためには、前提としてAnacondaのインストールが必要です。

Anacondaインストール

Anaconda
https://www.anaconda.com/products/distribution

公式サイトからパッケージをダウンロード。
そして、インストーラで促されるままにインストール。
Windowsの場合、日本語が入っているフォルダにはインストールが行えないので注意してください。

上記によりAnacondaが無事に入ったので、condaコマンドを手軽に実行するために
スタートメニューで「Anaconda Prompt」を検索して立ち上げます。
そして、PyTorch公式に表示されているコマンドを実行します!

PyTorch公式から環境に合わせてコマンドを取得
conda install pytorch torchvision torchaudio cudatoolkit=11.3 -c pytorch

condaコマンドにて無事にインストールが行えたことを確認したら、
公式に書いてあるコードを実行して確認します。
インストール途中でエラーが出た場合は、周辺コマンドなどのアップデートを適宜行ってください。
※私は、環境が古くて色々アップデートが必要でした。

コマンドプロンプトにて

python

Pythonを起動し入力モードにします。

import torch
x = torch.rand(5, 3)
print(x)

torchを実行!

tensor([[0.3380, 0.3845, 0.3217],
        [0.8337, 0.9050, 0.2650],
        [0.2979, 0.7141, 0.9069],
        [0.1449, 0.1132, 0.1375],
        [0.4675, 0.3947, 0.1426]])

結果が表示されれば無事にインストールはできています。
そして、GPUの利用もできます!
やっとPyTorchが入ったので次に行きます☆

Pythonの追加パッケージをインストール

追加パッケージをpipコマンドでインストールします。

pip install ftfy regex tqdm omegaconf pytorch-lightning IPython kornia imageio imageio-ffmpeg einops torch_optimizer setuptools==59.5.0

VQGAN+CLIPのソースをgitからおろしてきます。

GitリポジトリからVQGAN+CLIPをクローンしてきます。

git clone 'https://github.com/nerdyrodent/VQGAN-CLIP'
cd VQGAN-CLIP
git clone 'https://github.com/openai/CLIP'
git clone 'https://github.com/CompVis/taming-transformers'

クローン出来たら、VQGAN-CLIPフォルダの下に、checkpointsディレクトリを作成します。

VQGANの学習済モデルとyamlファイルをダウンロードします。

curl -L -o checkpoints/vqgan_imagenet_f16_16384.yaml -C - 'https://heibox.uni-heidelberg.de/d/a7530b09fed84f80a887/files/?p=%2Fconfigs%2Fmodel.yaml&dl=1' #ImageNet 16384
curl -L -o checkpoints/vqgan_imagenet_f16_16384.ckpt -C - 'https://heibox.uni-heidelberg.de/d/a7530b09fed84f80a887/files/?p=%2Fckpts%2Flast.ckpt&dl=1' #ImageNet 16384

公式には、curlコマンドでと書いてありますが…
直接ファイルをダウンロードして、checkpointsフォルダ下に
 ・vqgan_imagenet_f16_16384.yaml
 ・vqgan_imagenet_f16_16384.ckpt
という名前で保存しても問題ないです。
curlコマンド環境を整えるのに挫折して…私は直接ダウンロードしました。
 いや、こちらの方がダウンロード速度も早かったです。

動かしてみよう!!!

やっとここまで来ました!
コマンドプロンプトで、VQGAN-CLIPフォルダまで移動しPythonを実行します!

python generate.py -p "A painting of an apple in a fruit bowl"

こちらは、フルーツボールの中にあるリンゴを描いてほしいと命令を出しています。
※VQGAN-CLIPのチュートリアル用画像ですね。
実行が完了すると、同一フォルダにoutput.pngの名前で描いたイラストが出力されました!

フルーツボールの中のリンゴ

やりましたね☆
ようやくテキストから、画像を生成することに成功しました!!

ここで、注意点がありまして…
ローカルで実行してみたいからと言って、GPUを積んでいないマシンで実行をするのは
本当にオススメできません…!

私もこの対応を行う中で、一度、PyTorchの起動に失敗してCPU環境で実行してしまったのですが
GPU環境であれば1分程度で終わる処理が
CPU環境で実行してしまうと、一晩明かしても終わっていませんでした(悲)

色々動かしてみよう!!

折角出来上がった環境なので、他に何枚か描かせてみた画像を添付します。

python generate.py -p "sky and cat and magic"

空飛ぶ魔法ねこみたいなのが描かれたら良いなと思って実行しました!

空とねこと魔法と

割と目的通りな雰囲気は出たかなと思います☆

python generate.py -p "Mechanical robots and programming"

ロボ的な何かが描かれたらと思って実行しています。

ロボとプログラミング

ロボットがプログラミングをしている?みたいになりましたね(笑)

最後に

色々と予想外の仕上がりになるものもあり楽しい結果になりました!
業務には直接関係は無いものの、興味のあるものを環境を作って
実行してみるというのも、なかなか楽しいですね。

スタイル・エッジ・グループでは、色々なことに興味を持ってくれる、一緒に働く仲間を募集しています!
もし弊社に興味を持っていただけましたら、採用サイト↓も覗いてみてください!
recruit.styleedge-labo.co.jp

Puppeteerで自動化してみた

はじめに

こんにちは。スタイル・エッジ・グループに新卒入社し2年目のK2です!
9月半ばに本社の拠点がJR新宿ミライナタワーに移転し、一段と広く綺麗なオフィスになったので日々ワクワクしながら業務に励んでおります。
また、エンジニアとしても日々成長できるよう毎日楽しんで業務に取り組んでおります。

つい最近ですが、アカウント業務の半自動化を行うために、ブラウザの自動操作を行う機会がありました。

その際に Puppeteer というライブラリを使用したので、今回は Puppeteer についてまとめてみようと思います。


「Puppeteer」とは…

簡潔に説明すると、Puppeteerとは、DevToolsプロトコル上でChromiumChromeを制御するためのNodeライブラリです。

具体的にPuppeteerで出来ることの一例を挙げてみると、下記の通りです。

  • Webページ内での手動操作全般を自動化(フォーム送信、キーボード入力)
  • E2Eテストの実行

今回は自分が実際に行っていた、Webページ内での手動操作の自動化や、自動化時に詰まったポイントなどをまとめようと思います。

それでは早速、Puppeteerの環境構築から行っていきましょう。

想定する構築環境

下記は実際にローカルで動作した際の環境です。
Windows上での環境です

Node       v16.13.1
Npm        8.1.2
Puppeteer  18.0.3

環境構築手順

公式リファレンスはこちらをご覧ください

1. Node.jsのインストール

Puppeteerの動作環境を作るにあたって、Node.jsのインストールが必要となります。

Node.jsのインストールがお済みでない方はこちらからインストールしてください。(推奨版のダウンロードをお勧めします)

2. 作業用ディレクトリ作成
Puppeteerで作業する用のディレクトリも作成しておきましょう。今回は下記コマンドにて「Puppeteer_PRJ」とい名前でディレクトリを作成しました。

mkdir Puppeteer_PRJ

ディレクトリ作成後、作成したディレクトリに移動しておきましょう。

cd Puppeteer_PRJ

3. Puppeteerのインストール

Node.jsのインストールが完了しましたら、先ほど作成したディレクトリ配下で、下記コマンドにてPuppeteerをインストールしてください。

npm install puppeteer

上記手順より、Puppeteerが動作する環境が構築できました。

それでは実際にソースを記述し、ブラウザを自動で動かしてみましょう。

サンプルコードでブラウザ操作自動化

今回は試しに、Googleにアクセス後、「Puppeteer」というキーワードで検索し、スクリーンショットを撮るまでの操作を自動化してみたいと思います。

1.サンプルコード作成

Puppeteerをインストールしたディレクトリ階層以下(本記事では「Puppeteer_PRJ」のディレクトリ階層直下)にjsファイルを作成してください。今回はsample.jsを作成し、下記内容を記載しました。
※サンプルコードで使用している主な処理の詳細については、後述します。

sample.js

const puppeteer       = require('puppeteer');
const path            = require('path');
const { link }        = require('fs');

const URL             = 'https://www.google.co.jp/';
const PATH            = '.';

(async () => {
  const browser = await puppeteer.launch({
    headless: false,
    slowMo: 50,
  });

  //ブラウザ立ち上げ
  const page = await browser.newPage();
  await page.setViewport({
    width: 1200,
    height: 800,
  });

  //URLのページに移動
  await page.goto(URL, { waitUntil: 'domcontentloaded' });

  //検索欄に入力
  await page.$eval('input[name="q"]', el => el.value = '');
  await page.type('input[name="q"]', 'puppeteer');

  //検索ボタンをクリック
  await Promise.all([page.waitForNavigation({ waitUntil: 'load' }), page.click('input[value^="Google"]')]);

  //現在表示されているページのスクリーンショットを指定のPATHに保存
  await page.screenshot({
    path: path.join(PATH, 'sample.png'),
    fullPage: true,
  });

  await browser.close();
})();

2.コマンド実行

上記jsファイルを作成後にnodeコマンドを実行して、実際にブラウザの自動操作ができることを確認しましょう。

node sample.js

3.スクリーンショット確認

コマンド実行後、sample.jsを作成したディレクトリ階層と同じ階層に、下記sample.pngが作成できていることを確認できれば、自動化完了です。

Puppeteerで自動化する際によく使う処理

続いて、今回のサンプルコードでも利用した、Puppeteerで自動化する際によく使う処理もまとめてみました。

ページ遷移

指定したURLにページ遷移します。waitUntilオプションを指定することで、ナビゲーション終了までの間待機します。

page.goto('https://www.google.co.jp/', {waitUntil: 'domcontentloaded'})

今回の例の場合、https://www.google.co.jp/'に遷移後、DOMツリーの構築完了まで待機します。

テキスト入力

特定のDOM要素に対して、文字を入力します。
主にテキスト欄に入力する際に使用します。

page.type('input[name="q"]', 'puppeteer')

上記の場合、name属性にqを持つinput要素に対して、「puppeteer」という文字列を入力します。

クリック

特定のDOM要素をクリックします。
主にページ遷移などで使用されますが、ラジオボタンチェックボックスの要素に対してクリックを行う事もできます。

page.click('input[value^="Google"]')

上記の場合、「Google」の文字列と前方一致するvalue属性を持つinput要素をクリックします。

セレクトボックス選択

select要素を参照し、指定されたvalue属性に一致する要素を選択します。
プルダウンを操作する際に使用されます。

page.select('select[name="sample"]', '1');

上記の場合「sample」というname属性を持つselect要素について、value値が「1」であるoption要素を選択します。

待機時間系

ページ遷移や、自動化する際の各ステップに応じて待機時間を設けることで、Puppeteerでの自動操作が安定します。
Puppeteer動作時に待機時間を設けない場合、処理の遷移時にDOMツリーの構築が完了していない可能性があります。
待機時間を適切に設けることで処理の遷移時にDOM要素が参照できないことを防ぐことができるので、自動化の際は重要なポイントです。

1.waitForSelector

指定したDOM要素が出現するまで待機します。

page.waitForSelector('input[name="sample"]'), 

上記の場合、name属性にsampleを持つinput要素が出現するまで待機します。

2.waitForNavigation

ページが新しいURL に移動するか、ページが再読み込みされるまで待機します。

page.waitForNavigation({ waitUntil: 'load' })

3.各DOM要素指定のオプションにdelayを追加

前述した、テキスト入力、セレクトボックス指定、クリック等のDOM要素を指定するメソッドにはオプションとしてdelayをミリ秒単位で指定することができます。

特定の時間を予め待機させておきたい場合に使用します。

Puppeteerで自動化する際に詰まったポイント

続いて、Puppeteerで自動化する際に詰まったポイントを2つ紹介いたします。

要素が読み込まれる前に処理が実行されてしまう

1点目ですがブラウザ操作の自動化をする場合、DOM要素が取得可能なように待機時間の設定をすることがとても重要でした。

理由としては待機時間の設定をしない場合、DOM要素指定の処理を記述した際に、PCの動作状況(重い、軽い)ブラウザの動作によっては上手くDOM要素を参照できない場合があるためです。

クリック処理などだけではなく、自動操作したいブラウザの動作状況に応じて待機時間を正しく設けておくことで、スムーズに自動操作が進みます。

今回紹介した待機時間の処理は一例なので、自動化したい処理に応じた待機時間を設定することをお勧めします。

一意となるDOM要素の参照が難しい

2点目、一意となるDOM要素の参照が難しく個人的には詰まりポイントでした。
理由としては、DOM要素を参照する際にclassやnameなどの属性を元に参照するのですが、それらの単一属性の指定だけでは上手く参照できないケースがあるためです。

例として挙げると、下記の様なDOM要素(name属性にsampleを持つinput要素)を指定した場合

page.type('input[name="sample"]', 'test')

上記の様に「sample」というname属性を持つinput要素が1つであれば良いのですが、Webページによっては複数該当する場合もあります。

そうした場合、DOM要素は複数あるものの内1番最初の要素を取得するので、上記記述では複数該当するDOM要素の2番目の要素を指定できません。

取得する要素の、属性を変更する等で一部対応できましたが、そもそも指定したいDOM要素中の属性が少ない場合等も多くありました。

そこで解決策として、DOM要素の指定を一意にするために、取得したい要素の親要素で一意に絞り込める要素があるか探し、親要素も含め下記の様な形式で絞り込む方法で対応しました。

page.type('#inputWrapper input[name="sample"]', 'test')

上記例の様に親要素含めることで、取得したい要素を絞り込むことができます。

ただ、要素を細かく指定するデメリットとして、サイト側でコードの変更(コード変更があり、DOM要素の内容に変更がかかった場合)があった際に、上記の様な要素指定をするコード内容は都度変更しなければならない点が挙げられます。
サイト側でコードの変更が起こった場合は、自動操作が今まで通りできるか確認し、できない場合はPuppeteerで記述したコードの修正、若しくは自動操作の方法にキーボード操作を含めてみたりと工夫することも大切です。

最後に

いかがでしたでしょうか。
Puppeteerを利用すれば、上記の様に比較的簡単にブラウザ自動操作することができます。
ブラウザ上での定型的な手動作業を自動化することは、業務改善にもつながると思うので、一度試してはいかがでしょうか。


☆スタイル・エッジ・グループでは、一緒に働く仲間を募集しています☆
もし興味を持っていただけましたら、採用サイト↓も覗いてみてください!
recruit.styleedge-labo.co.jp

コードの標準化をめざして(自動コードチェック強化編)

はじめに

こんにちは。初投稿のneueと申します。

以前はフロントエンド関連の業務に従事していましたが、スタイル・エッジ・グループへのJoin後は、
Webインフラ運用やシステムの新規開発など、越境もあり学びの日々を過ごしています。
現在はというと、組織の開発体験向上のための、環境の開拓&刷新がメインのプロジェクトに参加しています。

今回は、そちらで取り組んだ施策の1つ「自動コードチェックの強化」について、どのようなことを考えながら進めたかを振り返ります。

きっかけ

きっかけは直近参加していた新規開発のプロジェクトにて。
弊社では、コードレビュー文化が強く根付いていることもあり、多くのコメントが飛び交っていました。

内容を見ていくと、設計思想に関する意見交換や、視野の補完のためのアドバイス、ベターな命名・実装方法などが主ではあるものの、中にはケアレスミスの指摘なども混在しています。

塵も積もれば…と言いますので、明白な解のある修正点は、レビュアーの手へ渡る前に処理できればと考えました。

現状の確認

策が全く講じられていないということはなく、PHP_CodeSnifferPHPコードの標準違反検出)は導入済みで、基本的なコードフォーマットは自動で適用されていました。

ただし、

  • 標準として扱うコーディングスタイルが古め(PSR-2)
  • 追加のルール定義が最低限
  • 動作タイミングがコミットフックかつ自動整形可能なエラーの修正しか行われていない

といった導入状況だったため、今回はコード標準化の第一歩として、

  • ルール定義の最新化・詳細化
  • 導入・運用方法の見直し

を実施することにしました。

過去にESLintStylelintのルール定義構築経験もあったので、割と気楽に考えていました。この時は…

使用方法の理解

使用方法を理解するにあたり、まずは公式ドキュメントであるWikiを確認します。

など、必要そうな情報は押さえられたものの…
各ルールの解説ページ(このようなものこのようなもの)が存在しないことに不安を覚えます。

概念の把握

続いて、PHP_CodeSnifferにおける各種概念をざっくり把握します。
「ルール」が最小の単位ではない所がポイントです。

単位

  • ルールセットはXMLで管理(Standardと呼ばれる)
    • ファイル内で実行時オプションも指定できる
  • ルールはファイル単位で定義されている(Sniffと呼ばれる)
  • 1つのルールの中に、複数のチェック項目が存在している(呼称は不明)

ルールセット(Standard)

  • 他のルールセットを丸ごと有効化できる(継承に近い)
  • ルール単位、ルール内のチェック項目単位での有効化ができる
  • 有効化した範囲のうち、一部を無効化できる
  • 検出・自動整形のどちらかだけを無効化できる
  • PSR-2やPSR-12など汎用的なものがパッケージに組み込まれている

ルール(Sniff)

  • 名前空間として、いずれも特定のStandardに所属している
  • 一部のルールに制御用のオプションが用意されている
  • 修正方法が明確なもののみ、自動整形ができる
  • 注意点として、異なるルールでチェック範囲がやや重複しているものが存在する
    • 相反するルールの整形が有効化されている場合、ループが発生する恐れもある

ルールセットの構築(流れ・マインド)

全体としては、このような流れで進めていきます。
最初から精査しすぎず厳しめに作って、現実(生きているコード)を見せながら軟化させていく形です。

  1. 土台とするルールの決定
  2. 個別ルールをローラーして必要そうなものを追加
  3. 実プロジェクトのコードで検証し、不都合なものを除外

ポイント

ここで作成するベースに完璧を求めすぎないことも重要です。
実際に使用するプロジェクト側で、ルールの調整や部分無効化を可能にすることを前提として、気楽に。

  • あくまでも土台となる1つの標準であること
  • プロジェクトごとのイレギュラーなコードを考慮しすぎないこと
  • 要望に応じて見直しは可能である(システムへの影響は及ぼさないので変更ハードルも高くない)こと

ルールセットの構築(実作業)

土台とするルールの決定

非推奨となっているPSR-2からの変更先として、PSR-12を選択します。
XMLファイル内に、各項目をどのルールを用いて検出しているかが細かく書かれているため、参考にします。

個別ルールのローラー

PSRや特定のフレームワークに紐付かない項目として、GenericSquizのルールを1つずつ確認します。
冒頭での不安は的中し、ソースコードを読み解く必要が出てきたことで序盤は探り探りでしたが、
手順を確立してからは併読支援のブックマークレットを作り、時間短縮できました。

  1. 組み込み済のPSR-12ルールセットで有効化されていないルールであることを確認
  2. publicのメンバ変数がある場合は、プロパティ一覧に記載があるはずなので制御内容を把握
  3. ソースコード中のadd(Fixable)?(Error|Warning)(OnLine)?(=チェック項目)を確認
  4. チェック項目名と前後の処理やコメントを読んで(ざっくり)チェック内容を把握
  5. イメージが付かなければ、テストファイル(Invavlidパターン例Validパターン例)を確認
  6. それでもイメージが付かなければ、サンプルコードを書いてエラー扱いになる内容を確認
  7. 導入できそうなルールであれば有効化(怪しいものも目印コメント付きで一旦有効化)

ルールセットの検証

実プロジェクトで使用しているコードに対して試験導入し、ルールを見直します。
※便宜上ルールと記載していますが、チェック項目単位で制御可能です。

導入直後は大量のエラーが出力されることが予想されます。
出力量が多い状態を維持して確認を続けると、待ち時間や見落としが発生するため、やや工夫が必要です。

  • 最初にInformation Report形式で出力して、全体的な傾向を確認
  • 発生件数が多いエラーから確認
  • 確認を終えたルールは一時的に無効化
  • 実行時間短縮のため、連続する検証時は適宜対象ディレクトリを絞る
    • ※広く発見→狭く解決→広く解決できているか確認→狭く解決→…

自動整形の結果が望ましくないルールの無効化

使用時のイメージと合わせるため、先に自動整形可能なルールから検証します。
自動整形の結果が問題ない状態となるまで、適用→ルール調整→復元を繰り返します。

  1. 当該ルール・整形後状態の確認
    1. 自動整形を実行
    2. git diffを出力(整形前後の差分を記録)
    3. git resetで整形実行前に戻る
    4. チェックを実行
    5. 記録した箇所に関するチェック項目を確認
  2. あるべき姿に手動調整
  3. 再度チェックを実行
    • エラー扱いになる:項目自体を無効化
    • エラー扱いにならない:自動整形のみを無効化

競合するルールの無効化 ※整形ループが生じた場合

整形内容の組み合わせによっては、ループが生じてしまう(状態A→状態B→A→B→…)ことがあります。
-vvオプション付きで自動整形を実行すると、処理の流れをトレースできるため、どちらか一方のルールを無効化します。

エラー内容の精査・対応

残りのエラーについて、なぜエラーになっているかを確認し、対応方針を検討します。

  • コードに問題がある
    • ルールは維持(コードを修正する想定)
  • 可読性目的でコードの書き方を工夫した箇所が引っかかる
    • ルールは維持(コメントでチェック対象から除外する想定)
  • 全体的に適合が難しい
    • ルールを無効化
    • 検証中のプロジェクトにおいてではなく、今後新規開発する場合を基準に検討
  • 解析の精度が低いのか、意図しない状態になる
    • ルールを無効化
    • 比較的最近導入されたルールや、複雑なコードの組み合わせなどで発生
    • サポートされたら復活できるよう、経緯付きでコメントアウトする(issueがあればリンクも添えておく)
  • ルールは問題ないが特定領域のみエラーが多発している
    • ルールは維持するが、除外パスのパターンを追加
    • 自動テストやDBシーディングなど、命名規則や記述方法がやや特殊になる範囲などで発生

導入・運用方法の見直し

ルールセットは、ある程度実用的なものになったので、PHP_CodeSnifferの導入方法を見直します。

プロジェクトへの導入

導入や運用開始後のアップデートを使い慣れた方法で実現させるため、Composerパッケージとして構築します。

PHP_CodeSniffer及び関連パッケージや設定ファイルをそれぞれ手動でプロジェクトへ導入するのではなく、1つのパッケージとして纏めることで、以下が実現できます。

  • プロジェクトへのインストール・アップデート作業のシンプル化
  • PHP_CodeSnifferと関連パッケージ・設定ファイル間のバージョン整合性の担保
  • プロジェクトの優先ルールを記述するXML雛形の管理

以下の手順だけで、導入が可能になりました。

インストール

  1. composer install
  2. composer scriptsにphpcs, phpcbfへのaliasを追加
  3. XML雛形をプロジェクトルートにコピー
  4. 独自調整があれば、プロジェクトの優先ルールをXMLに指定

アップデート

  1. composer update
  2. 不都合な差分があれば、プロジェクトの優先ルールとして調整

エディタとの連動

エディタ上でファイルを保存した際に、当該コードのハイライト&自動整形が行われるようにします。

PhpStorm

標準サポートしているため、公式ドキュメントを参考に設定します。

VS Code

標準ではサポートしていないので、PHP Sniffer & Beautifierを導入します。
全プロジェクトで自動実行されないようグローバル設定上は無効にし、ワークスペース単位で有効化します。

Gitコミットフック

元は自動整形の適用のみでしたが、差分ファイル内で違反が残存している場合はコミットが中止される強固な形に調整します。

動作イメージ

実際にエディタ上で保存すると、自動整形→残りのエラーが問題パネルに表示されます。

おわりに

いかがでしたでしょうか。

この記事を書いている時点では試験運用のタイミングのため、まだ具体的な効果は見えていませんが、コードレビューの時間が少しでも有意義に使えるようになることを願って、継続的にメンテナンスしていこうと思います。今回は既存ルールのみでしたが、頻出する指摘があれば独自ルールの作成も効果的かもしれません。

☆スタイル・エッジ・グループでは、一緒に働く仲間を募集しています☆
環境改善が好きな方など、もし興味を持っていただけましたら、採用サイト↓も覗いてみてください!

recruit.styleedge-labo.co.jp

MariaDB10.6.7へバージョンアップしたらハマった。

はじめに

こんにちは。エンジニアになって1年半が経ち、時の流れの早さに驚きが隠せないpeipeiです。
余談ですが、お盆休みは先輩にオススメされたAWS Cloud Questをやって大満足でした。(一部無料で学べるのでAWSで遊びたい方は是非!)

今回の記事はRDSをMariaDB10.3.xから10.6.7へバージョンアップした際にバグが発生し、原因を見つけるのに苦労した話です。
「記事として紹介すれば同じ現象で困っている人のためになるかもしれないよ?」と先輩から助言いただき、それなら書いてみよう!と思い、筆(?)をとりました。

早速始めます。

概要

ある日、AWSから下記のような通知がありました。

After August 1, 2022, Amazon RDS will upgrade all instances running MariaDB 10.5.9, 10.4.18, 10.3.28, 10.2.37 and older versions to the latest minor version in the same major series during a future maintenance window.

この通知によると RDSのMariaDBバージョン10.5.9, 10.4.18, 10.3.28, 10.2.37以下のマイナーバージョンは自動アップグレードがかかるようです。 これにより自社システムの一部ではRDSのバージョンアップが必要になりました。

何も対応しなくても自動的にバージョンアップはされるようですが、突然バージョンアップされてシステムで不具合が発生する可能性を避けなければなりません。

本記事はバージョンアップ対応のDB切り替え手順と
DB切り替え前の事前検証で発生したバグ対応に関してご紹介いたします。

バージョンアップ時のDB切り替え手順

バージョンアップにも様々な手法はあると思いますが、現在のシステム運用では下記の方法が最も影響が少ないと考えこちらの方法を採用しました。

  1. 既存RDSにリードレプリカを作成 システム構成1
  2. リードレプリカを10.3から10.6へバージョンへアップ システム構成2
  3. リードレプリカをマスターへ昇格 システム構成3
  4. マスター昇格したRDSへエンドポイントを切り替え システム構成4

事前検証での品質の担保

最も大切なのは、バージョンアップを行ってもシステムで不具合が起きないということを保証することですね。
上記のDB切り替えを行う前に、検証環境で事前検証を行いました。

「ユーザーにとってはデータベースのバージョンなどはどうでもよくて、動きが替わっていないことが重要!」
これは私の先輩からの有難いお言葉です。

品質を担保するときに事前に行ったことは主に2つです。

  • バージョンアップによる変更点を洗い出す
  • 検証環境をバージョンアップして、動作テストを行う

バージョンアップによる変更点を洗い出す

MariaDBの公式サイトからバージョンアップによる変更点を洗い出し、変更点がシステムに影響を与えそうか調査しました。

今回10.3.xから10.6.7まで一気に上げているので、10.3->10.4->10.6に関わる変更点をある程度抑えておかなければなりませんでした。

↓公式サイト
https://mariadb.com/kb/en/upgrading-from-mariadb-105-to-mariadb-106/

検証環境をバージョンアップして、動作テストを行う

事前に動作テスト項目を準備した上で、検証環境をバージョンアップして一つ一つ動作テストを行っていきました。
これはかなりマンパワーが必要だったので、チーム全員で手分けをして行っていきました。大変でしたがチーム一丸で力を入れて取り組めた経験は良かったかなと思います。

なども含めて細かく動作確認していきました。

バグが発生!!

上記の動作検証を行なっている際に、バージョンアップによる不具合が見つかりました。

↓Laravelのエラー文(イメージ)

Type error: Return value of APP\Http\Controllers\hogeController::getId() must be of the type int, null returned ...  

IDはint型で渡さないといけないのに、nullで渡ってきたよ。というエラーが発生しています。
10.3.x(旧バージョン)ではIDを取得して画面表示できていたのに、10.6.7(新バージョン)に上げると何故かIDが取得できなくてnullで返ってきてしまっています。
Google検索をしても、それらしい事例がなかなか見つけられなかったため、本格的に調査を行っていきました。

調査内容

1. 発行しているSQLは正しい?

↓発行されたSQL(イメージ)

SELECT * FROM hoge WHERE id IN (1, 2, 4, 5, ... 1951, 1952, 1953) AND ... ;

こちらをDBでクエリを直接叩くと、検索結果が全て正しく取得できました。
->よってSQL自体に問題はなさそうです。

2. sql_modeは10.3.x(旧バージョン)と10.6.7(新バージョン)で差がある?

新旧のDBでそれぞれsql_modeを確認しました。

SELECT @@GLOBAL.sql_mode;
STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION

また、アプリケーションから接続した際のsql_modeも確認しましたが、同様に差はありませんでした。
->バージョンによるsql_modeの違いはないため、今回の原因となっているとは考えにくくなりました。

3. IN句に指定しているデータ件数が多いことは問題ない?

当初からIN句に指定するIDが多いことは気になっていました。

SELECT * FROM hoge WHERE id IN (1, 2, 4, 5, ... 1951, 1952, 1953) AND ...;

もしかすると、指定するデータ件数が多過ぎたり、データ容量が上限を超えていたりするのではないだろうかという視点で調査を行いました。

発行するSQLを調整して指定する件数を500件に絞ってみたり、2000件に絞ってみたりと調べていくと、IN句に指定するデータ件数が1000件を境にしてエラー発生の有無が分かれていることが分かりました。
999件以下だと問題なく画面表示され、1000件以上だとエラーになっています。

地道に調べていったことで「1000件」というワードが鍵になるということが分かり、Google検索に「MariaDB10.6.7」と「IN句」、そして「1000件」というキーワードを使って、 ようやくバグについて書かれている記事を見つけることができました。

エラーの原因

in_predicate_conversion_thresholdというグローバル変数の影響で、IN句内で1000件以上の値があると結果を空で返してしまうバグでした。(MariaDB 10.6.7特有のバグ)

クエリは正しく発行されていても、Laravel側からDBへ接続してデータを取得する際に、このパラメータの影響でSQLの結果が空のデータセットとして返ってきていたようです。
↓参考記事
https://jira.mariadb.org/browse/MDEV-27937

対処法

このバグはMariaDB 10.6.8以降のバージョンでは解消されているようです。 現在(2022年8月)はMariaDB 10.6.8以上を選択することが可能ですが、当時対応していた時期に選択できた最新のバージョンはMariaDB 10.6.7までだったので、惜しかったということになります。。

その当時は、Laravel側からDBへ接続する際にin_predicate_conversion_thresholdを無効化する設定を加えることでバグを回避しました。

おわりに

バージョンアップ対応には影響調査と動作検証が非常に重要ですね。なんとか期限までに問題なくバージョンアップすることができて良かったです。

☆スタイル・エッジ・グループでは、一緒に働く仲間を募集しています☆
もし興味を持っていただけましたら、採用サイト↓も覗いてみてください!
recruit.styleedge-labo.co.jp

LocalStackを使ってLambda関数をローカルで検証してみよう


こんにちは。morizoです。
もともとwebアプリケーションエンジニアとして入社しましたが、ココ一年くらいはAWSを利用したサーバ運用やセキュリティ面の強化に取り組んでいます。

さて、チーム内で共有してる手順書をもとに日頃の運用業務を進めていると
作成日の古い手順書がUIの変更に追いついておらず、混乱することが多々あります。
近頃はメンバー内でも「AWS CLIつかっていこう」という機運が高まっておりますが、
あまり気楽に試せる環境がないことが悩みのタネになっています。
そこで、今回はLocalStackを利用してローカル環境でAWSサービスを動かしてみようと思います。
無料版だとCLIの操作のみに制限されているため、CLI操作の練習にもなりますね。
※執筆中にちょうど正式版がリリースされました

LocalStack導入とその動作検証

LocalStackのインストールと起動

githubで公開されていますのでこちらからcloneしてきます。
今回はお試しということもあり、Docker経由で起動します。
また、clone後にdocker-compose.ymlを一部編集しました。(編集箇所にコメントを追加しています)
環境変数についてはこちらにまとまっています。

version: "3.8"

services:
  localstack:
    container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}"
    image: localstack/localstack
    ports:
      - "127.0.0.1:4566:4566"            # LocalStack Gateway
      - "127.0.0.1:4510-4559:4510-4559"  # external services port range
      - "127.0.0.1:53:53"                # DNS config (only required for Pro)
      - "127.0.0.1:53:53/udp"            # DNS config (only required for Pro)
      - "127.0.0.1:443:443"              # LocalStack HTTPS Gateway (only required for Pro)
    environment:
      - DEBUG=${DEBUG-}
      - PERSISTENCE=${PERSISTENCE-}
     # Lambda関数実行のたびに同じコンテナを再利用する
      - LAMBDA_EXECUTOR=docker-reuse
      - LOCALSTACK_API_KEY=${LOCALSTACK_API_KEY-}  # only required for Pro
      - DOCKER_HOST=unix:///var/run/docker.sock
     # Lambda関数をコンテナにマウントすることで処理が早くなる場合がある(ローカル環境向け)
      - LAMBDA_REMOTE_DOCKER=0
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

どのようなことができる?

ここから実際にコンテナに入って作業をしてみます。
コンテナ内ではawslocalコマンドが使用できるので簡単のために使用しています。
まずはs3バケットを作成してみました。
※以後コマンドの行頭に実行環境を補足しています

(host) docker exec -it localstack_main bash

(container) awslocal s3api create-bucket --bucket morizo-s3
{
    "Location": "/morizo-s3"
}
(container) awslocal s3api list-buckets
{
    "Buckets": [
        {
            "Name": "morizo-s3",
            "CreationDate": "2022-07-ddThh:00:00.000Z"
        }
    ],
    "Owner": {
        "DisplayName": "webfile",
        "ID": "XXXXXXXXXXXXXXXXXXXXXXXXXXX"
    }
}

できていますね。
続いてLambdaを実行してみます。
以下ファイルをローカルで用意してコンテナ内にコピーしました。

def handler(event, context):
    print(event)
    return 'localstackはじめました'

作成したファイルをzip形式に圧縮して、Lambda関数を作成します。
※roleに指定する値は任意で問題ありません。

(container) zip localstack.zip localstack.py
(container) awslocal lambda create-function --function-name localstack --runtime python3.8 --role morizo --handler localstack.handler --zip-file fileb://localstack.zip
{
    "FunctionName": "localstack",
    "FunctionArn": "arn:aws:lambda:us-east-1:000000000000:localstack",
    "Runtime": "python3.8",
    "Role": "morizo",
    "Handler": "handler.handler",
    ~
    "LastUpdateStatus": "Successful",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ]
}

Successfulとあるのでどうやらうまくいったようです。
それでは実行してみます。

(container) awslocal lambda invoke --function-name localstack output.log
{
    "StatusCode": 200,
    "LogResult": "",
    "ExecutedVersion": "$LATEST"
}

200で返ってきました。
中身をみてみましょう。

(container) cat output.log
"localstack\u306f\u3058\u3081\u307e\u3057\u305f"
(container) echo -e `cat output.log`
"localstackはじめました"

unicode表示になっていたのでcatだけではうまく表示できませんでしたが、
コード内でreturnした文字列がファイルに出力されていることを確認できました。
簡単な関数なら充分やっていけそうです。

ほかのサービスとも組み合わせてみたい

固定の文字列を返すだけでは少し味気ないので、
ほかのサービスを組み合わせた処理を書いてみます。
「cloudwatch logsに保管したログをs3にエクスポート」してみましょう。

まずはlogsにlog groupとlog streamを追加します。

(container) awslocal logs create-log-group --log-group-name morizo-logs
(container) awslocal logs create-log-stream --log-group-name morizo-logs --log-stream-name morizo-logs-stream

logsに追加するログはこちらです。

{
  "logEvents":[
    {
      "timestamp": 1658746080711,
      "message": "Example Event 1"
    },
    {
      "timestamp": 1658746081711,
      "message": "Example Event 2"
    },
    {
      "timestamp": 1658746082711,
      "message": "Example Event 3"
    }
  ]
}

こちらのログをlogsに作成したlogstreamに追加します。

(container) awslocal logs put-log-events --log-group-name morizo-logs --log-stream-name morizo-logs-stream --cli-input-json file://logs.json
(container) awslocal logs get-log-events --log-group-name  morizo-logs --log-stream-name morizo-logs-stream
{
    "events": [
        {
            "timestamp": 1658746080711,
            "message": "Example Event 1",
            "ingestionTime": 1658758594869
        },
        {
            "timestamp": 1658746081711,
            "message": "Example Event 2",
            "ingestionTime": 1658758594869
        },
        {
            "timestamp": 1658746082711,
            "message": "Example Event 3",
            "ingestionTime": 1658758594869
        }
    ],
    "nextForwardToken": "f/00000000000000000000000000000000000000000000000000000001",
    "nextBackwardToken": "b/00000000000000000000000000000000000000000000000000000000"
}

こちらも無事に追加できました。
ではこのログをs3にエクスポートするLambda関数を実行してみましょう。
以下pythonコードを用意しました。

import boto3

def handler(event, context):
    client = boto3.client('logs', endpoint_url='http://host.docker.internal:4566', region_name = "us-east-1", aws_access_key_id='dummy', aws_secret_access_key='dummy')
    return client.create_export_task(
        logGroupName      = 'morizo-logs',
        fromTime          = 1658746080000,
        to                = 1658750400000,
        destination       = 'morizo-s3',
        destinationPrefix = 'localstack'
    )

挙動確認用に使用したLambda関数と同様にzipファイルを作成して登録・実行します

(container) awslocal lambda create-function --function-name create_export_task --runtime python3.8 --role morizo --handler create_export_task.handler --zip-file fileb://create_export_task.zip
(container) awslocal lambda invoke --function-name create_export_task output.log
(container) cat output.log | jq
{
  "ResponseMetadata": {
    "HTTPHeaders": {
      "access-control-allow-headers": "authorization,cache-control,content-length,content-md5,content-type,etag,location,x-amz-acl,x-amz-content-sha256,x-amz-date,x-amz-request-id,x-amz-security-token,x-amz-tagging,x-amz-target,x-amz-user-agent,x-amz-version-id,x-amzn-requestid,x-localstack-target,amz-sdk-invocation-id,amz-sdk-request",
      "access-control-allow-methods": "HEAD,GET,PUT,POST,DELETE,OPTIONS,PATCH",
      "access-control-allow-origin": "*",
      "access-control-expose-headers": "etag,x-amz-version-id",
      "content-length": "50",
      "content-type": "application/x-amz-json-1.1",
      "date": "aaa, dd Jul 2022 00:00:00 GMT",
      "server": "amazon.com, hypercorn-h11"
    },
    "HTTPStatusCode": 200,
    "RetryAttempts": 0
  },
  "taskId": "XXXXXXXXXXXXXXXXXXXXXXXXX"
}

ステータスコード200とtaskidが返ってきました!
確認のためにs3バケットをみてみます。

(container) awslocal s3 ls s3://morizo-s3 --recursive # 表示なし

おや、ステータスは200にはなっているけどオブジェクトがありません。
作成していないロググループやバケット名を指定するとエラーになるため、
コマンド自体は成功していますがログファイルがS3にエクスポートされていないようです。
こちらについては次回以降改めて挑戦してみます。

まとめ

さらなる調査は必要ですが、LocalStackを使ってDockerコンテナ上でLambda関数を実行するところまでできました。
無料版でも利用できるAWSサービスが数多くありますし、
業務で実際に活用できるLambda関数の動作確認をローカル環境でも一通りできるのではないでしょうか?
正式版リリースによって今後更に勢いづいてくるかと思いますので
更新情報を追っていきつつ、よりよいローカルでの開発・検証環境構築も進めていきたいです。

おわりに

☆スタイル・エッジ・グループでは、一緒に働く仲間を募集しています☆
もし興味を持っていただけましたら、採用サイト↓も覗いてみてください!
recruit.styleedge-labo.co.jp