ReactNative&ExpoによるPush通知(iOS)の実装

こんにちは。
最近ReactNative&ExpoでPush通知を実装する機会があったので、その内容を記事にさせていただきました。
私個人として、Expoを利用したPush通知機能の開発は

●難易度
  仕組みを理解することで実装自体は比較的簡単
  証明書などの手続きが基本的に不要。

●開発することによる効果
  簡易に通知を実装し、アプリの価値を高めることができる。

という点から、Expoでのアプリ開発をする上では非常にオススメの機能です。

その反面、ExpoでのPush通知の実装においては、

・日本語の記事はいくつかあるものの、サンプルが動かなかった。
・Push通知の仕組みも押さえておきたかったが、実装のコードのみの記事が多かった。
・テスト時の情報があまりなく、push通知のテスト部分で少し苦労した。


そういった経験から今回の記事を作成させていただきました。
私自身、そもそもPush通知の仕組みとは?という知識でしたので、Push通知の仕組みも含めて解説させていただきます。
また、今回はiOSでのテストのみ対象としています。
※基本はandroidでも同様ですが、androidの場合は追加で少し設定が必要です。

対象読者

①Expoを利用してより簡易的にPush通知を実装・テストしたいという方。
②そもそもPush通知とは?という方。

目次

1.環境
2.そもそもPush通知とは
3.Expoの提供するPush通知とは
4.実装①ライブラリインストール
5.実装②Push通知許可と通知用トークンの取得
6.実装③通知を開く・フォアグラウンドでの通知受信時の動作
7.ポイント Expoへのログイン
8.テスト①Expo Goのインストール
9.テスト②Push通知を送る
10.まとめ

1.環境

本記事では下記環境が構築されていることを前提としています。
・Expo(40.0.0)
・その他(上記Expoのバージョンに依存)
・テスト用iPhone (今回はiPhoneSE iOS 14.4を使用)
※本記事はReactNative&Expoの環境構築は省いています。

2.そもそもiOSのPush通知とは

上記の図のように

①端末側でユーザからPush通知の許可を取得。
②APNsからPush通知先の端末を特定するトークンを発行。
③②で発行されたトークン情報をサーバに登録。
④③で登録したトークン情報をキーに通知したい情報をApple社のPush通知用サーバに送信。
⑤Apple社のPush通知用サーバから各ユーザの端末にPush通知を送信。

上記のような仕組みとなり、トークンの発行や通知の送信などはApple社のPush通知用サーバを介する必要があります。
※通常はFirebaseなどのツールを利用することが多いかと思います。
また、iOSのPush通知を利用する場合には証明書の発行などの手続きを行う必要があります。

3.Expo(expo-notifications)を利用したPush通知の仕組み

上記の図のように

①端末側でユーザからPush通知の許可を取得
②Expoを介してApple社のPush通知用サーバから端末を特定するトークンを発行。
③②で発行されたトークン情報をサーバに登録
④③で登録したトークン情報をキーに通知したい情報をExpoのサーバを介してApple社のPush通知用サーバに送信。
⑤Apple社のPush通知用サーバから各ユーザの端末にPush通知を送信

基本的にはやはりApple社のサーバを介す必要があるのですが、そのあたりのやり取り(トークンの取得や通知情報の送信)をExpoが代行して行ってくれるため、
「とりあえずPush通知を実装したい」という場合は非常に簡易的にPush通知を実装することができます。
また、何より証明書発行などの手続きが必要ないということがとても大きなメリットかと思います。

4.実装①ライブラリインストール

今回必要となるライブラリは以下の通りです。

expo-notifications
Push通知を実装するためのコアとなるライブラリ

expo install expo-notifications

expo-constants
Push通知は実機でのみ確認可能となるため、実機かどうかの判定を入れています。

expo install expo-constants

5.実装②Push通知許可と通知用トークンの取得

registerForPushNotificationsAsync
①このアプリからのPush通知の許可を取得
②初回起動時は許可ダイアログを出してユーザからPush通知の許可を取得
③通知用トークンの取得
※今回はサーバなどには送信せず、画面にトークンを表示します。

App.js

import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import React, { useState, useEffect, useRef } from 'react';
import { Text, View, Button } from 'react-native';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: false,
  }),
});

export default function App() {
  const [expoPushToken,setExpoPushToken] = useState(null);

  return (
    <View
      style={{
        flex: 1,
        alignItems: 'center',
        justifyContent: 'space-around',
      }}>
      <Text>push通知のトークン: {expoPushToken}</Text>
      <Button
        title="push通知用のトークンを取得"
        onPress={async () => {
          const pushToken = await registerForPushNotificationsAsync()
          setExpoPushToken(pushToken);
        }}
      />
  </View>
  );
}


async function registerForPushNotificationsAsync() {
  let token;
  if (Constants.isDevice) {
    //①このアプリからのPush通知の許可を取得
    const { status: existingStatus } = await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;
    if (existingStatus !== 'granted') {
       //②初回起動時は許可ダイアログを出してユーザからPush通知の許可を取得
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }
    if (finalStatus !== 'granted') {
      //許可がない場合
      alert('Failed to get push token for push notification!');
      return;
    }
    //③通知用トークンの取得
    token = (await Notifications.getExpoPushTokenAsync()).data;
    console.log(token);
  } else {
    //実機以外の場合
    alert('Must use physical device for Push Notifications');
  }
  return token;
}

6.実装③通知を開く・フォアグラウンドでの通知受信時の動作

④ユーザが通知をフォアグラウンドで開いた場合のリスナー
今回は「フォアグラウンドで通知を受信しました」と表示することにします。
⑤ユーザが通知を開いた場合のリスナー
今回は「通知を開きました」と表示することにします。

App.js

import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import React, { useState, useEffect, useRef } from 'react';
import { Text, View, Button } from 'react-native';

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: false,
    shouldSetBadge: false,
  }),
});

export default function App() {
  const notificationListener = useRef();
  const responseListener = useRef();
  const [pushState,setPushState] = useState(null);
  const [expoPushToken,setExpoPushToken] = useState(null);

  useEffect(() => {
    // ④ユーザが通知をフォアグラウンドで開いた場合のリスナー
    notificationListener.current = Notifications.addNotificationReceivedListener(notification => {
      setPushState("フォアグラウンドで通知を受信しました。");
    });

    // ⑤ユーザが通知を開いた場合のリスナー
    responseListener.current = Notifications.addNotificationResponseReceivedListener(response => {
      setPushState("通知を開きました。")
    });
    // userEffectのreturnに登録する関数は、コンポーネントがunmountされるときに実行される。ここで主にcleanup処理を定義する
    return () => {
      Notifications.removeNotificationSubscription(notificationListener);
      Notifications.removeNotificationSubscription(responseListener);
    };
  }, []);
  return (
    <View
      style={{
        flex: 1,
        alignItems: 'center',
        justifyContent: 'space-around',
      }}>
      <Text>push通知のトークン: {expoPushToken}</Text>
      <View style={{ alignItems: 'center', justifyContent: 'center' }}>
        <Text>push通知受信時の動作: {pushState} </Text>
      </View>
      <Button
        title="push通知用のトークンを取得"
        onPress={async () => {
          const pushToken = await registerForPushNotificationsAsync()
          setExpoPushToken(pushToken);
        }}
      />
  </View>
  );
}


async function registerForPushNotificationsAsync() {
  let token;
  if (Constants.isDevice) {
    ////①このアプリからのPush通知の許可を取得
    const { status: existingStatus } = await Notifications.getPermissionsAsync();
    let finalStatus = existingStatus;
    if (existingStatus !== 'granted') {
      //②初回起動時は許可ダイアログを出してユーザからPush通知の許可を取得
      const { status } = await Notifications.requestPermissionsAsync();
      finalStatus = status;
    }
    if (finalStatus !== 'granted') {
      //許可がない場合
      alert('Failed to get push token for push notification!');
      return;
    }
    //③通知用トークンの取得
    token = (await Notifications.getExpoPushTokenAsync()).data;
    console.log(token);
  } else {
    //実機以外の場合
    alert('Must use physical device for Push Notifications');
  }
  return token;
}

7.ポイント Expoへのログイン

expo-notifiationsの機能を利用してテストをするためには、Expoにログインしておく必要があります。
以下のコマンドでログインしておくことでテストが可能になります。
Expoのユーザはこちらで作成可能です。

expo login

8.テスト①Expo Goのインストール

storeから「Expo Goアプリ」をインストールします。

アプリインストール後、[expo start]を実行した際に表示されるQRコードを実機でスキャンします。
これで画面が表示されるはずです。

9.テスト②Push通知を送る

Push通知を送信するためには、Expoの提供するAPIに必要な情報を送信する必要があります。
下記のようなJSONを「https://exp.host/–/api/v2/push/send」に対してPOST送信します。

[
  {
    "to": "取得したpush通知用のトークン",
    "title":"通知のタイトル部分",
    "body": "通知の内容部分"
  }
]

この際、ヘッダー情報は下記のものを設定します。
host: exp.host
accept: application/json
accept-encoding: gzip, deflate
content-type: application/json

例えば以下のようなPOST送信用のツールから情報を送信した場合

通知受信時(フォアグラウンド)

通知を開いた場合

10.まとめ

今回はできるだけ時間をかけずに早くリリースしたいということから「Expo」と「Expoが提供するPush通知用ライブラリ」の組み合わせでPush通知の実装・テストをより簡易的に行いました。
Push通知と聞くと「何やら難しそう」と思っていましたが、Expoを利用することでとても簡単に実装することができます。
是非皆さんも気軽にPush通知を実装してみてください。

Jaspersoft Studioを使用して帳票作成してみた

こんにちは。ソフトウェア開発をしていると領収証や請求書などの帳票を作成する機会があると思います。
最近、初めて帳票を改修する機会がありました。実装を見れば、ある程度何となくでも行けそうな箇所もありましたが、今回主に以下3点について改修したく、その際に知ったことを中心に改修箇所のポイントとして記事にすることにしました。
① 帳票に新たな項目と値を表示させたい
② 画像の変更をしたい
③ 明細部分の表示の実装を変更したい
※本記事は、帳票作成したことはないが、 既存プロジェクトに帳票がすでに作成されており、その改修作業をする人向けに記載をしております。そのため、実際のソースコードを追えば、 ある程度はできるであろうということを前提としております。

目次

● 環境
● はじめに
● 帳票作成ツール
● ポイント①:データソースの作成・紐づけ
● ポイント②:イメージの追加
● ポイント③:メインレポート内にサブレポートの埋め込み
● javaを使用したコンパイル・レポート出力方法
● Jaspersoft studio を使用したレポート出力方法
● まとめ

環境

本記事では、下記環境を前提に検証を行っております。
・プログラミング言語:Java (jdk1.8.0_77)
・帳票ライブラリ:JasperReports (v6.4.0)
・帳票作成ツール:Jaspersoft Studio (v6.16.0)

はじめに

