Wiz テックブログ

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

TerraformでCodeDeploy+CodePipeline (GitHub Ver2)を実装する

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

あるプロジェクトで実装したCodeDeployとCodePipelineを使ったデプロイをTerraform化したので、その実装例を紹介したいと思います。

バランサーとEC2

f:id:wiz_tak:20210212143033j:plain

今回はバランサーに複数のウェブサーバがぶら下がっていてそこにインプレースデプロイする構成とします。

デプロイフロー

f:id:wiz_tak:20210212114735j:plain CodePipelineソースアクションのGitHubバージョンですが、1が非推奨の為バージョン2で実装します。

※ Terraformでは、バージョン2のGitHub接続の為のCodeStartConnectionが一部使えないので注意(後述)

Terraformで実装

前提

VPC、EC2、ALBといったリソースは既に構築済みである事とします。

また各環境とモジュールのそれぞれに必要な変数が設定されているものとします。

ディレクトリ構成

.
├── envs
│   ├── develop
│   │   ├── backend.tf
│   │   ├── deploy
│   │   │   ├── backend.tf
│   │   │   ├── main.tf
│   │   │   └── variables.tf
│   │   ├── main.tf
│   │   └── variables.tf
└── modules
    └── deploy
        ├── codedeploy
        │   ├── main.tf
        │   └── output.tf
        ├── codepipeline
        │   └── main.tf
        └── provider
            └── main.tf

CodeDeploy

必要なリソースのインポート

$ terraform import module.codedeploy.aws_vpc.main_vpc vpc-0******
$ terraform import module.codedeploy.aws_lb_target_group.alb_target_group arn:aws:******
$ terraform import module.codedeploy.aws_iam_role.ec2_deploy_role RoleNameSample

①インポートしたリソースの定義

// VPC
resource "aws_vpc" "main_vpc" {
  ~~
}

// ALB Target Group
resource "aws_lb_target_group" "alb_target_group" {
  ~~
}

// EC2にアタッチしたロール
resource "aws_iam_role" "ec2_attach_role" {
  ~~
}

②デプロイアプリケーションとデプロイグループの作成

// アプリケーション作成
resource "aws_codedeploy_app" "deploy_application" {
  compute_platform = "Server"
  name             = "${var.project}-deploy"
}

// デプロイグループ作成
resource "aws_codedeploy_deployment_group" "deploy_group" {
  app_name               = aws_codedeploy_app.deploy_application.name
  deployment_group_name  = "${var.stage}-deploy-group"
  service_role_arn       = aws_iam_role.ec2_attach_role.arn
  deployment_config_name = "CodeDeployDefault.OneAtATime"

  // デプロイ対象のEC2タグ
  ec2_tag_set {
    ec2_tag_filter {
      key   = "Deploy"
      type  = "KEY_AND_VALUE"
      value = var.stage
    }
  }

  // 失敗時のロールバック
  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }

  // ロードバランサーの有効化
  deployment_style {
    deployment_option = "WITH_TRAFFIC_CONTROL"
    deployment_type   = "IN_PLACE"
  }

  // ターゲットグループ
  load_balancer_info {
    target_group_info {
      name = aws_lb_target_group.alb_target_group.name
    }
  }
}

CodePipeline

①IAMロール作成とポリシーアタッチ

ポリシーは長いので割愛します。

ActionにはCodeDeployやS3など必要なリソースを適宜セットしてください。

// IAMロール作成
resource "aws_iam_role" "codepipeline_role" {
  name = "${var.project}-${var.stage}-pipeline-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "codepipeline.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF
}

// ポリシーの作成
resource "aws_iam_role_policy" "codepipeline_policy" {
  name = "${var.project}-${var.stage}-pipeline-policy"
  role = aws_iam_role.codepipeline_role.id

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "iam:PassRole"
      ],
      "Resource": "*",
      "Effect": "Allow",
      "Condition": {
        "StringEqualsIfExists": {
          "iam:PassedToService": [
            "cloudformation.amazonaws.com",
            "elasticbeanstalk.amazonaws.com",
            "ec2.amazonaws.com",
            "ecs-tasks.amazonaws.com"
          ]
        }
      }
    },
    ~~
}
EOF
}

②S3バケット作成

アーティファクトストアとなるS3バケットを作成します。

resource "aws_s3_bucket" "codepipeline_bucket" {
  bucket        = "${var.project}-${var.stage}-codepipeline-${var.region}"
  acl           = "private"
  force_destroy = false
}

③パイプライン作成

ソース/デプロイステージのみを作ります。

(ビルドステージは今回はスキップします)

~~
variable "codestar_connection_arn" {}
~~


resource "aws_codepipeline" "codepipeline" {
  name     = "${var.project}-${var.stage}-pipeline"
  role_arn = aws_iam_role.codesart_connection.arn

  // アーティファクトストア
  artifact_store {
    location = aws_s3_bucket.codepipeline_bucket.bucket
    type     = "S3"
  }

  // ソースステージ
  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["SourceArtifact"]

      configuration = {
        ConnectionArn    = var.codestar_connection_arn
        FullRepositoryId = var.repository
        BranchName       = var.repository_branch
      }
    }
  }

  // デプロイステージ
  stage {
    name = "Deploy"

    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "CodeDeploy"
      input_artifacts = ["SourceArtifact"]
      version         = "1"

      configuration = {
        ApplicationName     = var.codedeploy_application_name
        DeploymentGroupName = var.codedeploy_deploy_group_name
      }
    }
  }
}

aws_codestarconnections_connectionリソースについて

Terraformで保留状態になっており使用出来ません。

(2021年2月12日現在)

その為、事前にAWSコンソールのCodePipelineから適当にアプリケーションを作成し、発行されたarnを変数定義する必要があります。

f:id:wiz_tak:20210212135004p:plain

入出力アーティファクトについて

パイプラインの各ステージで必要な output_artifacts/input_artifacts の箇所は実装する際に少し迷いそうですが、

以下の図の様なワークフローを理解すると分かり易いのかなと思います。

f:id:wiz_tak:20210212135658j:plain

まとめ

Terraformで言語化すると作成するリソースにはどのリソースやコンポーネントが必要なのかといった全体像の把握が改めて出来る点が良いなと思っています。

ただ一部保留や未対応であったりと100%がTerraformで対応出来る訳ではない点が残念です。

最後に

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

careers.012grp.co.jp

第2回LT会を行いました。

第2回LT会レポ

今回のLT会の内容は

発表者: 4名

制限時間: 発表時間 5分 + 質問時間 10分

テーマ: 自由

で行いました。

また今回はCommentScreenというツールを使用し、 リアルタイムで参加者のコメント確認できるようにしました。

f:id:nakamoto03:20210210195024p:plain
comment screen

CommentScreenを使用するとこのように画面にコメントが流れるようになります

それでは1つずつ発表を紹介していきます。

JSモジュールバンドラのこれからについて考える

f:id:nakamoto03:20210212085535p:plain
JSモジュールバンドらのこれからについて考える
1人目の方にはJSモジュールバンドラについて発表していただきました。
現状JSモジュールバンドラとして圧倒的にwebpackが使用されている中、esbuild、Snowpack(The faster frontend build tool)の関心/満足度が上がっており両者共にwebpackを上回ってます。(参照: State of JS 2020) そこでesbuildとSnowpackを実際に使い、その所感を発表していただきました。 詳しくはSnowpack 試してみましたにまとめられています。気になる方は、記事をみていただけると幸いです。  

モリーを認識したプログラム

2人目の方にはCPU・メモリ・ハードディスクなどの違いから、Javaにおけるインスタンスを生成した際のメモリ使用量、メモリの解放のタイミングなどエンジニアが最低限知っておくべき内容に関して紹介していただきました。

PHPのInterfaceの使いどころ

f:id:nakamoto03:20210211225453p:plain
Interface
3人目の方にはInterfaceを使い、「依存性の注入」を使う事で、あるクラスが依存している別のオブジェクトを外部から渡し、クラス間の依存度を下げる設計方法について、またそのメリットに関して紹介していただきました。 詳しくはPHPのInterface -メリットと使い所-にてInterfaceについて投稿されています。気になる方は、記事をみていただけると幸いです。

TypeScript導入のススメ

f:id:nakamoto03:20210211225351p:plain
TypeScript導入のススメ
ラストの方には、TypeScriptとは何か、JS=>TSへの移行ツールなどなどTypeScriptを導入するにあたってコストや、使用する際の最低限のルールについてといった内容を紹介していただきました。

おわり

第2回はこのような内容でした。

この記事ではざっくりとした内容しか紹介できませんでしたが、

LT会の内容をもっと社外へ公開できるよう目指していきたいです。

Wizではエンジニアを募集中です。

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

careers.012grp.co.jp  

Vue3.0における状態管理(Vuex,Provide / inject)

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

今回は、Vue.jsにおける状態管理についてお話したいと思います。

Vue2.x時代、多くの方はVuexを使用していたのではないでしょうか?Vue2.x、Vue3.xともに公式のドキュメントにおいても、状態管理のセクションでVuexが紹介されています。
ただVue3.0以降は、Vuexに打って変わる状態管理方法がいくつか提唱されていますので、それぞれの内容を簡単に見つつ比較したいと思います。

Vuex

VuexVue.jsのアプリケーションのための状態管理ライブラリであり、単純なグローバルオブジェクトとは異なり、Vuexのストアはリアクティブです。
また、Vuexは以下のように単方向のデータフローになるので、変更される状態の追跡が明示的です。
vue-devtoolsを使用すると、getterで得られる値、mutationcommitした履歴などが確認できます。

vuex.png

vuex.vuejs.org

actions

  • stateを直接更新することはなく、mutationsを経由することでstateを更新する。
  • 非同期の処理を入れることができる。

mutations

  • actionsから受け取った値をstateにセット

mutationsでは単にstateの値を変更するだけの処理を行うことが多いかと思います。
それに対しactionsでは比較的複雑なロジックを記述しmutationsに対して値を返すイメージです。

実際のコード

今回は簡単なカウンターアプリを実装します。
SFC内部で全て完結させる場合は上記のような形になります。

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="decrement">-</button>
    <button @click="increment">+</button>
  </div>