帳票を作成するためには、「JasperReports」と呼ばれるJavaで動作するオープンソースの帳票出力ライブラリを使用します。
JasperReportsでは、帳票のレイアウト等が記述されたXMLファイルを読み込んで帳票を出力します。

また、作成されたレポートは、PDF、RTF、HTML/XHTML、MS Word(Docx)、MS Excel(XLS)、Open Officeなど様々な形式で出力することができます。出力方法は、Javaを使用した出力方法 または Jaspersoft Studioを使用した出力方法 を参照ください。

※ 拡張子は「.jrxml」です。
※ 「.jrxml」をコンパイルすると、「.jasper」という拡張子ファイルになります。

JasperReportsについて詳しく知りたい方は、こちら を参照ください。

帳票作成ツール

まずは、帳票作成ツールをご紹介します。
帳票を作成する際によく使用されるツールが、「Jaspersoft Studio」です。これは、レポートのレイアウトをマウス操作で設計できるツールです。
テンプレートファイルはXMLファイルの為、手書きすることも可能ですが、こういったデザインツールを使用した方が作業が捗ります。
ダウンロードは、こちら から

※本ページでは、プロジェクトの作成方法および基本的な使用方法の説明は割愛いたします。詳しく知りたい方は、こちら を参照ください。

ポイント①:データソースの作成・紐づけ

データベースからの取得結果やJava側で処理した値などを帳票に出力するには、帳票の項目へのマッピングが必要になります。マッピングさせるうえでの注意点は、以下2点です。

<1点目>
 Javaファイルとjrxmlファイルで項目名を完全一致させること

<2点目>
 Javaファイルとjrxmlファイルで項目の型が同じであること

以下は、JavaファイルJaspersoft studioを使用したデザインイメージそれをjrxmlファイルで見た場合の3つを表しております。
見ていただけると、同じ項目名・型になっていることがお分かりいただけると思います。

Javaソース

        // フィールドの設定値
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
        
        // パラメーターの設定値
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("tel", "090-9999-9999");

Jaspersoft studioを使用したデザインイメージ

jrxmlファイル

jrxmlファイルでは、項目名と型を以下のように<field>または<parameter>を使用して宣言します。
<field name="項目名" class="型"/>
<parameter name="項目名" class="型"/>
このように宣言してあげることで、それ以降、<textField>内などで使用が可能となります。

<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.16.0.final using JasperReports Library version 6.16.0-48579d909b7943b64690c65c71e07e0b80981928  -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="MyProj" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" whenResourceMissingType="Error" uuid="3ea3dc26-1bc4-4f32-b3d4-e877955c227d">
	<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
	<property name="net.sf.jasperreports.print.create.bookmarks" value="false"/>
	<import value="java.io.ByteArrayInputStream"/>
	<parameter name="tel" class="java.lang.String"/>
	<field name="name" class="java.lang.String"/>
	<pageHeader>
		<band height="252">
			<textField isBlankWhenNull="true">
				<reportElement key="" x="20" y="10" width="300" height="30" uuid="856ed887-d16d-462c-b786-9d1d44619953">
					<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
					<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
				</reportElement>
				<textElement verticalAlignment="Middle">
					<font fontName="IPA ゴシック" size="24" pdfFontName="HeiseiKakuGo-W5" pdfEncoding="UniJIS-UCS2-HW-H" isPdfEmbedded="false"/>
				</textElement>
				<textFieldExpression><![CDATA["My name is " + $F{name}]]></textFieldExpression>
			</textField>
			<textField isBlankWhenNull="true">
				<reportElement key="" x="20" y="48" width="230" height="32" uuid="1dc9d4a3-e551-4433-b948-544f90d6dbf8">
					<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
					<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
				</reportElement>
				<textElement verticalAlignment="Middle">
					<font fontName="IPA ゴシック" size="24" pdfFontName="HeiseiKakuGo-W5" pdfEncoding="UniJIS-UCS2-HW-H" isPdfEmbedded="false"/>
				</textElement>
				<textFieldExpression><![CDATA["TEL : " + $P{tel}]]></textFieldExpression>
			</textField>
		</band>
	</pageHeader>
</jasperReport>

ポイント②:イメージの追加

イメージの追加方法には、主に以下の2点があります。

【方法①:画像が格納されているパスを記載する方法】
 こちらは一般的に使用されている方法で、プロジェクトにイメージを保存し、そのファイルパスを指定します。

<image>
  <reportElement x="200" y="10" width="210" height="80"/>
  <imageExpression class="Java.lang.String"><![CDATA["./sample.jpg"]]></imageExpression>
</image>

【方法②:Base64を使用する方法】
  Base64とは、64進数を意味する言葉で、すべてのデータをアルファベット(a~zA~z)と数字(0~9)、一部の記号(+,/)の64文字で表すエンコード方式です 。
そのため、方法①:ファイルパスの指定のようにプロジェクトに画像を保存する必要がありません。Base64について詳しく知りたい方は、こちら を参照ください。
手順は以下の通りです。

<手順①:Base64エンコーダーでBase64形式に変換>
  Base64エンコーダーと呼ばれるBase64形式にデータを変換してくれるソフトを使用します。ネットで検索するといくつも出てきますので、それを使用し、Base64形式に変換します。今回は、こちら を使用しました。

<手順②:コードの貼り付け>
 Base64形式のコードを、<parameter> に設定します。

<parameter name="image" class="java.lang.String">
 <defaultValueExpression><![CDATA["iVBORw0KGgoAAAANSUhEUgAABkA(長すぎるため、省略)AAD2EAAA8jWdB7OD"]]></defaultValueExpression>
</parameter>
<image hAlign="Left" vAlign="Bottom">
	<reportElement x="403" y="168" width="125" height="41" uuid="3261ff18-10ac-443e-8ee6-1ec3d3e8a3d9">
		<property name="com.jaspersoft.studio.unit.x" value="pixel"/>
		<property name="com.jaspersoft.studio.unit.y" value="pixel"/>
		<property name="com.jaspersoft.studio.unit.height" value="pixel"/>
		<property name="com.jaspersoft.studio.unit.width" value="pixel"/>
	</reportElement>
	<imageExpression><![CDATA[new java.io.ByteArrayInputStream(javax.xml.bind.DatatypeConverter.parseBase64Binary($P{image}))]]></imageExpression>
</image>

ポイント③:メインレポート内にサブレポートの埋め込み

請求書などを作成していると、商品の明細を出力することがあり、出力するレコードは可変になるパターンがほとんどです。その際に使用するのが、サブレポート機能です。
サブレポート機能とは、メイン帳票の中に複数の別の帳票(サブレポート)を配置することができる機能です。

ポイントとなるのは、以下2点です。

1:サブレポートとなるjrxmlファイルのパスの設定
  <subreportExpression>タグ内に、サブレポートとなるjrxmlファイルのパスを記載します。

2:サブレポートに渡す値の引数の設定
  <dataSourceExpression>タブ内に、「
net.sf.jasperreports.engine.data.JRBeanCollectionDataSource(引数) 」を記載し、サブレポートに渡す値を設定します。

<subreport overflowType="Stretch">
 <reportElement key="" stretchType="RelativeToTallestObject" mode="Transparent" x="-20" y="6" width="595" height="165" isRemoveLineWhenBlank="true" printWhenGroupChanges="Group1" uuid="12fdf542-a1d5-4e85-9ee7-c955f413a668">
  <property name="com.jaspersoft.studio.unit.height" value="pixel"/>
  <property name="com.jaspersoft.studio.unit.y" value="pixel"/>				  
 </reportElement>				 
 <dataSourceExpression><![CDATA[new net.sf.jasperreports.engine.data.JRBeanCollectionDataSource($F{lineDataList})]]></dataSourceExpression>				 
 <subreportExpression><![CDATA[$P{SUBREPORT_DIR} + "subreport.jasper"]]></subreportExpression>
</subreport>

Javaを使用したコンパイル・レポート出力方法

実際に java から帳票を生成してみます。今回は、PDFを出力します。
流れは以下の通りです。

① パラメータ・データソースの生成

 帳票に出力したいデータを生成します。

② jrxmlファイルのコンパイル

対象の jrxmlファイルを読み込んで、「JasperCompileManager.compileReportToFile(対象のファイルパス)」を使用し、 jasperファイルへコンパイルします。

③ データバインド

 「 JasperFillManager.fillReport(JasperReport, パラメータ, データソース)」を使用し、対象のjasperファイルへデータをバインドします。

④ PDF出力

exportReportToPdfFile(生成されたレポートオブジェクト, 生成ファイル名)」に生成したいファイル名を引数に指定し、PDFを出力します。なお、
exportReportToPdfFile()」 を変更することで、HTMLやXML等で出力することが可能です。こちら が参考になります。