</template>

<script lang="ts">
import { reactive, computed, defineComponent } from "vue";
export default defineComponent({
  name: "App",
  setup() {
    const state = reactive({
      count: 0,
    });
    const count = computed(() => state.count);
    return {
      count,
      increment() {
        state.count += 1;
      },
      decrement() {
        state.count -= 1;
      },
    };
  },
});
</script>

では、実際にVuexを使用し、コンポーネントを分割した場合はどのような記述になるでしょうか。
ディレクトリ構成は以下のような形で進めてみます。

/src
 ├ components
 │  ├ DecrementBtn.vue
 │  └ IncrementBtn.vue
 ├ store
 │  └ index.js
 └ App.vue

Vuexのストアの定義を以下のようにします。

▼store/index.js

import { createStore } from "vuex";

const state = {
  count: 0
};
const mutations = {
  increment(state) {
    state.count++;
  },
  decrement(state) {
    state.count--;
  }
};
const actions = {
  increment: ({ commit }) => commit("increment"),
  decrement: ({ commit }) => commit("decrement")
};

export default createStore({
  state,
  actions,
  mutations
});

▼親コンポーネント(App.vue)

<template>
  <div>
    <p>{{ count }}</p>
    <increment-btn />
    <decrement-btn />
  </div>
</template>

<script lang="ts">
import { computed, defineComponent } from "vue";
import { useStore } from "vuex";
import IncrementBtn from "./components/IncrementBtn.vue";
import DecrementBtn from "./components/DecrementBtn.vue";

export default defineComponent({
  components: {
    IncrementBtn,
    DecrementBtn,
  },
  setup() {
    const counter = useStore();
    const count = computed(() => counter.state.count);

    return {
      count,
    };
  },
});
</script>

▼子コンポーネント(DecrementBtn.vue)

<template>
  <button @click="decrement">+</button>
</template>
<script>
import { defineComponent } from "vue";
import { useStore } from "vuex";

export default defineComponent({
  setup() {
    const counter = useStore();
    return {
      decrement: () => counter.dispatch("decrement"),
    };
  },
});
</script>

実際の挙動の確認はこちら↓

codesandbox.io

簡単なアプリケーションということもありますが、コンポーネント間のストアへのアクセス方法や、データフローなど非常に明示的であることがわかるかと思います。

Provide / inject

Provide / injectは、これまでコンポーネントの階層の深さに関係なく、親コンポーネントから子階層へ依存関係を提供するプロバイダとして機能します。
これまでは親コンポーネントから子コンポーネントにデータを渡す際、propsを使用していたかと思います。
しかし、階層が深くなるにつれバケツリレーのように伝搬していくのは非常に面倒でした。

components_provide.png

v3.ja.vuejs.org

こちらはVue2.2から実装されているものでしたが、Vue3.0からはComposition APIのリリースに伴い注目されています。

Composition API

Composition APIは、ロジックをカプセル化することでコンポーネント間でのロジックの再利用を可能にするAPIです。
特に大規模なアプリケーションになると、Vue2.xまではViewとロジックが密結合になっており、SFCが冗長になってしまうことが問題でした。
しかし、Composition APIを活用することでSFCにはViewに関することのみを記述し、ロジックを外部に切り出すようなことも可能になります。

実際のコード

基本的なディレクトリ構成は変わらず、以下のような形。

/src
 ├ components
 │  ├ CountView.vue
 │  ├ DecrementBtn.vue
 │  ├ ParentProvider.vue
 │  └ IncrementBtn.vue
 ├ store
 │  └ index.ts
 │  └ key.ts
 └ App.vue

コンポーネント内でしていた状態管理の部分を切り抜き、ストアを作成しキーを定義します。

▼index.ts

import { reactive } from "vue";
export default function counterStore() {
  const state = reactive({
    count: 0
  });
  return {
    get count() {
      return state.count;
    },
    increment() {
      state.count += 1;
    },
    decrement() {
      state.count -= 1;
    }
  };
}
export type CounterStore = ReturnType<typeof counterStore>;

▼key.ts

import { InjectionKey } from "vue";
import { CounterStore } from "./index";

const CounterKey: InjectionKey<CounterStore> = Symbol("counterStore");
export default CounterKey;

provideは( key , value )を受け取り、provideされた値はコンポーネントからキーを用いてinjectを取り出せます。

▼親コンポーネント(ParentProvider.vue)

<template>
  <div>
    <slot />
  </div>
</template>

<script lang="ts">
import { provide } from "vue";
import counterStore from "../store/index";
import CounterKey from "../store/key";

export default {
  setup() {
    provide(CounterKey, counterStore());
    return {};
  },
};
</script>

これでParentProvider.vue以下のコンポーネントはどの階層においてもストアオブジェクトを受け取ることが可能です。

▼子コンポーネント(CountView.vue)

<template>
  <p>{{ count }}</p>
</template>
<script>
import { defineComponent, inject, computed } from "vue";
import CounterKey from "../store/key";

export default defineComponent({
  setup() {
    const counter = inject(CounterKey);
    const count = computed(() => counter.count);
    return {
      count,
    };
  },
});
</script>

ストアオブジェクトを受け取る側はinjectにキーを指定します。同様の記述でボタン関係も実装します。
最終的には以下のようにまとめることでVuexやSFC1つで記述していたものと同様の動きをします。

▼App.vue

<template>
  <div>
    <parent-provider>
      <count-view />
      <increment-btn />
      <decrement-btn />
    </parent-provider>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import ParentProvider from "./components/ParentProvider.vue";
import CountView from "./components/CountView.vue";
import IncrementBtn from "./components/IncrementBtn.vue";
import DecrementBtn from "./components/DecrementBtn.vue";

export default defineComponent({
  components: {
    ParentProvider,
    CountView,
    IncrementBtn,
    DecrementBtn,
  },
  setup() {
    return {};
  },
});
</script>

実際の挙動の確認はこちら↓

codesandbox.io

まとめ

provideは、今回のようなルートコンポーネント以外のコンポーネントからも使用することができます。
つまり、限定的な範囲でストアを定義することが可能となります。
Vuexではグローバルに定義する必要があったため、プロジェクトの規模によってはスコープの小さい状態管理ができるProvide / injectを使用するのもいいかもしれません。
一方で、Vuexは、Fluxでデータフローなどが決まっているため、チーム内でのガイドライン等を作る手間が少なく、さらにDevToolでのデバックができることも大きな利点かと思います。
チームとしてComposition APIを使用し、かつ規模の小さいプロジェクトの場合Provide / injectを使用し、大規模なプロジェクトの場合Vuexを使う。そんな選択が今後できるかと思います。

参考にさせていただいた記事等


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

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

careers.012grp.co.jp

Laravelで既存画像のWebP対応

今回はPHPのLaravelで既存画像のWebP化及び切替表示について実装例を紹介してみようと思います。

目的

記事の画像をWebP化することによって記事の表示スピードを早くする。

WebP(うぇっぴー)とは

軽量の新画像形式で開発したグーグルによると、画像をWebP化することによって可逆形式ではpngの26%、不可逆形式では25〜34%もファイルサイズを減らすことができると紹介されています。 詳細については[こちら]をご覧ください。

Chromeブラウザのみの対応だったWebP 画像が Edge, Firefox 等の最新版で使用でき、8割近いブラウザで使えるようになってきたそうです。 WebP対応ブラウザ確認は[こちら]をご覧ください。

たくさん画像を使う記事サイトで高速化のためにWebPを使うことが有効ですが、サポートしないブラウザもあるのでWebP対応ブラウザはWebPを、非対応ブラウザは既存画像(jpegpng等)を利用するなど切替表示の必要があります。

現状

1) 画像ファイルURL

画像ファイルURLは、認証制御の理由で実際のファイルURLを利用しないで下記のようにAPIで取得します。

src="https://xxxxx.jp/api/image/1"
2) 画像ファイルの情報

画像ファイルの情報は下記のようにDBのテーブルに格納しています。

id name context_type created_at
1 image01.jpg image/jpeg 2018-02-08 16:13:38
2 image01.png image/png 2018-02-08 16:13:59

App\Models\ImageFileには上記テーブルの項目が入っています。

実装手順

1) 画像の新規登録&変更登録時対応

管理画面で画像の新規登録&変更登録する際に、アップロードされた画像からWebP画像を作成し別途保存する処理を追加

2) 画像表示の切替処理

サイトで画像を表示する際に、WebP対応ブラウザかどうか判定してWebP対応ならWebP画像、非対応なら既存の画像タイプを表示するように切替処理を.htaccessで設定

3) 既存画像の対応

バッチで一括で既存画像をWebp化する仕組みを作る

前提条件

1) libwebpのインストール

下記のコマンドで

$ php -r 'print_r(gd_info());'

下記が表示される場合、サポートされていないのでlibwebpライブラリをインストールする必要があります。

[WebP Support] => 0 
或は
[WebP Support] => 

下記のコマンドでlibwebpをインストール

$ sudo yum --enablerepo=remi-php73 install php73-gd

下記のコマンドでapache再起動

$ service httpd reload

下記のコマンドで確認

$ php -r 'print_r(gd_info());'

下記が表示されればWebPがサポートされていると考えられます。

[WebP Support] => 1

2) LaravelにIntervention Imageを追加

Composer Installation

$ php composer.phar require intervention/image

config/app.phpにパッケージを登録

'providers' => [
    Intervention\Image\ImageServiceProvider::class,
]

'aliases' => [
    'Image' => Intervention\Image\Facades\Image::class,
]

下記のコマンドで、configフォルダ内にimage.phpという設定ファイルが作成されます。

Laravelでconfigファイルを公開(作成)する場合

$ php artisan vendor:publish --provider="Intervention\Image\ImageServiceProviderLaravelRecent"

Laravelのバージョンが4以下の場合

$ php artisan config:publish intervention/image

これでLaravelでのIntervention Imageインストールは完了です。

              

1. 画像の新規&変更登録時対応

ルート