以下は、実際に作成してみたソースコード例です。

    public static void main(String[] args) throws Exception {

        // フィールド(データソース)の設定
        TestDetailDto testDetailDto = new TestDetailDto();
        testDetailDto.setName("tanaka taro");
        List<?> dataSourceList = Arrays.asList(testDetailDto);
        JRDataSource res = new JRBeanCollectionDataSource(dataSourceList);

        // パラメーターの設定
        Map<String, Object> parameters = new HashMap<>();
        parameters.put("tel", "090-9999-9999");

        try {
            // jrxmlファイルをjasperファイルへコンパイル
            File jrxmlFile = new File("sample.jrxml");
            JasperReport report = JasperCompileManager.compileReport(jrxmlFile.getAbsolutePath());
            // パラメータとデータソースを埋め込む
            JasperPrint jasperPrint = JasperFillManager.fillReport(report, parameters, res);
            // PDF出力
            JasperExportManager.exportReportToPdfFile(jasperPrint, "sample.pdf");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Jaspersoft studio を使用したレポート出力方法

Jaspersoft studioでは、作成したレポートを様々な形式で出力することができます。これは、作成後に[Preview]画面から出力したい形式を選択することで出力することが可能です。

まとめ

今回は、帳票出力する際に使用されるツールとjrxmlファイルのソースコードの書き方をいくつかポイントを絞ってご紹介しました。Jaspersoft studio は、帳票作成にはかかせないツールであり、柔軟な帳票を作成することができます。また、jrxmlファイル内で、コードがどう記載されているかを知るということが重要です。コードがわかるようになると、ある程度のことは Jaspersoft studio を使用せずとも、改修できるようになると思います。 ぜひみなさんもいろいろな帳票を作成してみてください。

WEB業界におけるディレクターとは何か?

こんにちは。SBWorksでディレクターとして働いているWと申します。前職も含めると、かれこれ10年近くWEB業界でディレクターというお仕事をやらせてもらっていますので、これまでの経験にもとづき、

  • WEB業界のディレクターの仕事内容
  • ディレクターに必要なスキル

についてお話しさせていただければと思います。

WEB業界のディレクターの仕事内容は?


身も蓋もない話ですが、正直に言ってしまうと、ひとくちにディレクターと言っても、その仕事内容は働く会社やアサインされるプロジェクトによって異なり、明確に「これだ」と定義するのは難しいと思います。

担保する工程で分けるとすると、
いままでいくつかの現場で見聞きしてきた中でも、

  • パターン1:ビジネス検討およびシステム概要の検討
  • パターン2:ビジネス検討から実装まで
  • パターン3:ビジネス検討とシステム概要の検討+プロマネ

という3つのパターンがありましたので、
現場によりディレクターのカバー範囲となる工程が異なるのが分かっていただけるかと思います。

また経験上、ディレクターは得意とする領域により、下図のように4つの系統に分けることもできるかと思います。

コアスキルを中心として、

  • SEOやWEB広告などが得意なら、マーケティング系ディレクター
  • 開発マネジメントが得意なら、開発系ディレクター
  • UI/UXなどが得意なら、フロント系ディレクター
  • 業務知見が豊富にあり業務運用が得意なら、バックエンド系ディレクター

という感じですね。
※ディレクターのコアスキルに関しては後述いたします。

ただ、どの系統にも共通して概ね、

  1. ビジネスの検討を行うこと(自社サービスであれば自社メンバと、お客様がいるのであればお客様と)
  2. そのビジネスを実現するための要件定義を行う
  3. 必要に応じてプロジェクトのマネジメントや設計・実装・テストを行う

というのがディレクターとしての仕事の流れになります。

なお、SBWorksでは「ビジネス検討とシステム概要の検討+プロマネ」のパターンの「開発系ディレクター」が多く、私もいま現在この役割を担っています。

ディレクターに必要なコアスキルは何か?


どの系統のディレクターにも必要なコアスキルがある、
とお話ししましたが、その中でも代表的なものを4つ、ピックアップします。

・コミュニケーション能力

ディレクターはビジネス検討もしくはそれに近いフェーズを担うため、ビジネスやプロジェクトの意思決定者と会話し、判断いただく機会がかなり多いです。ビジネスやプロジェクトの方針を意思決定者に判断頂く際、なかなかGOが出ないとその分スタートは遅れ、機会損失になってしまいます。そのため、コミュニケーション能力…厳密に言えば「物事を分かりやすく、端的かつロジカルに説明できる能力」は必須能力と言えます。

・ITに対する幅広い知見

自社サービス然り、お客様のビジネス然り、予算は無尽蔵にあるわけでありません。何をどう組み合わせればそのサービス・ビジネスは費用対効果高く実現できるのか、ディレクターはシステム概要の検討時に模索する必要があります。そのため、実際にシステムを構築できなくても構わないので、プログラミング言語の特性や各種インフラ・ミドルウェアの特徴など、幅広い知識を自分の引き出しから出せるようにしておく必要があります。

・関係者調整力

ディレクターは社内外含めて複数のステークホルダーと関わる場面が多いですが、ステークホルダー全員の要望が一致していることは稀です。全員の要望を叶えようとするとプロジェクトは一向に前に進まないため、第三者視点に立ち、各ステークホルダーの要望を取りまとめて1つのプラン・方向性を練り上げる、といった役割がディレクターに求められます。そのため、皆の要望を聞きつつ、その落としどころを探って方々を説得して回り推進する「関係者調整力」がとても重要なスキルになってきます。

・提案力

特にお客様のビジネスをお手伝いする場合ですが、ディレクターに求められるものは、「筋の良い提案」です。現在の状況や様々な事象を思考フレームワークを使いながら分析し、クリティカルな課題を抽出、その課題を解決する方法をメリット・デメリット等とあわせて分かりやすく資料にまとめ、お客様に提案する…といった、筋の良い提案を行うための一通りの力も、ディレクターに欠かせないスキルのひとつです。また、提案を行う際、お客様の目線に立つことも大事ですが、「このサービスを使うユーザはどう思うか?」「自社(開発会社)として最適なソリューションは何か?」といった多角的な視点があると、関わる人皆が幸せになれる提案ができると思います。

まとめ


WEB業界におけるディレクターの役割は幅広く現場によって異なるものの、そのどれにも共通したスキルが必要であることをお話しいたしました。

なお、SBWorksではディレクターを大募集しておりますので、我こそは!という方はぜひ、blogコメントや弊社公式HPの問い合わせフォームからコンタクトいただければと思います。

お読みいただきありがとうございました。

CS-Cart – HTTP 500 エラー対策

目次

1. 本記事について
2. 環境情報
3. 問題発生時のリソース状況
4. 原因
5. 対処内容
6. 対処後のリソース状況

1. 本記事について


CS-Cart のレスポンス(サイト応答)が極端に劣化し、HTTP 500 エラーが発生するケースがあったため、確認すべき箇所と、その対処方法を記事にします。

本記事は CS-Cart 導入済みの方向けのため、
CS-Cart についての説明は割愛致します。
詳しくは公式サイトをご確認ください。

公式サイト
開発者ドキュメント

2. 環境情報


– OS/ソフトウェア
EC2(Amazon Linux 2, SSD, 64 ビット, t3.large)
RDS(MySQL 8.0.17, マルチAZ, db.r3.large)

CS-Cart マーケットプレイス版(4.9.2_JP_1)
PHP(7.2.30)
PHP-fpm(7.2.30)
Apache(2.4.39)

– AWS環境(ロードバランサ + EC2 + RDS のシンプル構成です)

3. 問題発生時のリソース状況


以下は CloudWatch ダッシュボードで表示したメトリクスです。
※ 表示されている値は 5 分間隔の平均値です。

問題が発生した 19:00 前後の状況を確認すると、

①EC2
 1. CPUUtilization(CPU 使用率)は特に問題なし
 2. CpuWait(CPU 待ち時間)が上がっている

②ApplicationELB(ロードバランサ)
 1. TargetResponseTime(ユーザへリクエストが返却されるまでの時間)が上がっている
 2. NewConnectionCount(新規接続数)は遅延が発生した時間帯以外と比較する限り問題なし

③RDS
 1. DatabaseConnections(DBコネクション数)が上がっている
 2. CPUUtilization(CPU 使用率)は特に問題なし
 3. ReadLatency(SelectSQLの待ち時間)が上がっている

上記のため、

・WEB サーバが忙しい訳ではない。
・DB コネクション数が上がっている時間帯に処理待ちが多く発生し、レスポンスタイムが遅くなっている。

という状況でした。


4. 原因


RDS の Performance Insights を確認したところ、問題が発生した時間帯で「Table_locks_waited(テーブルロックによる処理待ち)」が多数発生していました。

「Table_locks_waited MySQL」で調べると、テーブルのストレージエンジンが「MyISAM(マイアイサム)」の場合は テーブルロック方式という記述が、、

CS-Cart テーブルのストレージエンジンを確認したら「lock_keys」テーブルを除き、全て「MyISAM(マイアイサム)」でした。

※ 詳しい内容は後述致します。

テーブルのストレージエンジンを確認するためのSQLは以下です。

select TABLE_NAME, ENGINE from information_schema.TABLES where TABLE_SCHEMA like '%cscart%';

テーブルロックの原因となっているストレージエンジンの変更を検討したいですが、CS-Cart が MyISAM 以外に対応していない可能性があるため、「CS-Cart MyISAM」で調べたところ、
CS-Cart は InnoDB(イノディービー) にも対応しているので問題ない。という情報と、
CS-Cart 開発者が高速化の方法について説明している記事の中で InnoDB への変換を推奨している情報が見つかりました。

MySQLストレージエンジン
How to speed up CS-Cart. Server details

これで裏付けが取れましたので、以降の対処を行いました。


5. 対処内容


それぞれ以下の作業を行いました。

①RDS
 CS-Cart テーブルのストレージエンジンを「MyISAM(マイアイサム)」から「InnoDB(イノディービー)」へ変更

②EC2
 OPcache と APCu を導入
 ※ こちらはテーブルロックへの対処ではないですが、高速化の観点で CS-Cart 開発者から推奨されているため、実施しました。

①RDS

CS-Cart テーブルのストレージエンジンを「MyISAM(マイアイサム)」から「InnoDB(イノディービー)」へ変更

MyISAM はテーブルロックの方式なので、参照クエリや更新クエリが多くなるにつれ、処理待ちが発生します。

共有ロック
(READロック)
他のプロセスは読み込みはできるが、書き込みができなくなる
排他ロック
(WRITEロック)
他のプロセスは読み込みも書き込みもできない

本件の問題が発生した際はテーブルロックが多く発生しておりましたので、
処理待ちが発生した結果、ユーザへ HTTP 500 エラーが返却されてしまいました。

この問題を解消するため、テーブルエンジンを InnoDB へ変更します。

InnoDB を採用した理由は行ロック方式であることと、
CS-Cart は InnoDB でも適切に動作する旨、公式サイトに明記されているためです。
MySQL のテーブルエンジンは MyISAM や InnoDB の他にもありますが、特殊なユースケースで使用するタイプのため検討しておりません。
気になる方は公式サイトの「第16章代替ストレージエンジン」リンクを辿ってください。

公式サイト
第16章代替ストレージエンジン


MyISAM から InnoDB への変更は、

ALTER TABLE cscart_addon_descriptions ENGINE=InnoDB;

このように「ALTER TABLE」SQL で行います。
ひとつひとつ SQL を書くのが大変なので、以下のように一括で作成します。

select
concat('ALTER TABLE ', TABLE_NAME, ' ENGINE=InnoDB;') as 'sql'
from
information_schema.tables
where
table_schema like '%cscart%'
and
engine = 'MyISAM';

CS-Cart の全テーブルを MyISAM から InnoDB へ変更したら RDS への対処は完了です。

②EC2

OPcache と APCu を導入

PHP はインタプリタ言語のため、呼び出される度にプログラムの解析/解釈が行われ、オーバーヘッドがあります。
今回はクライアントへのレスポンス速度を改善する必要があるため、PHP の速度改善に寄与する「OPcache」と「APCu」をインストールします。

— 解説
OPcache を導入すると、コンピュータが解析済みの「バイトコード」をキャッシュして再利用してくれるため、処理が高速になります。
APCu は PHP が動的に作成したデータをキャッシュしてくれるので、ページの表示速度が改善されます。

本環境の PHP は「remi」リポジトリでインストールされているため、
インストールコマンドは以下になります。

$ sudo yum install --enablerepo=remi,remi-php72 php-opcache php-pecl-apcu

※ OPcache が php-opcache です。
※ APCu が php-pecl-apcu です。

インストールが終わりましたら、Apache を再起動しましょう。

$ sudo systemctl restart httpd

以下の表示となれば完了です。

OPcache(with Zend OPcache … の記述が追加されます)

$ php -v

PHP 7.2.30 (cli) (built: May 5 2020 18:04:45) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.2.30, Copyright (c) 1999-2018, by Zend Technologies

APCu(apcu が表示されます)

$ php -i | grep apc
/etc/php.d/40-apcu.ini,
apcu
MMAP File Mask => /tmp/apc.XXXXXX
apc.coredump_unmap => Off => Off
apc.enable_cli => Off => Off
apc.enabled => On => On
apc.entries_hint => 4096 => 4096
apc.gc_ttl => 3600 => 3600
apc.mmap_file_mask => /tmp/apc.XXXXXX => /tmp/apc.XXXXXX
apc.preload_path => no value => no value
apc.serializer => php => php
apc.shm_segments => 1 => 1
apc.shm_size => 32M => 32M
apc.slam_defense => On => On
apc.smart => 0 => 0
apc.ttl => 0 => 0
apc.use_request_time => On => On
apc.writable => /tmp => /tmp

本来、性能問題を解決するにあたっては、1つ1つ対応を試し、原因箇所の特定をし、対策をし、効果を確認するのが鉄則ですが、本記事では「それぞれどの程度効果があったのか」についての測定結果がありません。
急を要したため、テーブルのストレージエンジン変更とPHPのキャッシュ化を同時に行い解決を優先しました。


6. 対処後のリソース状況


※ 表示されている値は 5 分間隔の平均値です。

対応前後の状況を比較すると、

– ApplicationELB(ロードバランサ)
 1. TargetResponseTime(ユーザへリクエストが返却されるまでの時間)
   6.87 → 0.8
 2. NewConnectionCount(新規接続数)
   6.87 → 395

レスポンスタイムが改善されていることが確認できます。



本記事は以上です。
問題が発生した際に CS-Cart についての情報をインターネットで検索したのですが、なかなか欲しい情報が見つからず苦労したため、掲載致しました。
解決への一助となりましたら幸いです。

Dockerを使ったMagentoの開発環境構築方法

1. 概要

今回はECサイト構築プロダクトの代表例であるMagentoプラットフォームの開発環境をローカルでDockerを使って構築する方法を紹介します。Magentoとは利用者にユーザ管理、カートシステム、及び決済方法をはじめ、高度な機能と柔軟な変更性を提供したECサイトのプラットフォームです。本記事では、Magentoをダウンロードし、インストールする方法をまとめています。環境構築をポータブルで、扱いやすくするためにdocker-composeを使っています。

本記事内容

  • Magentoとは
  • 本記事を読む前提知識
  • 構築環境準備
  • 環境のデザイン・設計
  • 実装
  • 構築手順
  • 考察・まとめ

2. 本記事を読む前提知識

本記事は以下のような読者の方に向けて書きました。

  • ECサイトの構築、開発等に興味があり、新しいプラットフォームを探している人
  • 新しいフレームワーク、プラットフォーム勉強が好きな人
  • Magentoの開発、自作モジュールを勉強したい人

事前知識

以下の情報・知識について理解があることを前提としています

  • linuxの基本的な知識(ターミナル上コマンドで基本的な操作できる)
  • docker、 docker-composeコンテナー技術
    • dockerは、仮想化・インフラ分野で注目されている技術
    • docker-composeは複数のdockerコンテナーを一斉に管理できるようなツールです。複数のdockerコンテナーから構成される複雑システムをdockerのコマンドだけで操作・管理するのは膨大な作業になってしまう可能性があります。そこで、docker-composeがよく利用されている。docker-composeを使ったら1つのファイルに必要なコンテナーを記述して、docker-compose upという1つのコマンドを実行させるだけで環境が構築されます。
  • nginx、php、composer
    • nginxはサーバ上でウェブを立てるサービス
    • composerはphp依存関係ライブラリを管理するシステム
  • redis
    • インメモリーデータベースシステムで、アクセス性能がいいのでキャッシュ用に使用されていることが多いです。
  • elasticsearch
    • 大量のデータ中で早く検索できるような技術
  • microservice (マイクロサービス)
    • dockerといったコンテナー技術の流行により、大きなシステムを構築する際に、複数の小さいサービスの組み合わせで構築する考え方が注目されています。これをマイクロサービスとよんでいます。マイクロサービスの利点として、開発・変更にはシステム全体ではなくて一部のサービスだけが対象となり、アジャイル的な開発に向いています。

3. Magentoとは

MagentoはPHPで開発され、Symfony、Laminas (バージョン2.3.4まではZend)等を利用した大規模ECサイト向けのプラットフォームです。本プラットフォームはオープンソースプロジェクトで、2018年にAdobeに買収されました。ライセンスとして無料版(Community version)と有料版(Enterprise version)があり、有料版の方にセキュリティー、性能性、利用性の観点からモジュールが追加されています。BuiltWith®サイトの調査によるとMagentoは世界中で利用されているプラットフォームのうち3番目になっています。

Magentoの主なメリットとデメリットは以下の通りです。
【利点】
・大規模性
・ECサイトに必須な機能の全てが揃っています
・要件によって柔軟に変更できます
・オープンソースなので、Communityバージョンを無料でも利用可能
【弱点】
・小規模なデータの扱いには向いていません
・非常に複雑なプラットフォームなので習得に時間がかかります

4. 構築環境準備

  • dockerとdocker-composeのインストール
    • このサイトのガイドに沿ってdockerとdocker-composeのインストール
  • Magentoマーケットプレイスでアカウント作成
  • 作成したアカウントのアクセスキーを作成
    • ログインした後に、このサイトにアクセスし、アクセスキーを作成

5. 環境のデザイン・設計

MagentoのECサイトを構築する際、以下のサービスを立てる必要があります:

  • webサーバ構築に必要なサービス
    • nginxサービス
    • phpサービス
    • データベースサービス
  • Magentoサイトに利用するサービス
    • redis:キャッシュサービス、セッション用とキャッシュ用で2つ必要
    • elasticsearch:検索エンジン

したがって、今回はnginx、php、データベース、redis-session、redis-cache、elasticsearchといった6つのサービスをdockerで立てます。以下は環境のデザイン図になります。

以下にはブラウザからデータがどうやって流れて処理されていく手順を説明します:

  • ユーザはブラウザからアクセスするときに、リクエストはnginxに80ポートで送信されます。
  • nginxは、phpでの処理が必要なら(動的なコンテンツ)、phpに9000ポートで依頼を送信します。
  • Magentoのほとんどの処理がphpコンテナー上で行われるが、処理中にredis-session、redis-cache、elasticsearch、データベースに接続します。
  • 処理されたレスポンスがユーザのブラウザへ返却されます。

magentoのソースコードとデータベースのファイルをコンテナーと開発パソコンの間に共有した理由を以下に説明します:

  • 開発中に編集したファイルをすぐにMagentoのdocker環境に反映させ、テストするのは開発の流れである。したがって、magentoソースコードをnginx、phpコンテナーと共有しました。
  • データベースのボリュームを開発パソコンと共有することによって、この開発環境にデータベースも含めて一緒に他の開発者のパソコンに写したい場合、ファイルとしてコピーするだけで済みます。

6. 環境構築のdocker-composeファイル

本記事で使ったMagento開発環境構築のソースコードはこのgithubからアクセスできる。v0.1タグはちょうど本記事で利用したソースコードとなっています。上に書いてあるように、複数のdockerコンテナーを管理するためにdocker-composeを利用しました。以下に、docker-compose.ymlファイルを表示しています。

version: '3'
services:
  # magentoをダウンロードするため
  composer-install:
    build: ./composer
    volumes:
      - ./html:/html:delegated
      - ./config/composer/auth.json:/root/.composer/auth.json
  # ウェブサーバ
  nginx:
    image: nginx:1.12
    ports: 
      - 80:80
    # magentoコードとnginx設定ファイルの共有
    volumes:
      - ./html:/var/www/html:delegated
      - ./config/nginx/magento.conf:/etc/nginx/conf.d/default.conf:cached
    links:
      - php
    depends_on:
      - php
  # phpサーバ
  php:
    build: ./php
    volumes:
      - ./html:/var/www/html:delegated
    links:
      - db
      - redis-cache
      - redis-sessions
      - elasticsearch
    depends_on:
      - db
  db:
    image: mysql:5.7.31
    volumes:
      - ./db/data:/var/lib/mysql:delegated
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: magento
  elasticsearch:
    image: elasticsearch:7.9.0
    environment:
      - discovery.type=single-node
  redis-cache:
    image: redis:alpine
  redis-sessions:
    image: redis:alpine

nginxサービス

ベースはnginx:1.12イメージで、開発パソコンから80ポートで接続できるようにしています。magentoソースコード(html)をコンテナー上では/var/www/htmlにマウントしています。さらに、このコンテナーはphpコンテナーに接続します(links: -php)。

phpサービス

ベースはphp/Dockerfileになっています。自作のDockerfileでコンテナーを作成している理由は、magentoを動かすために複数のphpパッケージが必要となります。したがって、Dockerfileでphp:7.2-fpmコンテナー上にmagento動作に必要なパッケージをインストールしています。このコンテナーにもmagentoソースコードをマウントしています。さらに、phpコンテナーはdb、redis-cache、redis-sessions、elasticsearchコンテナーに接続します。

elasticsearch、redis-cache、redis-sessionsサービス

基本的にはデフォルトの設定で使うので、定義がベースとなるイメージを設定するだけで良いです。

7. 構築手順

本記事の作業は以下のようなラップトップを開発パソコンとして利用しました。

パソコンモデルMacBook Pro
プロセッサー2.3 GHz Dual-Core Intel Core i5
メモリー16 GB
オペレーティングシステムMacOS Catalina Version 10.15.7
Dockerアプリケーションdocker desktop v2.3.0.5

Magentoソースコードのダウンロード

まずは、このgithubから本記事で利用したソースコードをクローン・ダウンロードします。以下にはソースコードを置いた場所を「ルートフォルダ」とします。

以下には色々なdocker、docker-compose、シェルスクリプトのコマンドをターミナル上で動かす必要があるが、それらをターミナル上でcdコマンドでgithubからのソースコードをおいた「ルートフォルダ」の中で実行しています。したがって、読者はcdコマンドを使って予め、ターミナル上で現在フォルダを「ルートフォルダ」に変更しておけば良いです。

$ cd ルートフォルダ ← 現在フォルダをルートフォルダに変更します

1.magentoのソースコードをcomposerを使って「ルートフォルダ/html」の下にダウンロードします。composerを実行させるためのコンテナーをdocker-composeの中に準備してあります。composerの実行には認証が必要なので、「ルートフォルダ/config/composer/auth.json」ファイルの中身に4.で作成したアクセスキーをコピーする必要があります。

{
    "http-basic": {
        "repo.magento.com": {
            "username": "ここにパブリックキーをコピー",
            "password": "ここにプライベートキーをコピー"
        }
    }
}

2.以下のコマンドをターミナル上で「ルートフォルダ」の下に実行させてmagento 2.3.5バージョンをcomposerを使ってダウンロードできます。このスクリプトの中にはcomposer-installというdockerコンテナーをたてその上にcomposerコマンドでmagentoソースコードをダウンロードするコードが入っています。
※magentoは大きいプラットフォームなので、コマンド実行には数分ぐらいかかります

$ sh magento_download.sh

Magentoのインストール

ダウンロードした後、ターミナル上で「ルートフォルダ」の下にdocker-compose upコマンドで環境を立てます。

$ docker-compose up

その後、magentoをインストールする必要があります。そのためには、phpコンテナー上でmagentoインストールコマンドを実行させる必要があるが、そのためにmagento_install.shスクリプトを準備しています。以下のようにコマンドをターミナル上で「ルートフォルダ」の下に実行させれば、magentoのインストールができます。

$ sh magento_install.sh

シェルの中身は以下のようになっています。そこで、アドミン画面にログインする情報を自分で設定場合は、ADMIN_EMAILとADMIN_PASSWORDの値を変更すれば良いです。

#!/bin/sh

# 各サービスの名前の指定
# もし,.envファイルの中身を変更したら
# こちらも変更する必要がある
PHP=magento_php_1
# DATABASE=magento_db_1

# admin画面にログインするための情報
ADMIN_USER_NAME='admin'
ADMIN_PASSWORD='Password1234'
ADMIN_EMAIL='huchka@sbworks.jp'

docker exec -it ${PHP} php -d memory_limit=-1 /var/www/html/bin/magento setup:install \
  --db-host db --db-name magento --db-user root --db-password root --timezone 'Asia/Tokyo' --currency JPY --use-rewrites 1 --cleanup-database \
  --backend-frontname admin --admin-firstname AdminFirstName --admin-lastname AdminLastName --admin-email ${ADMIN_EMAIL} \
  --admin-user ${ADMIN_USER_NAME} --admin-password ${ADMIN_PASSWORD} --base-url 'http://127.0.0.1/' --language en_US \
  --session-save=redis --session-save-redis-host=redis-sessions --session-save-redis-port=6379 --session-save-redis-db=0 --session-save-redis-password='' \
  --cache-backend=redis --cache-backend-redis-server=redis-cache --cache-backend-redis-port=6379 --cache-backend-redis-db=0 \
  --page-cache=redis --page-cache-redis-server=redis-cache --page-cache-redis-port=6379 --page-cache-redis-db=1 \

# elasticsearchの設定
docker exec -it ${PHP} php /var/www/html/bin/magento config:set catalog/search/engine 'elasticsearch7'
docker exec -it ${PHP} php /var/www/html/bin/magento config:set catalog/search/elasticsearch7_server_hostname 'elasticsearch'

テスト、動作確認

以上で、magentoのダウンロードとインストールが完了したので、ブラウザで127.0.0.1にアクセスしたら以下のような画面が表示されるはずです。

さらに、admin画面に入りたい場合127.0.0.1/adminにアクセスし、インストール時に設定したユーザ名(ADMIN_USER_NAME)とパスワード(ADMIN_PASSWORD)でログインできます。

Magentoはindexer管理、キャッシュ管理、デプロイモード管理等の設定を楽にするためにcliコマンドを標準で提供しています。しかし、標準コマンドをphpコンテナーに入って(docker exec -itコマンドで)実行させる必要があり、一々コンテナーに入るのは無駄な作業になります。したがって、magento_command.shというシェルを本記事の作業で用意しました。例えば、キャッシュの状況を表示するmagentoのコマンドは”php bin/magento cache:status”です。これを実行させるためには以下のコマンドを打つだけで済みます。以下の結果から、全ての種類のデータをキャッシュするように設定されていることが分かります。

$ sh magento_command.sh cache:status
Current status:
                        config: 1
                        layout: 1
                    block_html: 1
                   collections: 1
                    reflection: 1
                        db_ddl: 1
               compiled_config: 1
                           eav: 1
         customer_notification: 1
            config_integration: 1
        config_integration_api: 1
                google_product: 1
                     full_page: 1
             config_webservice: 1
                     translate: 1
                        vertex: 1

8. 考察・まとめ

Magentoはサイズ的に膨大なphpスクリプトから構成されている大きなプラットフォームです。したがって、開発中や本番ライブでの性能が問題となってくることが多いです。性能を上げるためには、redis、varnish、elasticsearch等のキャッシュサービスと検索エンジンを利用することを強くお勧めします。さらに、CPU周波数が高くて、SSDを持っているサーバ、パソコンを利用した方が良いです。例えば、AWS上で本番のサーバを構築しているならC5系のインスタンスが適しています。magentoの処理は多数のphpファイルを渡って実行されるように設計されており、例えば一つのレスポンスに対し、100以上のphpスクリプトが実行されているということもあるからです。

ラップトップ上のdockerでMagentoの開発環境を何も考えずに構築すると、性能がとても低くなり、開発・作業できないぐらいなものになるケースもあります。そのときに最初に確認するべきは、dockerのリソースの設定になります。本記事で使った設定は以下の写真のように、CPUとMemoryはラップトップの半分ずつにしています。
次に、dockerコンテナーへのマウント(共有)する方法について確認します。つまり、htmlフォルダをどんなモードでマウントしているかということです。基本的には、volumeのマウントをdelegatedにした方が良いです。この設定をしたら、dockerコンテナーの方がファイル書き込みがホストのファイルに反映されるのに遅延を入れて性能をあげています。

本記事ではMagentoプラットフォームの開発環境を構築する方法をまとめました。環境構築にはマイクロサービスとして流行っているdocker (docker-compose)を利用しました。

tensorflowを使って気温を予測してみた

こんにちは。最近気象データを用いて、発電所の発電量を予測できるのかということを調査していました。調査中に得た知見(必要なデータの取得や環境構築、予測手順)についてまとめました。

目次

  • 概要
  • はじめに
  • キーワード
  • 構成内容
  • 今回使用するデータ
  • データ取得先
  • そもそもgrib2ファイルとは
  • 動作環境(grib2ファイルから気温を取得するための環境)
  • grib2ファイルから気温を取得する
  • 動作環境(tensorflowを実行するための環境)
  • tensorflowを用いてlstmネットワークを構築し、予測する
  • 考察、まとめ

概要

気象庁が提供している気象データを用いて、将来の気温をtensorflowを用いて予測しました。使用したデータと予測結果は下記の通りになりました。
・使用したデータについて
 1時間毎の気温データを、教師データ用に1年間、評価データ用に1ヶ月
・予測結果
 教師データから予測モデルを作成し、未来1ヶ月を予測した結果、下記のような高い精度で予測できました。
  全てのデータは3%以内の誤差(※1絶対温度換算)
  90%のデータは3%以内の誤差、9%のデータは、10%以内の誤差、1%のデータは、20%以内の誤差(※1摂氏換算)

 結果をグラフ化したものが下記になります。
※1 絶対温度とは、原子・分子の熱運動がほとんどなくなる温度を0K(単位:ケルビン)とする温度のことで、絶対温度T(K)と摂氏温度t(℃ )は、T=t+273(K)という関係が成り立つ。

未来1ヶ月の実データと予測データ

 このブログでは予測するまでに必要なデータの取得や環境構築、予測手順についてまとめました。

はじめに

1.tensorflowとは
 tensorflowとはGoogleが開発している、機械学習に用いられるソフトウェアライブラリです。詳しい使い方やチュートリアルはこちらを参考にしてください。

2.予測方法について
 tensorflow内で、long short term memory(lstm)というリカレントニューラルネットワーク(RNN)のモデルを使って予測を行なっています。RNNとは、脳内にある神経細胞のつながりを数式的なモデルで表現したものです。lstmはRNNのモデルの中の一つで、長期間の情報を学習することが可能になっています。
予測を行う上で、モデルの詳細を理解しなくても実装することは可能ですが、概要程度は理解している方が、記事の後半に書いてあるソースコードを理解しやすくなるので、時間のある人は下記記事を参考にしてください。
ニューラルネットワークの基礎知識 – 1
13. ニューラルネットワークの基礎
リカレントニューラルネットワーク_RNN (Vol.17)
わかるLSTM ~ 最近の動向と共に
RNNとLSTMを理解する

キーワード

  • python
  • grib2
  • tensorflow
  • long short time memory(lstm)
  • docker

構成内容

本記事では、下記環境を前提に検証を行っています。
OS:macOS Sierra 10.12.6
Docker Desktop:19.03.02
Python:3.5.2(docker内のpythonのバージョン)

今回使用するデータ

 気象庁が気象データをgrib2というファイル形式で提供しています。今回は下記のファイルを使用します。

データ取得先

今回は検証のため、こちらからデータを取得してきます。企業活動で頻繁にデータを利用する方は、※気象業務支援センターからデータを直接購入してください。

※気象業務支援センターでは教育研究機関向けにデータを提供しています.企業活動等のためにデータを頻繁に必要とされる方は,気象業務支援センターからデータを直接購入し,データ提供スキーム全体の維持発展にご協力ください.
http://database.rish.kyoto-u.ac.jp/arch/jmadata/

そもそもgrib2ファイルとは

 grib2とは、世界気象機関(WMO)が定める形式で書かれたファイルで、気象データを扱うために世界中で使用されています。grib2ファイルには、地表面を格子状に分割した各地点の気象データが含まれています。
 grib2について詳しく知りたい方は、こちらを参照してください。

動作環境(grib2ファイルから気温を取得するための環境)

 grib2ファイルを読み込むためにpygribを使います。環境はdockerで準備しました。
(dockerはこちらからインストールしました。Download for Macボタンを押して、dmgファイルをダウンロードして、インストールしてください。設定は全てデフォルトでOKです。)

FROM ubuntu:latest

ENV DEBIAN_FRONTEND=noninteractive

# パッケージのインストールとアップデート
RUN apt-get update && apt-get -y upgrade
RUN apt-get -y install build-essential
RUN apt-get -y install git vim curl wget
RUN apt-get -y install zlib1g-dev \
libssl-dev \
libreadline-dev \
libyaml-dev \
libxml2-dev \
libxslt-dev \
libncurses5-dev \
libncursesw5-dev 

# pyenv のインストール
RUN git clone git://github.com/yyuu/pyenv.git /root/.pyenv
RUN git clone https://github.com/yyuu/pyenv-pip-rehash.git /root/.pyenv/plugins/pyenv-pip-rehash
ENV PYENV_ROOT /root/.pyenv
ENV PATH $PYENV_ROOT/bin:$PATH
RUN echo 'eval "$(pyenv init -)"' >> .bashrc

# anaconda のインストール
ENV ANACONDA_VER 4.1.1
ENV LD_LIBRARY_PATH=/lib/x86_64-linux-gnu:$PYENV_ROOT/versions/anaconda3-$ANACONDA_VER/lib
RUN pyenv install anaconda3-$ANACONDA_VER
RUN pyenv global anaconda3-$ANACONDA_VER
ENV PATH $PYENV_ROOT/versions/anaconda3-$ANACONDA_VER/bin:$PATH

# ライブラリのアップデート
RUN conda update -y conda
RUN pip install --upgrade pip
RUN conda install -c conda-forge pygrib=2.0.2

RUN conda install -c conda-forge/label/gcc7 jpeg

RUN pip install numpy --upgrade
RUN pip install pandas --upgrade

RUN mkdir /temp

上記のDockerfileをbuildします

docker build -t pygrib_ubuntu .

データの取得

データ取得するためのソースコードは下記です。

# -*- coding: utf-8 -*-
from datetime import timedelta
import pygrib
import pandas as pd
import sys
import numpy as np
import os

# 取得したい場所の緯度経度
latitude = 34.123456
longitude = 134.123456

# ファイル内の時刻はUTCなので、JSTに変換
time_diff = timedelta(hours=9)

def _read(srcfn):
    src = pygrib.open(srcfn)
    return src

def _close(src):
    src.close()

def create_csv(parameter_name, file_name):
    selected_item = gpv_file.select(parameterName = parameter_name )

    df = pd.DataFrame({
        "validDate" : [item.validDate + time_diff for item in selected_item],
        file_name : [
            item.data(
                lat1 = latitude - 0.025,
                lat2 = latitude + 0.025,
                lon1 = longitude - 0.03125,
                lon2 = longitude + 0.03125,
            )[0][0][0] for item in selected_item
        ]
    })

    df.to_csv( file_name + '/' + os.path.basename(sys.argv[1]) + '_' + file_name + '.csv', index=False, columns=["validDate", file_name])

# ファイル読み込み    
gpv_file = _read(sys.argv[1])

# CSVファイル作成
create_csv("Temperature","temperature")

 下記コマンドを実行して、任意の時間帯の気温を取得します。
(Z__C_RJTD_20190701000000_MSM_GPV_Rjp_Lsurf_FH00-15_grib2.binはgrib2ファイルのファイル名)

# ローカル上
docker run -it -v /path/to/somepath:/temp pygrib_ubuntu
# docker上
python /temp/get_temperature.py Z__C_RJTD_20190701000000_MSM_GPV_Rjp_Lsurf_FH00-15_grib2.bin

 上記の実行例では、2019/7/1 9:00~2019/7/2 1:00までのデータ、つまり、16時間分のデータがcsvファイルで取得できます。データの単位はケルビン(K)であり、℃ではない点に注意してください。これを学習に使用したい分だけ、取得します。今回の予測には学習用に1年分を、評価用に1ヶ月分を使用しています。

動作環境(tensorflowを実行するための環境)

次に、tensorflowを使用するための環境をdocker上に構築します。

FROM tensorflow/tensorflow:latest-py3

RUN apt-get update && apt-get -y upgrade
RUN pip install pandas --upgrade
RUN pip install matplotlib --upgrade
RUN pip install keras --upgrade
RUN pip install tensorflow --upgrade
RUN pip install sklearn --upgrade
RUN mkdir /temp

上記のDockderfileをbuildします

docker build -t tensor_flow_ubuntu .

予測するためのソースコードは下記です。使用できるモデルはいくつかあるのですが、今回はlong short time memory(lstm)というモデルで予測を行いました。lstmは、時系列データで学習できるようにするネットワークで、今回の予測に適しているので採用しました。

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.pyplot as plt
import math
from keras.models import Sequential
from keras.layers import Dense, LSTM
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error

plt.switch_backend('agg')
dataframe = pd.read_csv('learn_temperature.csv', usecols=[1], engine='python')
dataset = dataframe.values
dataset = dataset.astype('float32')
plt.plot(dataset)
 
# 乱数を固定する
np.random.seed(7)
 
# データを正規化する
scaler = MinMaxScaler(feature_range=(0, 1))
dataset = scaler.fit_transform(dataset)

# 教師データと予測データに分割する
train_size = 8775
# train_size = int(len(dataset) * 0.7)
test_size = len(dataset) - train_size
train, test = dataset[0:train_size,:], dataset[train_size:len(dataset),:]

# 配列データを行列データに変換する
def create_dataset(dataset, maxlen):
    #dataX, dataY = [], []
    dataX = []
    dataY = []
    for i in range(len(dataset)-maxlen-1):
        # 時刻t、気温x(t)とすると、x(t),x(t+1)...x(t-maxlen)を入力とする
        a = dataset[i:(i+maxlen), 0]
        dataX.append(a)
        # x(t+maxlen)を出力とする
        dataY.append(dataset[i + maxlen, 0])
    return np.array(dataX), np.array(dataY)
 
# 行列データを作成
maxlen = 24
trainX, trainY = create_dataset(train, maxlen)
testX, testY = create_dataset(test, maxlen)
 
# 行列を[samples, time steps, features]という形に変換する
trainX = np.reshape(trainX, (trainX.shape[0], 1, trainX.shape[1]))
testX = np.reshape(testX, (testX.shape[0], 1, testX.shape[1]))

# LSTMネットワークを構築する
model = Sequential()
model.add(LSTM(4, input_shape=(1, maxlen)))
model.add(Dense(1))
model.compile(loss='mean_squared_error', optimizer='adam')
model.fit(trainX, trainY, epochs=10, batch_size=1, verbose=2)

# 予測する
trainPredict = model.predict(trainX)
testPredict = model.predict(testX)

# 正規化したデータを基に戻す
trainPredict = scaler.inverse_transform(trainPredict)
trainY = scaler.inverse_transform([trainY])
testPredict = scaler.inverse_transform(testPredict)
testY = scaler.inverse_transform([testY])

# グラフ化するために、教師データをずらす
trainPredictPlot = np.empty_like(dataset)
trainPredictPlot[:, :] = np.nan
trainPredictPlot[maxlen:len(trainPredict)+maxlen, :] = trainPredict

# グラフ化するために、予測データをずらす
testPredictPlot = np.empty_like(dataset)
testPredictPlot[:, :] = np.nan
print("len(trainPredict),len(dataset)")
print(len(trainPredict),len(dataset))
testPredictPlot[len(trainPredict)+(maxlen*2)+1:len(dataset)-1, :] = testPredict

# データセットと予測値をプロットする
plt.plot(scaler.inverse_transform(dataset), color ="g", label = "row")
plt.plot(trainPredictPlot,color="b", label="trainpredict")
plt.plot(testPredictPlot,color="m", label="testpredict")
 
# グラフを保存
plt.legend()
plt.savefig('result.png')

np.savetxt('test_predict_plot.csv',testPredictPlot,delimiter=',')

下記コマンドを実行して、予測を行います。

# ローカル
docker run -it -v /path/to/somepath:/temp tensor_flow_ubuntu
# docker上
python /temp/predict_temperature.py

 学習に使用したデータ(learn_temperature.csv)と、出力結果(test_predict_plot.csv)は下記です。test_predict_plot.csvは、学習期間の値(1~8789行目)はnanで、8800行目からが予測値になっています。

実際の値と予測値をグラフ化したものが下記です。

未来1ヶ月の実データと予測データ

予測精度については下記の通りとなりました。
 1.全てのデータは3%以内の誤差(ケルビン換算)
 2.90%のデータは3%以内の誤差、9%のデータは、10%以内の誤差、1%のデータは、20%以内の誤差(摂氏換算)

考察、まとめ

 今回はtensorflowを使って気温の予測を行いました。一般的にtensorflowのメリット、デメリットは下記と言われています。
メリット
・利用者が多いので、調べた時に情報を得やすい
デメリット
・計算グラフ構築後の変更が不可能
このくらいの予測では計算グラフを変更することはしなかったので、特に大きなデメリットは感じませんでした。tensorflowを使ってよかったと思います。
 次に、精度についてですが、学習期間のデータを増やす、学習回数を増やす等で精度を上げることは可能です。ただし、昨今温暖化が進んでいる影響で、学習期間のデータを増やしすぎると精度が逆に下がる等の傾向が見られるかもしれないので、調査してみるのも良いかもしれません。
 さらに今後は、このモデルを使って、別の気象データについても予測がうまくいくのか検証していきたいと思います。

SpringBoot、Thymeleafでのページング機能の実装(Java初心者向け)

はじめに

 現在のプロジェクトでは、プログラムはJava(Spring-Boot)、フロントはthymeleaf、Bootstrapを使って開発をしています。
 開発する中で、時間を要したのがページネーション処理です。同環境での実装例も見当たらず、数日かかっての実装となりました。
 そこで、Spring-Boot、Tymeleaf環境でのページネーションの実装方法をご紹介します。同環境でページネーション処理につまずいている方に読んでいただきたいです。

環境

実装ページング機能概要

  • テーブルでData内容を表示する(3件づつ)
  • ページ数の表示、そのページへの遷移が可能
  • 現在のページの所在が分かる。
  • 「次へ」「前へ」を表出させて遷移できる。
  • 「最初」「最後」を表出させて遷移できる。
  • 表示するページ数を制限する(3ページ)

ディレクトリ構成

src/
├ java/
│ ├ sampleController.java
│ ├ sampleForm.java
│ ├ sampleDao.java
│ └ member.java
└ resourse/
└ html/
└ sample.html

1-1 Controller

package jp.sbworks.standard.web.controller;

import java.util.HashMap;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import jp.sbworks.standard.web.dao.SampleDao;
import jp.sbworks.standard.web.db.single.pojo.SSample;

@Controller
public class SampleController {

    /** 1ページの表示数 */
    private final String limit = "3";

    /** ページネーションで表示するページ数 */
    private int showPageSize = 3;

    @Autowired
    private SampleDao sampleDao;


    @RequestMapping(value = "/sample",method = RequestMethod.GET)
    @Transactional(readOnly = true)
    public String index(Model model, @RequestParam HashMap<String, String> params) throws Exception {
        // パラメータを設定し、現在のページを取得する    
        String currentPage = params.get("page");

        // 初期表示ではパラメータを取得できないので、1ページに設定
        if (currentPage == null){
            currentPage = "1";
        }
        // データ取得時の取得件数、取得情報の指定
        HashMap<String, String> search = new HashMap<String, String>();
        search.put("limit", limit);
        search.put("page", currentPage);
    
        int total = 0;
        List<SSample> list = null;
        try {
            // データ総数を取得
            total = sampleDao.getMemberListCount();
            // データ一覧を取得
            list = sampleDao.getMemberList(search);
        } catch (Exception e) {
            return "error/fatal";
        }

        // pagination処理
        // "総数/1ページの表示数"から総ページ数を割り出す
        int totalPage = (total + Integer.valueOf(limit) -1) / Integer.valueOf(limit);
        int page = Integer.valueOf(currentPage);
        // 表示する最初のページ番号を算出(今回は3ページ表示する設定)
        // (例)1,2,3ページのstartPageは1。4,5,6ページのstartPageは4
        int startPage = page - (page-1)%showPageSize;
        // 表示する最後のページ番号を算出
        int endPage = (startPage+showPageSize-1 > totalPage) ? totalPage :    startPage+showPageSize-1;
        model.addAttribute("list", list);
        model.addAttribute("total", total);
        model.addAttribute("page", page);
        model.addAttribute("totalPage", totalPage);
        model.addAttribute("startPage", startPage);
        model.addAttribute("endPage", endPage);
        return "html/sample/index";
        }
    }

1-2 DAO

package jp.sbworks.standard.web.dao;

import java.io.IOException;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;

import org.sql2o.Connection;
import org.sql2o.Query;
import org.sql2o.Sql2o;

import jp.sbworks.standard.web.db.single.pojo.SSample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;


@Repository
public class SampleDao {

    @Autowired
// データベースアクセス用のライブラリ
    private Sql2o sql2o;
 
    // リスト情報を取得する 
    public List<SSample> getMemberList(HashMap<String, String> search) throws SQLException, IOException {
        // Jdbcに接続する
        Connection con = sql2o.open();
        con.getJdbcConnection().setAutoCommit(false);
        // クエリの作成
        Query query = con.createQuery("select ID as id, NAME as name, DEPARTMENT as department, DAY_OF_JOINING as dayOfJoining from SAMPLE limit :limit offset :offset;");
        int limit = Integer.valueOf(search.get("limit"));
        int page = Integer.valueOf(search.get("page")) - 1;
        // 何件情報を取得するかの指定。
        query = query.addParameter("limit", limit);
        // 何件目からの情報を取得するかの指定(※コントローラからパラメータを使って現在のページ数が分かる。それによって何件目からの情報を取得すればいいのかが分かる。)
        query = query.addParameter("offset", limit * page);

        return query.executeAndFetch(SSample.class);
    }
    
    // リスト情報件数を取得する 
    public int getMemberListCount() throws SQLException, IOException {
        Connection con = sql2o.open();
        con.getJdbcConnection().setAutoCommit(false);
        Query query =  con.createQuery("select count(1) from SAMPLE");
        return query.executeAndFetchFirst(Integer.class);
     }
}

1-3 DB

package jp.sbworks.standard.web.db.single.pojo;

import java.io.Serializable;
import java.time.LocalDate;

public class SSample implements Serializable {

    // ID 
    public Integer id;
    // 名前 
    public String name;
    // 部署 
    public Integer department;
    // 入社日
    public LocalDate dayOfJoining;
    // 登録者 
    public String regName;

}

1-4 html

<!DOCTYPE html>
<html lang="ja">
    <head>
        <script type="text/javascript" th:src="@{/webpack-bundled/Sample/bundled.js}"></script>
    </head>
<div class="table-scroll mb-2">
    <table class="table table-bordered table-sm mt-1">
        <thead>
            <tr>
                <th>社員ID</th>
                <th>名前</th>
                <th>部署</th>
                <th>入社日</th>
                <th>登録者</th>
            </tr>
        </thead>
        <tbody>
            <th:block th:each="item, status : ${list}">
                <tr>
                    <td th:text="${item.id}"></td>
                    <td th:text="${item.name}"></td>
                    <td th:text="${item.department}"></td>
                    <td th:text="${item.dayofjoining}"></td>
                    <td th:text="${item.regname}"></td>
                </tr>
            </th:block>
        </tbody>
    </table>
</div>
<!-- ここからページング処理 -->
<nav>
    <ul class="pagination pg-blue justify-content-center">
        <li th:if="${startPage} > 1" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=1'}" tabindex="-2">最初</a>
        </li>
        <li th:if="${page} > 1" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=' + ${page-1}}" tabindex="-1">前へ</a>
        </li>
        <th:block th:if="${endPage}<=0">
            <li class="page-item " th:classappend="${i == page} ? active" th:each="i : ${startPage}">
                <a class="page-link" th:href="@{'/sample?page=' + ${i}}" th:text="${i}"></a>
            </li>
        </th:block>
        <!-- StartPageからEndPageまでのページ数を表示する -->
        <th:block th:if="${endPage}>0">
            <li class="page-item " th:classappend="${i == page} ? active" th:each="i : ${#numbers.sequence(startPage, endPage)}">
                <a class="page-link" th:href="@{'/sample?page=' + ${i}}" th:text="${i}"></a>
            </li>
        </th:block>
        <li th:if="${page} < ${totalPage}" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=' + ${page+1}}">次へ</a>
        </li>
        <li th:if="${endPage} < ${totalPage}" class="page-item ">
            <a class="page-link" th:href="@{'/sample?page=' + ${totalPage}}">最後</a>
        </li>
    </ul>
</nav>

現在いるページの所在を分かりやすくするために、Bootstrapを使って色付けをしています。下記は現在表示しているページとして青色で表示する場合の処理を簡単に書きました。

<nav>
    <ul class="pagination pg-blue">
…
<li th:classappend="${i == page} ? active" >

結果

下記のように実装できました。

まとめ

ページングの処理は煩雑なりがちですが、基本的な動きはJavaで実装し、Thymeleafで条件分岐、繰り返し処理を活用することで、より可読性が高く、シンプルなプログラムとなります。
 私がつまづいた箇所は、2ページ以降のデータの取得です。コントローラでパラメータを使って現在のpage番号を取得することで、取得を始めるデータを特定でき、ページごとに正しいデータを取得することに成功しました。
 1ページに表示するデータ数や、表示ページ数の指定をプロジェクト用に沿って替えることで、活用いただければ幸いです。

author/A.Nishihara

開発環境構築について ~ Windows 10 Home + Dockerでmysqlを動かす ~

  • はじめに
    ローカル環境で開発作業をする際、以下のような様々な課題を経験することがあります。
    一例として、
    ・開発環境構築手順が多く、煩雑で環境を整えるのに1日費やす
    ・各自のマシンで若干設定が異なっていることによって、環境依存が生じる
    ・DBが共有化されている(共通のサーバーに有る)ので、データを容易に弄れない(データの洗い替えができないなど)
    etc
    があります。
    そこで、今回は各自のローカル環境構築の手間を減らすことを目的とした、
    Windows Homeでの開発環境の整備について記します。
    ※Windows ProとMacはDocker-toolboxを使わず、Dockerをインストールしてください。
    今回の内容で コマンドだけで、ローカル環境にAmazon Linuxを立ち上げ、その環境でアプリを動かすことができました。また、コンテナ上にMySQL(DB)を用意したことで、自分専用のDBが作れました。

構成内容

  • Windows 10 64bit Home Edition
  • Docker-Toolbox (インストーラver 18.03.0-ce)※基本的にlatestで良いと思います。
    Windows HomeではHyper-vが使えないので、Hyper-Vの代わりにOracle Virtual Boxを利用するDocker Toolboxを使います。
  • Java:java-11-amazon-corretto
  • amalizonlinux:2
  • MySQL:8.0.18

Oracle Virtual Box + Docker Toolboxをインストールする

  1. ダウンロード&インストール
    Docker Toolboxをダウンロード
    基本デフォルトで好きな場所にインストールしてください。
    デスクトップにアイコンは作っておいたほうが後々楽になります。
  2. 起動
    デスクトップに作成されたDocker Quickstart Terminalを起動させる。
    ここに起動後の画像を貼る。
    起動するとDockerでおなじみのクジラが出てくる。
                        ##         .
                  ## ## ##        ==
               ## ## ## ## ##    ===
           /"""""""""""""""""\___/ ===
      ~~~ {~~ ~~~~ ~~~ ~~~~ ~~~ ~ /  ===- ~~~
           \______ o           __/
             \    \         __/
              \____\_______/

docker is configured to use the default machine with IP 192.168.99.100
For help getting started, check out the docs at https://docs.docker.com


Start interactive shell

上記のメッセージが出ると思うので、IPをどこかに記録しておきます。(後で確認することも可能。)

合わせてDockerで使う必要なコマンドが最低限インストールされています。
docker ps
docker-compose
…etc

今回の構成

directory
| docker-compose.yml
|─Dokcer
|─java
│ Dockerfile(webアプリを動かすサーバー情報を記載)

└─mysql
│ Dockerfile(DBの情報を記載)

├─conf.d
│ my.cnf(mysqlのconf内容を記載)

└─initdb.d
1__init.sql(初期データ等を投入するためのSQL)
バージョン情報
java:java-11-amazon-corretto
amazonlinux:2
mysql:8.0.18

dockerでamazonlinuxを動かす準備

サーバーの構成を記載した、サンプルの設計書を記載します。

FROM amazonlinux:2

#タイムゾーンの設定
RUN /bin/cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime

# x86_64 args
ARG rpm_x64=java-11-amazon-corretto-devel-11.0.5.10-1.x86_64.rpm
ARG path_x64=https://d3pxv6yz143wms.cloudfront.net/11.0.5.10.1
ARG key_x64=13817E35D6AA26BB2D85267712EABAC5209DDBC0

# aarch64 args
ARG rpm_aarch64=java-11-amazon-corretto-devel-11.0.5.10-1.aarch64.rpm
ARG path_aarch64=https://d3pxv6yz143wms.cloudfront.net/11.0.5.10.1
ARG key_aarch64=13817E35D6AA26BB2D85267712EABAC5209DDBC0

# In addition to installing the RPM, we also install
# fontconfig. The folks who manage the docker hub's
# official image library have found that font management
# is a common usecase, and painpoint, and have
# recommended that Java images include font support.
#
# See:
#  https://github.com/docker-library/official-images/blob/master/test/tests/java-uimanager-font/container.java
RUN set -eux; \
    case "$(uname -p)" in \
        x86_64) rpm=$rpm_x64; path=$path_x64; key=$key_x64 ;; \
        aarch64) rpm=$rpm_aarch64; path=$path_aarch64; key=$key_aarch64 ;; \
        *) echo >&2 "Unsupported architecture $(uname -p)."; exit 1 ;; \
    esac; \
    \
    curl -O $path/$rpm \
    && export GNUPGHOME="$(mktemp -d)" \
    && gpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys $key \
    && gpg --armor --export $key > corretto.asc \
    && rpm --import corretto.asc \
    && rpm -K $rpm \
    && rpm -i $rpm \
    && rm -r $GNUPGHOME corretto.asc $rpm \
    && yum install -y fontconfig \
    && yum clean all

ENV JAVA_HOME=/usr/lib/jvm/java-11-amazon-corretto

# recommended by spring boot
VOLUME /tmp

# create directory for application
RUN mkdir /app
WORKDIR /app

COPY ./build/libs/hogehoge.war hogehoge.war

# EXPOSE は開けるポートを指定する.
EXPOSE 8180

# set entrypoint to execute spring boot application
ENTRYPOINT ["sh","-c","java -Dserver.port=8180 -Dspring.profiles.active=local -jar hogehoge.war"]

今回はDockerfileの中身はdockerhubをもとにしています。
仮想AWS EC2とするためAmazon Linuxを使います。
最後のENTRYPOINTのみ各自が動かす環境に合わせてください。  
※warファイルでの実行を想定したものになります。

DockerでMySQLを動かす準備

DBのサンプル設計書になります。

FROM mysql:8.0.18

#MySQL設定ファイルをイメージ内にコピー
ADD ./conf.d/my.cnf /etc/mysql/conf.d/my.cnf
COPY ./initdb.d/* /docker-entrypoint-initdb.d/

#docker runに実行される
CMD ["mysqld"]

今回は最小限にしています。
※Amazon Linuxはdockerhubからコピーしたものですが、MySQLは自前です。
こういった形で記載することもできます。
MySQLのバージョンは各自が利用している環境のバージョンに合わせてください。latestにて最新を取得することも可能です。

Dockerを動かす準備

上記のAmazon LinuxとMySQLを動かすためのファイル(docker-copose.yml)を用意します。

version: '3.3'
services:
    mysql:
        build: ./docker/mysql
        environment:
            MYSQL_DATABASE: dbName
            MYSQL_USER: user
            MYSQL_PASSWORD: password
            MYSQL_ROOT_PASSWORD: rootpassword
        ports:
            - "3314:3306" #3314はhost側のポート、3306はコンテナ側のポート
    app:
        build:
            context: ./
            dockerfile: ./docker/java/dockerfile
        depends_on:
           - mysql
        ports:
            - "8090:8180" #8090はhost側のポート、8180はコンテナ側のポート
        links:
            - mysql

実際に動かす

  1. docker-compose.ymlが配置してあるパスに移動します。
    例:”C:workspase”
  2. docker-compose.exe build
    を実行する。Dockerfileに記載された内容に問題なければbuildされます。
  3. buildが正常に完了した場合、
    docker-compose.exe up -d
    を実行します。
    ※-dオプションを付けることでバックグランド実行になります。
  4. 実行されていることを確認する。
    docker-compose.exe ps
    を実行すると以下のような状態になり、stateがupになっていればOKです。
Name                       Command               State                 Ports
---------------------------------------------------------------------------------------------------
hogehoge_app_1     sh -c java -Dserver.port=8 ...   Up      0.0.0.0:8090->8180/tcp
hogehoge_mysql_1   docker-entrypoint.sh mysqld      Up      0.0.0.0:3314->3306/tcp, 33060/tcp
  1. コンテナを止めたい場合
    docker-compose stop
    を実行してください。
    ※upの反対でdownコマンドも用意されています。
    ですが、これはコンテナを停止し、そのコンテナとネットワークを削除します。なので、コンテナのDBやアプリに改変を加えていた場合破棄されます。

まとめ

今までのローカル環境構築では、各自が各PJの手順に沿ってDB接続や、アプリの設定等を行っていたと思います。この時点でMacとWindows用で2種類存在することもあるかもしれません。
ですが、用意したDockerfileを配布することで配布された側も配布した側と同じ環境がすぐに作成することができます。 (OSを意識することが少なくなります)
Dockerfileやdocker-compose.ymlもバージョン管理していくことをおすすめします。

ローカル環境にAmazon Linuxを立ち上げ、その環境でアプリが動く用になりました。また、コンテナ上にMySQL(DB)を用意したことで、自分専用のDBが作られました。
これでデータを自由に弄れるようになりますし、何回でも同じデータで復元できます。単体テスト等でデータを作り直すのも容易になるはずです。
  
今後は、nginxを追加しかつSonarQubeによる静的解析等を追加していく予定。

author k.kawai

DBの環境差異調査(mysql編)

システム開発において、
 ・本番
 ・検品(ステージング)
 ・開発
と環境が別れている事は通常よくあることであり、それにともないDBの環境もそれぞれ存在することとなります。

開発体制が小規模であったり、管理ルールが整っていない時などに、それぞれのDB環境に差異が発生している事が多々あります。
その差異を視覚化し、容易に把握するための方法をご紹介します。

※今回使用した環境
 ・mysql : 5.6.40
      https://dev.mysql.com/downloads/windows/installer/5.6.html
 ・ローカル環境:windows 10
 ・比較ツール:WinMerge 2.14.0
        https://winmerge.org/downloads/?lang=ja

1.差分抽出SQL の用意


SELECT '### COLUMN情報 #########################' AS '';
SELECT TABLE_NAME
     , COLUMN_NAME
--   , ORDINAL_POSITION
     , COLUMN_TYPE
     , COLUMN_DEFAULT
     , IS_NULLABLE
     , COLUMN_KEY
     , IFNULL( COLLATION_NAME , '')      COLLATION_NAME
     , IFNULL( CHARACTER_SET_NAME , '')  CHARACTER_SET_NAME
     , COLUMN_COMMENT 
  FROM INFORMATION_SCHEMA.COLUMNS
 WHERE TABLE_SCHEMA = @DB_DATABASE
 ORDER BY TABLE_NAME , COLUMN_NAME
;

SELECT '### INDEX情報 ##########################' AS '';
SELECT TABLE_NAME
     , INDEX_NAME
     , SEQ_IN_INDEX
     , COLUMN_NAME
  FROM INFORMATION_SCHEMA.STATISTICS
 WHERE TABLE_SCHEMA = @DB_DATABASE
 ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX 
;

mysql では、DBの情報を INFORMATION_SCHEMA 内に保持しており、その情報を出力しています。
※上記例では、カラムの順番を考慮せず、カラム名順で評価しています。

2.差分抽出SQL の実行

上記 SQL をファイルに保存して、コマンドで実行します。
※ここでは、「db-columns.sql」と命名します。

mysql -e "set @DB_DATABASE='DB名'; source db-columns.sql ;" > 環境.txt

※接続情報等は、省略させて頂いています。

こちらの実行した結果は以下の通りとなります。
※ mysql のバージョンに関わらず、全てのバージョンで稼働すると思います。


### COLUMN情報 #########################
TABLE_NAME	COLUMN_NAME	COLUMN_TYPE	COLUMN_DEFAULT	IS_NULLABLE	COLUMN_KEY	COLLATION_NAME	CHARACTER_SET_NAME	COLUMN_COMMENT
hoge	birthday	date	NULL	YES				
hoge	key	int(11)	NULL	NO	PRI			
hoge	name	varchar(50)	NULL	YES	MUL	utf8_general_ci	utf8	
hoge	sex	char(1)	0	YES		utf8_general_ci	utf8	

### INDEX情報 ##########################
TABLE_NAME	INDEX_NAME	SEQ_IN_INDEX	COLUMN_NAME
hoge	hoge_i1	1	name
hoge	hoge_i1	2	birthday
hoge	PRIMARY	1	key

3.SQL実行結果の比較

上記、環境毎に出力し、その結果を winmerge 等で比較する事が出来ます。
例えば、ローカルの環境と比較。

別のタブで画像を開くと、大きな画像で参照できます。

上記は、以下の テーブルを比較した結果なのですが、

-- 左のDDL
CREATE TABLE `hoge` (
	`key` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(50) NULL DEFAULT NULL,
	`sex` CHAR(1) NULL DEFAULT '0',
	`birthday` DATE NULL DEFAULT NULL,
	PRIMARY KEY (`key`),
	INDEX `hoge_i1` (`name`, `birthday`)
)

-- 右のDDL
CREATE TABLE `hoge` (
	`key` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(50) NULL DEFAULT NULL,
	`sex` VARCHAR(1) NULL DEFAULT '0',
	`birthday` TIMESTAMP NULL DEFAULT NULL,
	PRIMARY KEY (`key`),
	INDEX `hoge_i1` (`name`)
)

WinMarge 上で、差分行が色分けされているのが判るかと思います。
・DATE型 と TIMESTAMP 型 の差異
・CHAR型 と VARCHAR型 の差異
・INDEX の差異

※Oracle ですと、「USER_TAB_COLUMNS」・「USER_IND_COLUMNS」テーブルを使用すると、同じ情報が取得できます。

author k.tomita

マイクロフレームワークを使ってみた-Spark 編-

本記事の概要

  • Spark Framework チュートリアルやってみた
  • Spark Framework を実際のプロジェクトに使うかを評価してみた

こんにちは。SBWorksです。

突然ですが、みなさま。マイクロフレームワークという言葉を知っていますか?

マイクロフレームワークとは

microframework is a term used to refer to minimalistic web application frameworks. It is contrasted with full-stack frameworks.

出典 https://en.wikipedia.org/wiki/Microframework

簡単に言うと、軽量でフルスタックなウェブフレームワークって感じですね。最近は、急激な環境変化に対応する為に、お互いに疎なサービスを開発し、それらの組み合わせて大きなサービスを実行するという開発がトレンドになってきています。フレームワークもその急激な変化に対応するために、軽量なフレームワークの開発が盛んに行われているようです。色々なフレームワークが開発されている中で、今回はJavaのフレームワークである、Spark Frameworkを使ってみようと思います。(詳しく知りたい人は公式ページのドキュメントを参照してください。)

Spark Framework のチュートリアルやってみた

それでは実際にSpark Frameworkを使ってみましょう。

まずは公式ページへ。

チュートリアルページへ。

今回はBasic webapp structure のチュートリアルをやってみます。

Githubにソースコードが公開されている。チュートリアルが充実してるのはありがたいですね。

javaディレクトリ配下のソースコードはこんな感じ。util + 画面毎に分かれてる感じ。

resourceディレクトリ配下のソースコードはこんな感じ。velocity使ってる。

gradleとmavenがあったので、今回はgradleでビルドしてみます。

$ cd path/to/spark-basic-structure
$ gradle build

起動してみます。

$ gradle run

立ち上がったらログイン画面(http://localhost:4567/login/)へアクセスしてみます。

ひとまず動きました。あとはUsername/Passwordを入力して(UserDao.javaを確認)、書籍一覧画面(localhost:4567/books/)へアクセスしてみると、

書籍一覧も表示できました。

Spark Framework を実プロジェクトに使うかを検証

チュートリアルを動かすことはできたので、これを実プロジェクトで使えるのかを検証していきたいと思います。

まずは本番環境にリリースすることを想定して、jarファイルを作成して起動します。

$ cd path/to/spark-basic-structure
$ gradle build
$ java -jar build/libs/spark-basic-structure-all-1.0-SNAPSHOT.jar

起動したら、先ほどと同じようにログイン画面(http://localhost:4567/login/)へアクセスしてみます。

CSSが効いていない…。ソースコードで該当箇所を調べます。どうやら下記ソースコードがgradle run で起動した時とjarで起動した時で異なるようです。

staticFileLocation("/public");

調べてみると、同じようにrunタスクで起動した時には正常に動くのに、jarで起動すると、静的ファイルが読み込めないという記事がいくつかあったので同じようにハマっている人がいるようです。
公式にissueを投げている人がいて、grade shadowでjarファイル作ったらうまくいくよと書いていたので、試してみるもうまく行きませんでした。

jarでの不具合が出ている問題が解決できないと、本番稼働できないので、今回はSpark Framework の使用は一旦見送ることにしました。

結論

  • Spark Framework でチュートリアルやってみた
  • Spark Framework を実プロジェクトでは使わないことにした

最後に

今後もマイクロフレームワークを検証していきたいと思います。

ありがとうございました!