// 画像アップロード
Route::post('/image', 'ImageController@uploadImage');
// 画像取得
Route::get('/image/{id}', 'ImageController@getImage);

blade

Laravel:
{{ Form::file('image') }}

html:
<input name="image" type="file">

Controller

ImageController.php

<?php 
use App\Models\ImageFile;
use Intervention\Image\ImageManagerStatic as Image;

//画像アップロード
public function uploadImage(Request $request) {
        //  画像情報を取得
        $file = $request->file('image');
        
         // 画像情報をDBに保存
        $ImageFile = new ImageFile();
        $ImageFile->name            = $file->getClientOriginalName();
        $ImageFile->context_type  = $file->getMimeType();
        $ImageFile->created_at     = Carbon::now();
        $ImageFile->save();

        // storageフォルダ以下に画像を保存(物理的なファイルのURLがないパターン)
        $file->move(storage_path(config('system.path.imgae')), $ImageFile->id);

        // 画像を読み込む: make()
        $path = storage_path(config('system.path.imgae')).$ImageFile->id;
        $image = \Image::make($path);

        // WebPを作成
        $webp_path = storage_path(config('system.path.imgae').$ImageFile->id .'.webp');
        $image->save($webp_path);
}

2. 画像表示の切替処理

.htaccess処理

.htaccessで画像取得のURLが /image/{id}の場合、/image/{id}?webp=1にリダイレクトさせることによって、 ブラウザによって自動的に切り替えできるようにします。

<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews
    </IfModule>

    RewriteEngine On

    RewriteCond %{HTTP_ACCEPT} image/webp
    RewriteCond %{REQUEST_URI} ^/image/[0-9]*$
    RewriteCond %{QUERY_STRING} !webp=[^&]*
    RewriteRule ^(.*)$ $1?webp=1 [L]
</IfModule>

.htaccessはできることが多いので良かったら[こちら]も参考に見てください。

画像取得処理

.httaccessのリダイレクトでURLに追加した?webp=1を取得し、対応ブラウザかWebPだったらWebPに切り替えるようにします。

ImageController.php

<?php
use App\Models\ImageFile;
use Intervention\Image\ImageManagerStatic as Image;

public function getImage(Request $request, $id) {
    
    // ファイル情報を取得
    $imageFile = ImageFile::find($id);
    if (!$imageFile) {
        return;
    }

    $contextType = $imageFile->context_type;
    $fileName = $imageFile->id;
    
    // 対応ブラウザかWebPだったらWebPに切り替える
    $webp = $request->input('webp');
    if($webp == true){
        $contextType = 'image/webp';
        $fileName = $imageFile->id . '.webp';
    }

    // ファイルをダウンロードさせる
    $headers = array('Content-Type' => $contextType, 'Content-Disposition' => 'inline; filename="'.$imageFile->name.'"');
    $pathToFile = storage_path(config('system.path.imgae')).$fileName;
    $response   = response()->file($pathToFile, $headers);

    return $response;
}

3. 既存画像のWebp化対応

バッチで一括で記事既存画像のWebp化の仕組みを作ります。

app/Console/Kernel.php

<?php
use App\Console\Commands\CreateWebpFile;
class Kernel extends ConsoleKernel
{
    protected $commands = [
        CreateWebpFile::class,
    ];
}

下記のコマンドでクラスを作成

$ php artisan make:command CreateWebpFile

app/Console/Commands/CreateWebpFile.php

<?php
namespace App\Console\Commands;
use Illuminate\Support\Facades\DB;
use Intervention\Image\ImageManagerStatic as Image;

class Kernel extends ConsoleKernel
{
    protected $signature = 'CreateWebpFile';
    
     public function handle()
    {
        $imgaeFile= DB::table('imgae_file')->get();
        if (!$imgaeFile) {
            return;
        }

        foreach ($imgaeFile as $image) {
            // id取得
            $imageId = $image->id;

            $Path = storage_path(config('system.path.imgae')) . $imageId;
            $PathWebp = $Path . '.webp';

            if(file_exists($Path) && !file_exists($PathWebp)){
                // webp保存
                $image = Image::make($Path)->orientate();
                $image->save($PathWebp);
            }
        }
    }
}

プロジェクトディレクトリに入って、下記のコマンドでWebPファイルが作成されました。

$ php artisan CreateWebpFile

さいごに

バックエンドエンジニアとしてWizに入社後、アプリケーション実装だけでなくSEO対策、SSL更新、メールサーバー構築、WEBサーバー構築など幅広く挑戦・成長できるチャンスをたくさんもらいました。

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

careers.012grp.co.jp

PHPのInterface -メリットと使い所-

今回はPHPのInterfaceの使い所について、例を交えつつ紹介してみようと思います。

本題に入る前にこの記事内のサンプルコードについてです。
サンプルコードは全て、フレームワークにLaravelを使用しているという前提で記述しています。
LaravelでInterfaceを使う場合、サービスプロバイダーのregisterメソッド内でbindメソッドを使用し、実装クラスとInterfaceの紐付けを行ってください。

$this->app->bind(XxxInterface::class, ConcreteClass::class);

これがないと"XxxInterface is not instantiable..."みたいなエラーが出てしまいますので、ご注意ください。

Interfaceの概要

PHPの公式ドキュメントを確認すると、以下のように説明されています。

オブジェクトインターフェイスにより、あるクラスが実装する必要があるメソッドの 種類を、これらのメソッドの実装を定義することなく、指定するコードを作成できる ようになります。

インターフェイスは通常のクラスと同様に定義することができますが、 キーワード class のかわりに interface を用います。またメソッドの実装は全く定義されません。

インターフェイス内で宣言される全てのメソッドはpublicである必要があります。 これは、インターフェイスの特性によります。 https://www.php.net/manual/ja/language.oop5.interfaces.php

Interfaceを使うメリット

"php interface メリット"などのキーワードでググるといろいろ出てきますが、大きなメリットの一つとして

ということがあげられます。
Interfaceを使うと、複数のクラスがお互いに直接依存し合わず、Interfaceに依存している状態を作ることができます。

どういう状態なのか、以下の例1をご覧ください。

例1

  • メルマガを配信するような機能があったとする
  • 外部のメールサービスを使っている
  • メールサービスのAPI呼び出し用にライブラリが提供されており、それを使って機能を実装している

という状態があるとします。
サンプルコードで書いてみると、こういう感じです。

ある時、使用しているメールサービスを変更するような仕様変更があったとします。
その場合、上記のようにInterfaceを使った実装をしていれば、変更の影響範囲をEmailLibServiceのみに抑えることができます。

コントローラーのemailMagazineメソッドでは、EmailLibInterfaceのsendというメソッドを呼び出しています。
EmailLibServiceのsendメソッドを直接使用していません。
なので使用するメールサービスが変わった場合にEmailLibServiceの中身を書き換えようが、新しくクラスを作って差し替えようが、EmailLibInterfaceさえimplementsされていれば呼び出し元に影響が及ぶことはありません。

例えばコントローラーからライブラリのクラスを直接newして使用していたり、あるいはEmailLibServiceのメソッドを直接呼び出していたとすると、コントローラーにも影響が及んでいたかもしれません。

Abstract classでもいいのでは?

Abstract classについてはこちらをご覧ください。

https://www.php.net/manual/ja/language.oop5.abstract.php

InterfaceとAbstract classは

  • 抽象メソッドを定義できる
  • 定義されたメソッドを継承先のクラス(Interfaceは実装クラス)で必ず実装しなければならない
  • そのままでは使えない(直接newしてインスタンスを作成したりできない)

などの点で似ています。しかし

  • Interfaceのメソッドは全てpublicでなければならない
  • Abstract classはInterfaceと違って、メソッドに具体的な実装内容を持たせることができる

などの違いがあります。
なのでAbstract classを使えば共通処理を具体的な実装部分まで定義してしまって、動きが変わる部分のみ抽象メソッドで定義するようなことが可能です。
Abstract classの方ができることが多く自由度が高いので、Interfaceを使う必要がなさそうに思えます。
しかし、具体的な処理を持った共通メソッドに変更を加える必要がある場合、継承先クラス全てに影響が及んでしまうというデメリットがあります。
そう考えるとInterfaceの場合は元々実装内容を持たないので、変更に強いと考えられます。
振る舞いが異なるという前提で同じメソッドを不特定多数のクラスに持たせたい場合は、Interfaceを使用するメリットがあるのではないでしょうか。
ということを踏まえた上で、2つ目の例です。

例2

例えばユーザーに何かしらの通知を送るようなことがあったとして

  • パラメーターによってどういう通知を送信するかを変えたい
  • 通知のフォーマットや通知に必要な要素はバラバラ

みたいな条件を満たして実装したい場合、以下のような使い方ができます。

(参考: https://youtu.be/cUV1nXPfjFY)

Interfaceは複数implementsできる

もう一つAbstract classとInterfaceの特徴的な違いは、Interfaceは複数implementsできるという点です。

https://www.php.net/manual/ja/language.oop5.interfaces.php#language.oop5.interfaces.implements

PHPは多重継承ができない仕様です。Abstract classを使用する場合、あくまでもクラスの継承なので1つのAbstract classしか継承できません。対してInterfaceに個数の縛りはありません。カンマ区切りで複数implementsが可能です。

例えば例2で "ある通知クラスの場合は管理者にも同時に通知を送りたい" みたいな場合、以下のような形で複数implementsすると便利なのではないでしょうか。

↑のように空のInterfaceを作成して、自作の型のように手軽に扱えるのもInterfaceのいいところだと思います。

さいごに

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

careers.012grp.co.jp

Next.js10新機能 next/imageを使ってみた

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

2020年10月27日にNext.js10がリリースされ、パフォーマンスを改善するための20個を超える新しい機能が追加されました。

nextjs.org

私たちフロントエンドチームでも、プロジェクトにNext.js10を導入しており様々な恩恵を得ています。

本記事では、その中でも最も便利だと感じたnext/imageについてご紹介したいと思います。

next/imageとは

next/imageとは、画像の表示を自動で最適化してくれるコンポーネントです。

主に以下の最適化を行います。

  1. WebPフォーマット*1変換(対応ブラウザのみ)
  2. 画像遅延読み込み(Lazy Load)
  3. レスポンシブ対応

導入方法

Next.jsにデフォルトで備わっているため、next/imageをimportするだけで使用することができます。

基本的に<img>要素の書き方と変わりませんが、width・heightの記述は必須ですので注意が必要です。

import Image from "next/image"
~~~
<Image src="/free.jpg" width={600} height={400} />

画像比較

では実際に<img>next/imageの比較をし、どのように画像の最適化が行われているか確認してみましょう。

比較対象として、以下のフリー画像を使用します。(free.jpg 843kb)

林道

▼ コード

import Image from "next/image"

const ImageArea = () => {
  return (
    <>
      // 通常のimg
      <img src="/free.jpg" width={600} height={400} alt="通常のimg 林道"/>
      // next/imageのコンポーネント
      <Image src="/free.jpg" width={600} height={400} alt="next/imageのコンポーネント 林道"/>
    </>
  )
}

export default ImageArea

表示を見てみると、同じ見た目の画像が並んでいることが確認できます。

林道画像比較

こちらは最終的に吐き出されたnext/imageのHTMLです。

next/imageのHTML

src/srcset属性を見てみると、/_next/imageを参照していることが確認できます。

/_next/imageビルド時に生成される画像サーバーでして、こちらにパラメータを送ることで表示させる画像の切り替えを行っています。

以下は開発者ツールの画像読み込み箇所です。

開発者ツールの画像読み込み箇所

next/imageではWebPフォーマットでの表示となっており、ファイルサイズが70%程削減されています。

様々な画像で試してみたのですが、元々のファイルサイズが大きい画像ほど圧縮率が高い傾向があるようです。

次にレスポンシブ対応を確認してみましょう。

以下のように、next/imageの画像のみレスポンシブが適用されていることが確認できます。


width・heightの直接的な指定は、レイアウトまわりで色々と問題を引き起こす原因となるため避けがちです。

しかし、next/imageではアスペクト比に基づいて自動的にレスポンシブ対応となるため問題なく指定することができます。

最後に遅延読み込みによる最適化です。

左の開発者ツールを見てみると、画像から200px程近づいた時に読み込みを行っていることが確認できます。


このように、next/imageでは「サイズ最適化」「遅延読み込み」「レスポンシブ対応」の最適化を全て自動で行ってくれます。

外部ファイルの読み込み

外部サーバーから画像を読み込む場合、そのままURLを指定するとエラーが出てしまいます。

next.config.jsに以下の設定を追記することで、Next.js側がURLを認識し画像の読み込みをしてくれます。

module.exports = {
  images: {
    domains: ["example.com"], //ドメインを指定
  },
}

オプション引数

next/imageでは豊富なオプション引数が存在しています。

以下はオプション引数の設定例です。

<Image
  src={"/free.jpg"}           // 画像へのパスまたはURL ※必須
  width={600}                 // 画像の横幅 ※必須
  height={400}                // 画像の高さ ※必須
  layout={"fill"}             // レイアウト, "fill" | "fixed" | "intrinsic" | "responsive", default:"intrinsic"
  sizes={"80vw"}              // メディアクエリのマッピングサイズ, default:"100vh"
  quality={50}                // 画質, default:100
  priority={true}             // 表示優先度, default:false
  loading={"lazy"}            // 遅延読み込み, "lazy" | "eager", default:"lazy"
  unoptimized={false}         // 画像最適化, default:true
  objectFit={"cover"}         // object-fit ※layout='fill'の場合
  objectPosition={"50% 50%;"} // object-position ※layout='fill'の場合
/>

対象の画像に合わせて柔軟に設定できることもnext/imageの魅力の一つです。

それぞれの引数の詳細は下記の公式ドキュメントに記載されてますので、興味のある方は見てみてください。

nextjs.org

まとめ

以上next/imageの導入方法・最適化についての説明でした。

画像はWebページのトータルバイト数のうち50%を占めていると言われてますので、next/imageを積極的に活用しパフォーマンスを改善していきましょう!

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

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

careers.012grp.co.jp

*1:Google開発の画像フォーマット。ファイルサイズを大幅に軽量化できる。

Lambda@EdgeでCloudFrontの配信をコントロールしたい

Lambda@Edgeとは

CloudFrontのエッジサーバでコードを実行するLambda関数です。

配信をカスタマイズ出来ます。

感覚としてはCloudFrontのフックで動くLambda、という感じですが考慮する点・制約がいくつかあります。

  • 番号付きバージョンのみをトリガーに設定出来る(エイリアスに設定出来ない)
  • バージニア北部(us-east-1)リージョンのみ

詳しくはドキュメント

設定出来るフック

f:id:wiz-yoshitomi:20210126161055j:plain

Lambda@Edgeがフックに設定出来る箇所は四つあります。

  • ビューワーリクエス
  • ビューワーレスポンス
  • オリジンリクエス
  • オリジンレスポンス

キャッシュがある時はオリジンリクエスト・オリジンレスポンスは発生しません。

例:webp画像を配信して表示速度高速化したい

  1. リクエストした画像(jpeg,png)のwebpファイルが存在したらwebpファイルを取得する
  2. 存在しない場合はwebpファイルを作成してから取得する

このようなLambda@Edgeを作成し、オリジンリクエストに設定してみます。

前提:S3+CloudFrontの構成は既に作成されているものとする。

Lambdaを用いたS3画像の取得・加工はドキュメントを参考にしています。

①IAMロールを作成

アクセス許可

AWSLambdaExecuteをアタッチ

信頼関係

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "edgelambda.amazonaws.com",
          "lambda.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

② Lambda関数を作成

最初に述べた制約に則り関数を作成します。 今回の設定は以下です。

リージョン:us-east-1 ランタイム言語:Node.js 12.x 実行ロール:前項目で作成したロールを設定

③関数コードを作成

index.jsファイルを作成

const AWS = require('aws-sdk');
const sharp = require('sharp');
const https = require('https');
const url = require('url');
const s3 = new AWS.S3();

const BASE_URL = 'CloudFrontURL';
const BUCKET_NAME = 'S3バケット名';

exports.handler = async (event, context) => {
    const request = event.Records[0].cf.request;
    
    // 画像のみ
    if (request.uri.match(/\.(jpe?g|png)$/) && request.uri.split[1] !== 'tmp') {

        const webpUri = request.uri + '.webp';
        let cloudfrontReq = url.parse(BASE_URL + webpUri);
        let webpExits = false;

        // webpファイルへ正常にアクセス出来るか
        await new Promise((resolve, reject) => {
            cloudfrontReq.method = 'HEAD';
            cloudfrontReq.timeout = 3000;
            https.request(cloudfrontReq, cloudfrontres => {
                if (cloudfrontres.statusCode >= 200 && cloudfrontres.statusCode < 300)
                    webpExits = true;

                resolve();
            }).on('error', reject).end();
        });

        // webpファイルが無い場合は作成する
        if (!webpExits) {

            const key = request.uri.substr(1)
            const webpKey = webpUri.substr(1)

            // オリジンの画像を取得
            try {
                const params = {
                    Bucket: BUCKET_NAME,
                    Key: key
                };
                let origimage = await s3.getObject(params).promise();

            } catch (error) {
                console.log(error);
                return;
            }

            // webpへ変換
            try {
                let buffer = await sharp(origimage.Body).webp().toBuffer();

            } catch (error) {
                console.log(error);
                return;
            }
            
            // S3へput
            try {
                const putParams = {
                    Bucket: BUCKET_NAME,
                    Key: webpKey,
                    Body: buffer,
                    ContentType: "image"
                };

                const putResult = await s3.putObject(putParams).promise();
                webpExits = true;
            } catch (error) {
                console.log(error);
                return;
            }
        }
        
        // webpが存在する or 変換が成功した場合webpのuriでrequestを上書きする
        if (webpExits)
            request.uri = webpUri;
    }

    return request;
};

④デプロイパッケージを作成

convert-webpフォルダ(名前は任意)を作成し、そこにindex.jsを入れ、そこにnpmでsharpライブラリをインストールします。

$ npm install --arch=x64 --platform=linux --target=12.13.0 sharp

今回の場合のファイル構造は、以下のようになります。

convert-webp
├ index.js
└ node_modules
    ├ sharp
    ├ ...

⑤.zipファイルをアップロード

作成したファイルをzipへ圧縮し、「関数コード」→「アクション」→「.zipファイルをアップロード」の手順でアップロードを行います。

⑥テストを行ってみる

こちらのドキュメントより、オリジンリクエストのイベント構造を確認出来ます。

⑦ Lambda@EdgeをデプロイしCloudFrontへ設定する

「デザイナー」→「トリガーの追加」をクリックします。

「トリガーを選択」→「CloudFront」で出現する「Lambda@Edgeへのデプロイ」項目を選択します。

f:id:wiz-yoshitomi:20210126181909p:plain

以上を入力し「デプロイ」をクリックで設定完了です。

⑧確認

DevToolを開き「Networkタブ」→対象の画像を選択

Responce headerのcontent-typeがimage/webpになっていればOKです!

注意点なのですが今回はオリジンリクエストにフックしているため、キャッシュが残っている状態だとオリジンリクエストが発生せず関数が発火しません。

確認の際はご注意ください。

まとめ

今回は省略していますが、CloudFrontのheaderのホワイトリストAcceptを追加し、webpを許可しているブラウザかの確認の処理があった方が良いかと思います。

Lambda@Edgeを使用せずにS3へのオブジェクト追加をフックにしたLambdaでwebpファイルを生成するという方法も考えられますが、同一バケットの「オブジェクト生成」フックで新規オブジェクト追加する構成は再起的に実行されてしまう危険性があるため推奨されていません。

やはりキャッシュがない時(オリジンリクエストが発生する時)の起動は体感でも遅く感じるので、ユースケースに合わせてキャッシュの有効期限を長くしたりなど対処した方が良いかなと思いました。

最近話題のLambdaのコンテナイメージフォーマットもどうやら使えるようなので、こちらも試してローカルまで一貫したサクサク開発にしてみたいです。

〜最後に〜

Wizではエンジニアを募集中です!

興味のある方は是非ご覧ください!

careers.012grp.co.jp