JavaScriptでAmazon Chime SDKを動かしてみる

Amazon Chime SDKの動作を確認するクライアントサイドをJavaScriptで実装してみたいと思います。

事前準備

サーバサイド

サーバサイドにて、ミーティング(meeting)と参加者(attendee)を作成して、クライアントサイドへ渡してあげる必要があります。 以下などを参考に、サーバサイドを実装してください。

gsol.hatenablog.com

Amazon Chime SDKダウンロード

以下READMEを参考に、Amazon Chime SDKをダウンロードしてください。

https://github.com/aws/amazon-chime-sdk-js/tree/master/demos/singlejs

実装内容

<html lang="ja">
<head>
  <script src="aws/amazon-chime-sdk.min.js"></script>
  <script type="text/javascript">
    var logger = '';
    var deviceController = '';
    var configuration = '';
    var meetingSession = '';
    
    // Meeting参加
    function addMeeting() {
      // サーバサイドにてミーティングと参加者を作成し、クライアントサイドで受け取る
      var meeting = {XXXX};
      var attendee = {XXXX};
      (async () => {
        logger = new ChimeSDK.ConsoleLogger('MyLogger', ChimeSDK.LogLevel.ERROR);
        deviceController = new ChimeSDK.DefaultDeviceController(logger);
        console.log('deviceController', deviceController);
             
        // ミーティングセッション作成
        configuration = new ChimeSDK.MeetingSessionConfiguration(meeting, attendee);
        console.log('configuration', configuration);
        meetingSession = new ChimeSDK.DefaultMeetingSession(configuration, logger, deviceController);
        console.log('meetingSession', meetingSession);

        // 入出力デバイス取得(ブラウザはマイクとカメラの許可を求める)
        const audioInputDevices = await meetingSession.audioVideo.listAudioInputDevices();
        console.log('audioInputDevices', audioInputDevices);
        const audioOutputDevices = await meetingSession.audioVideo.listAudioOutputDevices();
        console.log('audioOutputDevices', audioOutputDevices);
        const videoInputDevices = await meetingSession.audioVideo.listVideoInputDevices();
        console.log('videoInputDevices', videoInputDevices);

        meetingSession.audioVideo.chooseVideoInputQuality(1280,720,3,1000);
        await meetingSession.audioVideo.chooseVideoInputDevice(videoInputDevices[0].deviceId);
        await meetingSession.audioVideo.chooseAudioInputDevice(audioInputDevices[0].deviceId);
        if (audioOutputDevices[0]) {
          await meetingSession.audioVideo.chooseAudioOutputDevice(audioOutputDevices[0].deviceId);
        }

        // オーディオ要素取得・バインド
        const audioElement = document.getElementById('audio-preview');
        meetingSession.audioVideo.bindAudioElement(audioElement);

        const videoElements = {}; // ビデオタイル要素
        for (let i = 0; i < 16; i++) {
          videoElements[i] = document.getElementById(`video-preview` + i);
        }
        const indexMap = {};

        const acquireVideoElement = tileId => {
          // 既にバインドされている場合、同要素を返却
          for (let i = 0; i < 16; i++) {
            if (indexMap[i] === tileId) {
              return videoElements[i];
            }
          }
          // バインド可能な要素を返却
          for (let i = 0; i < 16; i++) {
            if (!indexMap.hasOwnProperty(i)) {
              indexMap[i] = tileId;
              return videoElements[i];
            }
          }
          return;
        }
        if (!acquireVideoElement) {
          alert('利用可能なビデオ要素がありません。');
        };

        // observer設定
        const observer = {
          audioVideoDidStart: () => {
            console.log('Started');
          },
          // 映像要素取得・バインド
          videoTileDidUpdate: tileState => {
            if (!tileState.boundAttendeeId) {
              return;
            }
            console.log('Start video');
            meetingSession.audioVideo.bindVideoElement(tileState.tileId, acquireVideoElement(tileState.tileId));
          }
        };

        // 出力開始
        meetingSession.audioVideo.addObserver(observer);
        meetingSession.audioVideo.start();
        meetingSession.audioVideo.startLocalVideoTile();

        document.getElementById('meetingId').value = meetingInfo.Meeting.MeetingId;
        document.getElementById('attendeeId').value = attendeeInfo.Attendee.AttendeeId;
       }) ();
    },
    error : function() {
      console.log('通信エラーです');
    }

    // Meeting退室
    function leaveMeeting() {
      // observer設定
      const observer = {
        audioVideoDidStop: sessionStatus => {
          const sessionStatusCode = sessionStatus.statusCode();
          if (sessionStatusCode === ChimeSDK.MeetingSessionStatusCode.Left) {
            alert('退室しました。');
          } else {
            alert('セッションが切れました。ステータスコード: ' + sessionStatusCode);
          }
        }
      };
    }
  </script>
</head>
<body>
  <div>
    video test<br>
    MeetingId : <input id="meetingId" value="" style="width:300px"/><br>
    AttendeeId : <input id="attendeeId" value="" style="width:300px"/>
    <br>
    <button type="button" value="" onclick="addMeeting()">参加</button>
    <button type="button" value="" onclick="leaveMeeting()">退室</button>
  </div>
  <video id="video-preview0" style="width:100%; height: 200px"></video>
  <video id="video-preview1" style="width:100%; height: 200px"></video>
  <video id="video-preview2" style="width:100%; height: 200px"></video>
  <video id="video-preview3" style="width:100%; height: 200px"></video>
  <video id="video-preview4" style="width:100%; height: 200px"></video>
  <video id="video-preview5" style="width:100%; height: 200px"></video>
  <video id="video-preview6" style="width:100%; height: 200px"></video>
  <video id="video-preview7" style="width:100%; height: 200px"></video>
  <video id="video-preview8" style="width:100%; height: 200px"></video>
  <video id="video-preview9" style="width:100%; height: 200px"></video>
  <video id="video-preview10" style="width:100%; height: 200px"></video>
  <video id="video-preview11" style="width:100%; height: 200px"></video>
  <video id="video-preview12" style="width:100%; height: 200px"></video>
  <video id="video-preview13" style="width:100%; height: 200px"></video>
  <video id="video-preview14" style="width:100%; height: 200px"></video>
  <video id="video-preview15" style="width:100%; height: 200px"></video>
  <audio id="audio-preview"></audio>
</body>
</html>

各メソッドなどは以下を参照してください。

https://aws.github.io/amazon-chime-sdk-js/

動作確認

ミーティング参加

f:id:sanok-gsol:20201130173256p:plain

ミーティング退出

f:id:sanok-gsol:20201130173311p:plain

注意点

ミーティングセッションの自動終了

生成したミーティングは、音声接続が無いまま5分経過すると自動的にセッションが終了します。 その他ミーティングのセッションが自動的に終了する場合がありますので、以下をご参照ください。

https://docs.aws.amazon.com/chime/latest/dg/mtgs-sdk-mtgs.html

ブラウザの制約

Chromiumをベースにしているブラウザ(Chrome、Edgeなど)では、「https」もしくは「localhost」にてアクセスしないと、「getUserMedia」メソッドが正常に動作しないため、 ビデオ映像や音声などが出力されません。 以下をご参照ください。

https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-powerful-features-on-insecure-origins

Amazon Chime のミーティングと参加者をJavaで作成する

AWS SDK for Java(以下SDK)を用いて、Amazon Chime(以下Chime)のミーティングと参加者を作成してみたいと思います。

事前準備

credentialsの設定

まず、IAMユーザを作成して「AmazonChimeFullAccess」、「 AmazonChimeSDK」の権限を付与したcredentialsを作成してください。 credentialsをサーバに設定する方法は以下をご参照ください。

https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html

AWS SDK for Javaダウンロード

以下を参照し、SDKをダウンロードして展開してください。

https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/setup-install.html

実装内容

ディレクトリ構成例

{project}
│  .classpath
│  .project
│
├─.settings
│      org.eclipse.jdt.core.prefs
│
├─bin
│  └─chime
│          createMeeting.class
│
├─lib
│      aws-java-sdk-1.11.884.jar
│
├─src
│  └─chime
│          createMeeting.java
│
└─third-party
    └─lib
          aspectjrt-1.8.2.jar
          aspectjweaver.jar
          aws-swf-build-tools-1.1.jar
          commons-codec-1.11.jar
          commons-logging-1.1.3.jar
          freemarker-2.3.9.jar
          httpclient-4.5.9.jar
          httpcore-4.4.11.jar
          ion-java-1.0.2.jar
          jackson-annotations-2.6.0.jar
          jackson-core-2.6.7.jar
          jackson-databind-2.6.7.3.jar
          jackson-dataformat-cbor-2.6.7.jar
          javax.mail-api-1.4.6.jar
          jmespath-java-1.11.884.jar
          joda-time-2.8.1.jar
          netty-buffer-4.1.48.Final.jar
          netty-codec-4.1.48.Final.jar
          netty-codec-http-4.1.48.Final.jar
          netty-common-4.1.48.Final.jar
          netty-handler-4.1.48.Final.jar
          netty-resolver-4.1.48.Final.jar
          netty-transport-4.1.48.Final.jar
          spring-beans-3.0.7.RELEASE.jar
          spring-context-3.0.7.RELEASE.jar
          spring-core-3.0.7.RELEASE.jar
          spring-test-3.0.7.RELEASE.jar

ダウンロードした「AWS SDK for Java」から持ってきた「{project}/lib」配下と「{project}/third-party/lib」配下のライブラリにはビルド・パスを通してください。

ソース(createMeeting.java)

package chime;

import com.amazonaws.AmazonClientException;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.services.chime.AmazonChime;
import com.amazonaws.services.chime.AmazonChimeClientBuilder;
import com.amazonaws.services.chime.model.Attendee;
import com.amazonaws.services.chime.model.CreateAttendeeRequest;
import com.amazonaws.services.chime.model.CreateAttendeeResult;
import com.amazonaws.services.chime.model.Meeting;
import com.amazonaws.services.chime.model.CreateMeetingRequest;
import com.amazonaws.services.chime.model.CreateMeetingResult;

public class createMeeting {

    public static void main(String[] args) {
    
        // credentials確認
        ProfileCredentialsProvider credentialsProvider = new ProfileCredentialsProvider();
        try {
            credentialsProvider.getCredentials();
        } catch (Exception e) {
            throw new AmazonClientException(
                    "Cannot load the credentials from the credential profiles file. " +
                    "Please make sure that your credentials file is at the correct " +
                    "location (~/.aws/credentials), and is in valid format.",
                    e
            );
        }
        
        // Chime リージョン設定
        AmazonChime chime = AmazonChimeClientBuilder.standard().withRegion("us-east-1").build();
        
        // Meetingリクエスト作成
        CreateMeetingRequest meetingReq = new CreateMeetingRequest();
        meetingReq.setClientRequestToken("test");
        meetingReq.setMediaRegion("ap-northeast-1");
        // Meeting作成
        CreateMeetingResult meetingRes = chime.createMeeting(meetingReq);
        Meeting meeting = meetingRes.getMeeting();
        // MeetingId取得
        String meetingId = meeting.getMeetingId();
        
        // Attendeeリクエスト作成
        CreateAttendeeRequest attendeeReq = new CreateAttendeeRequest();
        attendeeReq.setExternalUserId("test_user");
        attendeeReq.setMeetingId(meetingId);
        // Attendee作成
        CreateAttendeeResult attendeeRes = chime.createAttendee(attendeeReq);
        Attendee attendee = attendeeRes.getAttendee();
        
        System.out.println(meeting);
        System.out.println(attendee);
    }
}

各クラスやメソッドなどは以下を参照してください。

https://sdk.amazonaws.com/java/api/latest/

動作確認

以下の通り、meeting情報とattendee情報が生成されていることが確認できます。 ※一部マスクしてます

・meeting

{
  MediaPlacement:{
    AudioFallbackUrl: "wss://haxrp.m3.an1.app.chime.aws:443/calls/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    AudioHostUrl: "XXXXc6f84f16ea0ec5ddbd5bbab101a5.k.m3.an1.app.chime.aws:3478",
    ScreenDataUrl: "wss://bitpw.m3.an1.app.chime.aws:443/v2/screen/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    ScreenSharingUrl: "wss://bitpw.m3.an1.app.chime.aws:443/v2/screen/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    ScreenViewingUrl: "wss://bitpw.m3.an1.app.chime.aws:443/ws/connect?passcode=null&viewer_uuid=null&X-BitHub-Call-Id=f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    SignalingUrl: "wss://signal.m3.an1.app.chime.aws/control/f02353d3-122c-4c21-9cbb-076e56e2XXXX",
    TurnControlUrl: "https://ccp.cp.ue1.app.chime.aws/v2/turn_sessions"
  },
  MediaRegion: "ap-northeast-1",
  MeetingId: "f02353d3-122c-4c21-9cbb-076e56e2XXXX"
}

・attendee

{
  Attendee:{
    AttendeeId: "efca3edd-1973-3801-517f-69cc2df3XXXX",
    ExternalUserId: "test_user",
    JoinToken: "ZWZjYTNlZGQtMTk3My0zODAxLTUxN2YtNjljYzJkZjM1YTAyOjA0ODBiN2VmLTcwNTctNGJiNS05NmM0LWI2NmMwOWQ5NDXXXX"
  }
}

サーバサイドの処理については以上となります。ここで生成した情報を利用することで、ミーティングに参加してビデオチャットを行えるようになります。画面の実装については、続きの記事に記載していますので、そちらもご参考ください。

gsol.hatenablog.com

Apache Airflowの紹介(kintoneとの連携編)

Airflowとkintoneアプリを連携して、期日が近づくとメール送信・Slack通知する処理を作成したので紹介します。

処理の概要

次のようなイメージで処理を作成していきます。

  • kintoneの契約書管理アプリから各レコードの契約満了日を取得。
  • 契約自動更新が無しで直近(一週間以内)で契約満了となるのものについて、契約更新のお知らせメール・Slackメッセージを送信。

kintoneアプリ

デフォルトで用意されている契約書管理アプリを利用します。
f:id:toheih:20201012154335p:plain

  • 連絡先にはメールアドレスを指定することとします。

AirflowのDAG作成

以下のタスクを実行するDAGを作成します。

  • kintoneからREST APIでデータ取得。
    • レコードを一括取得するカーソルを作成。(SimpleHttpOperator)
    • カーソルIDをXComから取得。(PythonOperator)
    • カーソルからデータを取得。(SimpleHttpOperator)
  • 取得結果からメール本文を作成し、メール送信処理用のDAGを実行(PythonOperator)
  • メール送信処理を実行(EmailOperator)

XComとは、タスク間で変数のやり取りをするための仕組みです。
また、メール送信処理については複数回実行可能とするためデータ取得処理のDAGとは別のDAGを作成し、データ取得処理DAGから呼び出すようにします。

kintoneのデータ取得処理

複数レコードを取得する必要があるため、カーソルを利用して取得します。

# REST API実行結果判定処理
def response_check(response):
    result=response.json()
    return response.status_code == requests.codes.ok

# カーソル作成タスク
create_cursor = SimpleHttpOperator(
    task_id='create_cursor',
    http_conn_id='',
    method='POST',
    endpoint='https://(サブドメイン名).cybozu.com/k/v1/records/cursor.json',
    data=json.dumps(
        {
            'app': '(アプリID)',
            'fields' : ['company_name', 'tanto_name', 'tanto_address', 'keiyaku_manryo_date'],
            'query' : 'keiyaku_manryo_date <= THIS_WEEK() and keiyaku_auto_update not in ("有")'
        }
    ),
    headers={'X-Cybozu-API-Token': '(kinoneのAPIトークン)', 'Content-Type' : 'application/json'},
    response_check=response_check,
    log_response=True,
    xcom_push=True,
    dag=dag,
)
  • response_checkで指定した関数によって、タスクを正常終了とするかエラーとするかを決定できます。今回はHTTPステータスコードで判定しています。
  • xcom_push=Trueにすることで、タスクの実行結果をXComに保存することが出来ます。
    • 保存した値はタスクのViewLogから確認することが出来ます。

f:id:toheih:20201015160247p:plain

次に、カーソルからレコードデータを取得します。
取得にはカーソルIDが必要で、カーソル作成APIの取得結果から取得します。

# カーソルID取得処理
def get_id_from_xcom(**context):
    jsonvalue = context['ti'].xcom_pull(task_ids='create_cursor')
    value = json.loads(jsonvalue)
    return value['id']

# カーソルID取得タスク
get_id = PythonOperator(
    task_id='get_id',
    dag=dag,
    provide_context=True,
    xcom_push=True,
    python_callable=get_id_from_xcom,
)

# レコードデータ取得タスク
open_cursor = SimpleHttpOperator(
    task_id='open_cursor',
    http_conn_id='',
    method='GET',
    endpoint='https://(サブドメイン名).cybozu.com/k/v1/records/cursor.json',
    data={"id" : "{{ ti.xcom_pull(task_ids='get_id') }}"},
    headers={'X-Cybozu-API-Token': '(kinoneのAPIトークン)'},
    response_check=response_check,
    log_response=True,
    xcom_push=True,
    dag=dag,
)
  • context['ti'].xcom_pull(task_ids='xxxx')でXComに保存された値を取得することが出来ます。
    • また"{{ ti.xcom_pull(task_ids='xxxx') }}"と記載することで文字列に埋め込むことも出来ます。
  • カーソルから取得したレコードデータは以下の通りXComに保存されます。

f:id:toheih:20201015160553p:plain

メール・メッセージ送信処理

レコードデータからメール・メッセージ本文を作成し、別DAGに渡します。

# 本文作成処理
def create_mail_content_from_xcom(**context):
    jsonvalue = context['ti'].xcom_pull(task_ids='open_cursor')
    value = json.loads(jsonvalue)
    for keiyaku_info in value['records']:
        k = {'mail_content' : '', 'slack_content' : '', 'mail_address' : keiyaku_info['tanto_address']['value'] }
        mail_content = keiyaku_info['company_name']['value'] + '<br/>'
        mail_content += keiyaku_info['tanto_name']['value']+ '様<br/><br/>'
        mail_content += '平素よりサービスをご利用いただき、誠にありがとうございます。<br/>'
        mail_content += 'お客様の契約有効期限が間近となっておりますのでお知らせいたします。<br/>'
        mail_content += 'このメールは ' + keiyaku_info['keiyaku_manryo_date']['value'] +' に契約満了を迎える方へ送信しています。<br/>'
        mail_content += '更新のお手続きをお願い致します。'
        k['mail_content'] = mail_content
        k['slack_content'] = mail_content.replace('<br/>','\n')
        trigger_dag(dag_id='sub_notice',
                    conf=json.dumps(k),
                    execution_date=None,
                    replace_microseconds=False)
        pass
    return

# 本文作成処理実行タスク
create_mail_content = PythonOperator(
    task_id='create_mail_content',
    dag=dag,
    provide_context=True,
    xcom_push=False,
    python_callable=create_mail_content_from_xcom,
)
  • trigger_dagで別DAGを実行することが出来ます。またconfに指定した値は別DAGで参照することが可能です。

別DAGではメール送信処理・Slackメッセージ送信処理を実装します。

# メール送信処理タスク
send_email = EmailOperator(
    task_id='send_email',
    dag=sub_dag,
    to="{{ dag_run.conf['mail_address']}}",
    subject='契約満了のお知らせ',
    html_content="{{ dag_run.conf['mail_content']}}",
    mime_charset='utf-8',
)

# Slackメッセージ送信処理タスク
send_slack = SlackWebhookOperator(
    task_id='send_slack',
    dag=sub_dag,
    http_conn_id='slack_test',
    message="{{ dag_run.conf['slack_content']}}",
)

send_email >> send_slack
  • Slack送信を行う場合はあらかじめConn IdとWebhook URLをAirflow側に登録しておく必要があります。
    • Admin > Connection から登録することが出来ます。
    • Conn IdはSlackWebhookOperatorのhttp_conn_idに指定します。

f:id:toheih:20201015165504p:plain

動作確認

実際にkintoneにデータを登録します。
f:id:toheih:20201015182719p:plain

AirflowのDAGを実行し、エラーなく完了しました。
f:id:toheih:20201015182956p:plain

メールが届くことが確認できました。
f:id:toheih:20201015183059p:plain

Slackのほうにもちゃんとメッセージが送られています。
f:id:toheih:20201016131639p:plain

今回はDAGを手動で実行しましたが、スケジュール実行も可能なので定期的にデータをチェックして通知することも可能です。

以上、Airflowとkintoneの連携について紹介しました。

Apache Airflowの紹介(プラグイン編)

前回に引き続きAirflowについて紹介します。
今回はプラグインについてです。

プラグインとは

Airflowで使用するOperatorやHook、Sensorを独自に作成することができる機能です。
※Hookとは外部リソースとのやり取りをするプログラムで、Sensorはあるアクションが発生した際に実行されるOperatorです。
今回は独自のOperator(カスタムOperator)を作成してみます。

作成方法

事前準備

前回用意したdocker-compose.xmlにvolumesの設定を追加します。
プラグイン用のフォルダを設定します。

 volumes:
     - ./dags:/usr/local/airflow/dags
     - ./plugins:/usr/local/airflow/plugins

フォルダ構成

docker-compose up をするとpluginsフォルダが作成されます。
pluginsフォルダ配下に以下のファイル、フォルダを作成します。

plugins
├─sample_plugin.py・・・・・・・プラグインクラス
└─operators
     └─ sample_operator.py ・・・カスタムOperatorクラス

プラグインクラスの作成

sample_plugin.pyはプラグインクラスになります。
プラグインとして追加するOperatorの名前を定義します。
以下のように定義します。

from airflow.plugins_manager import AirflowPlugin
from operators.sample_operator import SampleOperator

class SamplePlugin(AirflowPlugin):
    name = "sample_plugin"
    operators = [SampleOperator]

プラグインクラスでは、airflow.plugins_manager.AirflowPluginクラスを継承する必要があります。
またSampleOperatorはこれから作成するカスタムOperatorクラスです。

カスタムOperatorクラスの作成

sample_operator.pyはカスタムOperatorクラスになります。
このクラスで具体的に実行する処理を定義します。
今回は公式サイトにあるサンプル処理(渡された引数に「Hello」をつけてログ出力する処理)を使用します。

from airflow.models.baseoperator import BaseOperator
from airflow.utils.decorators import apply_defaults

class SampleOperator(BaseOperator):

    @apply_defaults
    def __init__(
            self,
            name: str,
            *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.name = name

    def execute(self, context):
        message = "Hello {}".format(self.name)
        print(message)
        return message

カスタムOperatorクラスではairflow.models.baseoperator.BaseOperatorクラスを継承する必要があります。

動作確認

作成したOperatorを実行するDAGを作成します。
Operatorの引数には「toheih」を渡します。

from datetime import timedelta
from airflow import DAG
from airflow.utils.dates import days_ago

from airflow.operators.sample_plugin import SampleOperator

default_args = {
    'owner': 'airflow',
    'depends_on_past': False,
    'start_date': days_ago(0),
    'email': ['airflow@example.com'],
    'email_on_failure': False,
    'email_on_retry': False,
    'retries': 0,
    'retry_delay': timedelta(minutes=5),
}
dag = DAG(
    'hello',
    default_args=default_args,
    description='hello DAG',
    schedule_interval=timedelta(days=1),
)

hello = SampleOperator(
    task_id='sample-task',
    name='toheih',
    dag=dag,
)

hello

Aiflowにデプロイして動作させてみます。
プラグインを有効にするにはAirflowを再起動する必要があります。
f:id:toheih:20200611132834p:plain

エラーなく動作完了しました。
f:id:toheih:20200611132827p:plain

以下の通り「Hello toheih」と表示されることが確認できました。
f:id:toheih:20200611132831p:plain


以上、簡単ですがAirflowのプラグイン機能について紹介しました。

Apache Airflowの紹介(概要・環境構築編)

今回は、ApacheOSSとして提供しているAirflowを調査しましたので
Airflowでできることと、環境構築について紹介したいと思います。

Airflowでできること

Airflowではワークフローの作成とスケジュール実行、モニタリングを行うことができます。
ここで言うワークフローとは、複数のタスクを依存関係に従って実行していくものを言います。

ワークフローの作成

AirflowではワークフローはDAGと呼ばれています。
DAGとは有向非巡回グラフのことで、大雑把に説明すると各ノード(タスク)に順番をつけることができ(有向)、一度実行したノードを再実行しない(非巡回)グラフです。
ja.wikipedia.org
DAGはPythonで各タスクを定義することで作成します。

DAGを構成するタスクはOperatorと呼ばれており
Bashコマンドの実行や外部Pythonスクリプトの実行、HTTPリクエストの送信、Slackへの通知など様々なOperatorが用意されています。
Python API Reference — Airflow Documentation

簡単なワークフローであれば
用意されているOperatorを組み合わせるだけで作成することができます。

また、独自に開発したOperatorをプラグインとして追加することも可能です。
Plugins — Airflow Documentation

スケジュール実行

DAGに対して開始日時や実行間隔などが設定でき、定期実行させることが可能です。
タスクでエラーが発生した場合の再実行回数など、エラーハンドリングについても設定可能です。
設定はDAGのパラメータとして記述します。
https://airflow.apache.org/docs/stable/tutorial.html#default-arguments

モニタリング

DAGの一覧、各タスクの実行状況、タスクの依存関係などを確認できる管理コンソールが用意されており、Webブラウザ上で確認することができます。
失敗したタスクを手動で再実行させることも可能です。

f:id:toheih:20200513151757p:plain
DAGに定義された各タスクの実行状況が視覚的に確認できます。

Airflowの環境構築

Airflowのセットアップ

公式サイトでは数コマンドで完了させていますが、
githubにてDockerfileが公開されており、今回はこちらを利用して環境構築を行いました。
GitHub - puckel/docker-airflow: Docker Apache Airflow

以下3つの構築方法(Executor)があるようで
今回はLocalExecutorを利用しました。

  • SequentialExecutor
  • LocalExecutor
  • CeleryExecutor

コンテナの開始後、http://localhost:8080Webブラウザでアクセスし以下画面が表示されればセットアップ完了です。
f:id:toheih:20200513154903p:plain

DAGの作成とデプロイ

コンテナ開始後、dagsフォルダが自動で作成されます。
そこにPythonスクリプトで記述したDAGを配置すると、自動デプロイされ実行可能になります。
今回は公式サイトにあるチュートリアルのDAGを配置します。
airflow.example_dags.tutorial — Airflow Documentation

f:id:toheih:20200513160903p:plain
DAGを配置すると自動でデプロイされます。

DAGの実行

左端に表示されているトグルをOnにすると、
DAGが設定したスケジュール通りに実行されます。
チュートリアルDAGの場合、2日前を基準にして2回実行されます。
なお、右側のボタンをクリックするとスケジュールに関係なく即時実行されます。
f:id:toheih:20200513162203p:plain

DAG名をクリックするとTree View画面が表示され、DAGの実行状況が確認できます。
f:id:toheih:20200513162905p:plain

チュートリアルDAGでは以下タスクを定義していますが、
これらの実行結果を画面から確認することができます。

  • 現在日付を表示
  • Bashのsleepコマンドで5秒間処理を遅延
  • Jinja Templateを利用して現在日付と一週間後の日付、メッセージを表示

各タスクのステータスをクリックするとタスクの詳細画面が表示されます。
ViewLogをクリックして実行ログを確認します。
f:id:toheih:20200513165234p:plain

実行ログに現在の日付が表示されていることが確認できます。
f:id:toheih:20200513165527p:plain


以上、簡単ですがAirflowについて紹介しました。
今後は、Operatorプラグインの開発やREST APIを利用した外部サービス連携などについて紹介できればと思います。

httpのバージョンによる違いと古い場合の問題について

開発部のS.Kです。 今回は、普段意識しないhttpのバージョンによる小話を紹介したいと思います。

httpとは

httpというのは、ブラウザがWebサーバと通信するときに使っている通信プロトコルです。 現在はバージョン3(HTTP/3)まで規格されています。 バージョン1では1つデータをリクエストすると、それが返ってくるまで次のデータをリクエストできないという問題がありました。 バージョン2では複数のデータをまとめてリクエストできるようになり、効率が飛躍的に向上しています。 バージョン3は色々特殊になるので今回は割愛。

ブラウザによる工夫

http/1で単純にページを表示しようとすると、効率が悪く時間がかかります。 そこで、現在使われているようなブラウザでは、サーバとの接続を複数本作り、ある程度平行してデータを要求できるように工夫しています。 ただこの方法も限度があり、同時接続数には上限が設定されています。 例えばGoogleChromeでは6つが上限のようです(2020年1月現在)。

http/1で通信するときに起きるトラブル

http/2で通信するには、ブラウザとサーバがそれぞれ対応している必要があります。 ブラウザは、よほど古いものでなければ、まず対応しているでしょう。 それに対してサーバは様々なので、中にはhttp/1しか対応していない可能性もあります。 普通に使う分にはhttp/1でもそれほど気にならないと思いますが、特殊なケースではうまく動かなくなることがあります。 例えばintra-martのIMBoxでは、簡易チャット機能としてDirectMessageというものがあります。 相手からメッセージが来ると画面に表示してくれるのですが、この機能を実現するために、常にサーバと接続を保っています。 なので、サーバがhttp/1しか対応しておらず、DirectMessageを使える画面を6つ以上開くと、接続数が上限に達して、それ以上画面を表示できなくなります。 その状態では別ページに移動することも出来ないので、急に何も操作できなくなって驚くことでしょう。

まとめ

http/1で通信すると、表示が遅いし、多重にページを開くとうまく動かないことがある、という小話でした。 サーバを運営している方は、きちんとhttp/2で動くようになっているか確認しましょう。

Formaのエラーメッセージの表示順を変える方法

intra-martのFormaDesigerで、入力チェックエラー時のエラーメッセージの表示順を変える方法を紹介します。

やりたいこと

FormaDesignerで作った画面で、入力チェックエラーの表示の順番を変えたい、と思ったことありませんか?

例えば以下のような画面の場合。画面項目の並びとエラーメッセージの表示順が合っていません。

画面項目の並び順と同じく、エラーメッセージも

  • 新住所
  • 郵便番号
  • 電話番号
  • 旧住所

の順で出したいですよね。どうやったら変更できるのでしょうか。

f:id:hamaguchiyu:20191220173741p:plain

やること

アイテムの配置順を変える

やり方としては、Fromaのフォーム編集画面で「アイテムの配置順」を変えるという方法があります。「アイテムの配置順」はアイテムを選択し、右クリックで開いたメニューから変更できます。

今回の場合は「郵便番号」に対し、「背面へ移動」× 2回を行っています。

f:id:hamaguchiyu:20191220174244p:plain

変更したアイテムの配置順はラベル一覧で確認することができます。配置順を変えた結果、ラベル一覧の順番も以下のように変わっています。

f:id:hamaguchiyu:20191220173841p:plain

結果

f:id:hamaguchiyu:20191220173904p:plain

アイテムの配置順を変えたことでエラーメッセージも

  • 新住所
  • 郵便番号
  • 電話番号
  • 旧住所

の順で表示されるようになりました。

VirtualBoxで仮想マシンを最小サイズでエクスポートするテクニック

開発部のS.Kです。 VirtualBox仮想マシンをエクスポートするときの、ちょっとしたテクニックを紹介したいと思います。

概要

VirtualBox仮想マシンを作成して、色々なアプリケーションをインストールしてからエクスポートすると、他の環境でもすぐにその環境が使えて便利です。 しかし、OSをインストールしてアプリケーションも追加した後の仮想マシンをそのままエクスポートすると、かなり大きなファイルサイズになってしまいます。 場合によっては数十GBにもなり、流石にここまでファイルサイズが大きいと扱いにくて大変です。

ですがこれに少し工夫をすることで、エクスポートファイルのサイズを最小限にすることが出来ます。 今回は、そのテクニックを紹介します。

前提条件

仕組み

エクスポートファイルの中身の大半は、仮想ハードディスクです。 これをいかに小さく出来るかがポイントになります。 可変サイズの仮想ストレージを使っていると、保存領域が不足したときに、上限に達するまで少しずつ自動で拡張されていきます。 しかし、拡張は自動で行われても、縮小は自動で行われず、その後に空き領域が十分に出来てもそのままになります。 この状態でエクスポートすると、本当は使っていない領域の分までエクスポートされ、ファイルサイズが大きくなってしまいます。

縮小を行うコマンドは用意されており、手動で実行は出来ます。 ただし、これを使う場合は事前に未使用領域を0埋めしておく必要があります。 なので、まずは未使用領域を0埋めして、その後に仮想ストレージの縮小コマンドを実行することで、仮想ストレージを最小サイズにすることが出来ます。

手順

まずは、仮想マシンにログインして、空き領域を0埋めするために以下のコマンドを実行します。

$ sudo dd if=/dev/zero of=zero bs=4k
$ sudo rm zero

これを実行すると、空き容量と同じサイズのzeroというファイルが作られ、直後に削除されます。 zeroは中身に全て0が入力されたバイナリデータです。 これを空き領域いっぱいに作成してから消すことで、空き領域全てに0が書き込まれます。

この後、仮想マシンをシャットダウンして、ホストマシンで以下のコマンドを実行します。

VBoxManage.exe modifyhd "仮想ストレージのuuid" --compact

実行すると、0で埋められた未使用領域を開放して、仮想ストレージのファイルサイズを縮小してくれます。 実行前後で仮想ストレージファイルを比べると、ファイルサイズが減っていることが分かると思います。

最後に、この状態で仮想マシンのエクスポートを行います。 問題なければ、最小限のサイズで仮想マシンがエクスポートされるはずです。

まとめ

仮想マシンを最小サイズでエクスポートする方法を紹介しました。 それほど大きな手間も掛からないため、仮想マシンをエクスポートするときは、常に行っても良いと思います。

IM-Workflowの案件状態を判定する方法

intra-martでIM-Workflowの案件状態を判定する方法を紹介します。

IM-Workflowの案件情報を取得する際、案件の状態によってAPIを使い分ける必要がありますが、対象の案件の状態が「未完了」か「完了」かがわからないことがあります。

その場合、案件状態マネージャ (UserMatterStatus) を使用することで、案件状態ごとに処理をわけることができます。

案件状態による処理分岐の実装

以下ソースです。

案件状態マネージャ (UserMatterStatus) で案件状態を取得して、案件状態ごとに分岐し、APIを適切に使い分けています。

// 案件状態を取得する
String searchLevel  = "1";// "0":未完了案件のみ検索、"1":未完了/完了案件を検索、"2":未完了/完了/過去案件を検索
UserMatterStatus userMatterStatus = new UserMatterStatus(searchLevel);
UserMatterStatusModel userMatterStatusModel = userMatterStatus.getMatterStatus(systemMatterId);
String matterStatusCode = userMatterStatusModel == null ? null : userMatterStatusModel.getMatterStatusCode();

// 案件状態による分岐
if (MatterStatus.matSts_Active.toString().equals(matterStatusCode)) {
    // 未完了案件の場合
    ActvMatterNode actvMatterNode = new ActvMatterNode(systemMatterId);
    //…処理
} else if (MatterStatus.matSts_Complete.toString().equals(matterStatusCode)) {
    // 完了案件の場合
    CplMatterNode cplMatterNode = new CplMatterNode(systemMatterId);
    //…処理
}

ここでは未完了/完了案件を対象としていますが、処理対象の案件を変えたい場合(過去案件も含めたい場合など)は、案件状態マネージャ (UserMatterStatus) のコンストラクタのパラメータ「searchLevel」を変更してください。

intra-martとkintoneを連携してみる

ワークフロー(intra-mart)の申請データをアプリ(kintone)に連携させてみました。

intra-martのLogic Designerとkintone REST APIを使い、ノンプログラミングで実現することができました。

今回はその際の手順を整理したいと思います。

やったこと

f:id:hamaguchiyu:20190628175059p:plain

1. kintone

1.1. kintoneアプリをつくる

kintoneのアプリを作ります。

f:id:hamaguchiyu:20190628165235p:plain

アプリのAPIトークンを生成します。アクセス権にもチェックを入れておきます。

f:id:hamaguchiyu:20190628180518p:plain

2. intra-mart

2-1. 申請画面をつくる (BIS)

BISでフォームを作成します。 f:id:hamaguchiyu:20190628180954p:plain

2-2. REST定義をつくる (Logic Designer)

Logic Designerの [ユーザ定義] > [REST定義新規作成] からREST定義を作成します。

入力値には下記を設定します。

  • apiToken、domain、app ・・・ kintoneに接続するための情報
  • record ・・・ kitoneに登録する情報 f:id:hamaguchiyu:20190628181335p:plain

リクエスト情報・レスポンス情報を設定します。 上記の入力値で定義した「domain」「apiToken」を使用して、エンドポイント・リクエストヘッダを設定します。

f:id:hamaguchiyu:20190628181456p:plain

2-3. ロジックフローをつくる (Logic Designer)

[ロジックフロー定義一覧] からロジックフローを新規作成します。

フローには上記で作成したREST定義(kintone_register1)を設定します。

f:id:hamaguchiyu:20190628182616p:plain

入出力設定、定数設定の設定を行います。

入力出力設定には申請画面の入力値(textbox1)を定義します。

定数設定にはkintoneの接続情報(apiToken、app、domain)を定義します。

f:id:hamaguchiyu:20190628182640p:plain

REST定義に入力、定数の値をマッピングします。

f:id:hamaguchiyu:20190628182657p:plain

2-4. 案件終了処理を設定する (BIS)

BISで [外部連携] > [データソース] よりデータソースを作成します。フロー定義には上記で作成したロジックフローを指定します。

f:id:hamaguchiyu:20190628182747p:plain

BISの[フロー編集]から、上記で作成したロジックフローを案件終了処理に指定します。

f:id:hamaguchiyu:20190628182815p:plain

以上で設定は完了です。

3. 実行結果

intra-martのワークフローを承認して案件を完了させてみます。

無事にkintoneアプリにintra-martのワークフロー申請情報が登録されました。

f:id:hamaguchiyu:20190628182928p:plain

Logic Designerとkintone REST APIを使えば、intra-mart x kintone連携も簡単に実現できますね。

AsciiDocの良いところ

開発部のS.Kです。 AsciiDocについて少し調べてみたので、共有してみたいと思います。

AsciiDocとは?

AsciiDocは、軽量のマークアップ言語です。 Markdownを知っている方は、それとよく似た言語と考えていただければ、分かりやすいと思います。 知名度で言えばMarkdownの方が圧倒的に高いですが、AsciiDocにはMarkdownには無い機能がいくつかあります。

AsciiDocの利点

表の幅広い記述・表現が可能

AsciiDocは表を色々な記法で記述できます。 Markdownと同じような罫線風の記述も出来ますし、csvのようなカンマ区切りの記述も出来ます。 表示に関しても、セル内での改行やセル結合も可能です。 Markdownでも表は描けますが、あまり細かい記述はできません。

外部ファイルを文書内に埋め込める

include構文を使うことで、外部ファイルを文書に埋め込むことができます。 これは画像の埋め込み表示とは異なり、AsciiDocで書かれた文書をその位置に差し込むように利用することが出来ます。 これを用いることで、複数の文書で共通する記述を共有したりできます。 また、エクステンションを追加すればplantUMLで記述したUMLを表示することもできるので、そういったものを読み込むのも効果的です。

まとめ

AsciiDocを総評すると、Markdownをより高機能かつシステマチックにした感じの言語です。 Markdownで文書管理したいけど機能が少し物足りない、という方はAsciiDocも検討してはいかがでしょうか。 AsciiDocを記述する専用テキストエディタとしては、AsciidocFXなどがあります。 それ以外にも、Atom, VSCode, SublimeTextなどの有名エディタに拡張機能がそれぞれ存在するので、試してみるのも良いと思います。

初心者の小石_データベースとストレージの関係

はじめに

お久しぶりの投稿となります、G.Mです。
まだまだ寒い日が続きます。関東でも積雪があり、この時期に入ってもまだまだ厳しい冬が続きます。
皆さま、日々業務や学業がお忙しいところかとは存じますが、体調管理は万全にしていきましょう。

今回は表題の通り、intra-martにおける『データベースとストレージの関係』についてご紹介させていただきます。intra-martにおいては基本的な内容であり、公式ドキュメントにも記載されてはいるのですが、非常につまづきやすい内容だと個人的に思っており、今回取り上げようと思い立ちました。

データベースとストレージの概要

まず、データベースとはなに? ストレージとはなに? というあたりについては、皆さますでにご存知と思われます。もし理解できているかどうか自信がないという方は、この機会に押さえておきましょう。

intra-martにおいても、もちろんデータベースとストレージは利用されています。そこで、いったい何につまづきやすいのかというと、それらの関係性を見落としやすいという点です。

多くのシステムにおいて、必要な情報はデータベースに保管することでしょう。実際にintra-mart環境を構築するにあたっても、PostgreSQLOracleSQLServerといったデータベースを用意していると思われます。

では、ストレージについてはどうでしょう。intra-martをセットアップする際に保存先を指定するのですが、デフォルト指定のままでも問題なく動作するため、あまり意識していない場合があるかもしれません。
というより、intra-martに初めて触れ、とりあえず環境を構築してみようと試みている方々の大半は、まったく意識していないのではないでしょうか。

データベースとストレージの関係

データベースとストレージの関係性を見落としやすい、と前述しました。実は、intra-martにおいてデータベース(テナント)とストレージは原則1対1の関係になっています。intra-mart上のデータを、データベースとストレージそれぞれに分割して保存している、というイメージです。

データベースとストレージ
データベースとストレージ
つまり、もしもデータベースの中身を削除して新しい環境を作りたいと思ったなら、ストレージも完全に削除し、新しく作り直す必要があるわけです。

ストレージの存在を意識していないと、そのことを見落としてしまいます。
環境を再作成するのに、データベースは削除したけれどストレージはそのままだとか。
環境を別にもう一つ作ろうとしているのにストレージの保存先を変更せず、2つの環境から同じストレージを参照してしまっているとか。
現環境を別の環境に移行するにあたり、データベースの中身は移したけれど、ストレージを移していなかったとか。
そうして、システムが起動しなくなってしまった、ワークフローのデータが表示されなくなってしまった、よくわからないけど何かがおかしい、という事態に陥った経験をお持ちの方は多いのではないでしょうか。

マニュアル通りにやったのに壊れた
マニュアル通りにやったのに壊れた

実際にどのデータがデータベースに保存され、どのデータがストレージに保存されるか、というのをすべて洗い出すのは非常に困難です。
また、intra-martのバージョンが進むことで、保存先が変更されたり、設定で任意に変更できるようにもなっています。たとえば、ワークフローについては下記を参照してください。

https://www.intra-mart.jp/document/library/iap/public/im_workflow/im_workflow_specification/texts/setting_guide/setting_list/tenant_unit/setting_guide_18.html

もちろん、大切なデータがどこに保存されているかを把握しておくのは大事なことです。しかし、とにかく覚えておかなければならないことは、データベースとストレージは対になっている、という一点です。これを忘れないだけでも、環境構築における事故はぐっと減るのではないでしょうか。
少なくとも、現場に新規参入した新人社員さんなどには、まず先に教えてあげたい内容ですね。

テナントが複数あったり、バーチャルテナントを利用していたり、あるいは分散環境だったり。場合によって、データベースとストレージの関係もかなり混み行ってきます。ですので運用にあたっては、この2つの扱い方を明確にしておくことを強くお勧めします。

おわりに

私が初めてintra-martに触れたとき、その環境セットアップにとても苦労したことを覚えています。
何度も失敗し、原因不明のエラーに悩み、しばらくそうしているうちにようやく、仕組みが分かるようになっていきました。
そういった経験やノウハウはぜひとも形に残して、後続の人たちの手助けとなることを祈ります。

受け継がれるナレッジ
受け継がれるナレッジ

新年度もあと少し。4月入社する新人さんたちには、少しでも早く実力をつけ、私に楽をさせて欲しい会社に貢献できる人材に成長して欲しいですね!

JDK11へのバージョンアップについて

開発部のS.Kです。 だいぶ久しぶりの投稿となります。

今回はJDKの移行で少々苦戦したので、その話を少々。 OracleJDKのリリースサイクルを変え、長期サポート版が無償では公開されなくなるという話題が、ずいぶん前からありました。 実際に、OracleJDK8の無償公開版の更新は、先月(2019年1月)で停止しています。 そのため、OpenJDK11への移行を行ってみました。

所感としては、OracleJDKからOpenJDKにすることでの問題はあまり無さそうです。 ただ、そもそもJDKを11へバージョンアップするというところでは、色々と気をつけなければいけない感じです。

JDK11では、非推奨となっていた様々なモジュールが削除されています。 個人的に一番影響がありそうに思えるのは、JAXBでしょうか。 スキーマが定義された設定ファイルを、Javaプログラムから簡単に読み書きするためのモジュールです。 JDKに同梱されなくなっただけなので、別途追加することも出来るのですが、そうするとクラスローダー絡みと思われる問題が起きることがありました。 これまではJava本体と同時に読み込まれていたクラスが、もう少し後のタイミングで読まれることになるため、思わぬところでClassNotFoundExceptionが発生したりします。 クラスの読み込みに関しては結構Javaの深い知識が要求されるので、原因を調べるのが大変でした。

今後もJDKのバージョンアップは定期的に必要となる作業なので、この知見を今後に活かしたいと思います。

FormaDesignerで画面の見栄えをよくしてみた

intra-mart Accel Platform 2018 Winter(Urara) でFormaDesignerのアイテムに「iAP UIデザインモード」機能が追加されたので早速試してみました。

主に入力アイテム、共通マスタアイテムが対応されているようです。

www.intra-mart.jp

今回やったこと

社内で運用しているBISで作成した申請書の入力項目(アイテム)を「iAP UIデザインモード」に設定していきました。

設定後はこんな感じです。

見栄えがすごくよくなりました!

f:id:hamaguchiyu:20190119142201p:plain

設定方法

設定はいたって簡単で、アイテムのプロパティで「iAP UIデザインモード」のチェックをONにするだけです。

f:id:hamaguchiyu:20190119132549p:plain

注意点としては、設定した際に若干アイテムのサイズが広がってしまうのと、表示位置が最前面になってしまうため、設定した後にアイテム配置の調整が必要です。

画面に配置したアイテムの数が多いとかなり骨の折れる作業になりそうです。(今回は数が少なかったのでそこまで大変ではありませんでしたが。。)

所感

他の申請書やFormaアプリも「iAP UIデザインモード」に対応していきたいと思います。

設定をアイテムごとに地道にやっていくしかなさそうなので、 システム全体で「iAP UIデザインモード」のデフォルト設定をONにできたり(→forma-config.xmlの<ui-theme-mode>で設定できるようです)、画面単位で一括ON/OFF設定できたりする機能があればいいな、と思いました。

intra-martの隠しコマンド?

はじめに

みなさまこんにちは、またしてもG.Mです。
外は随分と涼しくなってきました。早々秋物に着替えた方も多いのではないでしょうか。
9月も半分を回り、平成最後の年末が見えてくる頃合です。
これからも何かと『平成最後の××』が盛りだくさんですので、張り切っていきましょう。

さて今回は、intra-martのワークフロー開発で役に立つ豆知識を一つご紹介します。
intra-mart技術者、特にintra-mart開発を最近始めた方向けのテーマです。

【前置き】システム案件IDとユーザデータID

初めてintra-martのワークフローに触れるという方が、まずぶつかるがこの聞き慣れない単語『システム案件ID』ではないでしょうか。
さらに『ユーザデータID』というものもあって、頭の中でごっちゃになりやすい要素ですね。

いずれも、ワークフローを起票・申請したとき、その案件を特定するために用いられる一意のIDです。
『システム案件ID』と『ユーザデータID」は1:1の関係にあり、 どちらか一方だけあれば、案件について欲しい情報を引き出すことができます。

このとき、それぞれの用途は以下のようになっています。

  • システム案件ID(SystemMatterId):ワークフロー側が採番し、システム内部でのみ利用するID
  • ユーザデータID(UserDataId):ユーザコンテンツ側が任意のタイミングで採番し、業務データとの関連付けなどに用いるID

二つのIDの関係イメージ
二つのIDの関係イメージ

システム開発において、ワークフローと連携する業務テーブルの外部キーとして用いるのは『ユーザデータID』である、と覚えておけば、ひとまず問題ありません。

逆に、intra-martがもともと持っているテーブルからデータを取得する場合には『システム案件ID』をキーとして利用します。
APIの引数などでしばしば要求されるかと思われます。

システム案件IDを知りたい

システム案件IDがあれば、案件の情報はなんでも取得できます。
しかし、intra-martの標準画面を見る限り、システム案件IDという項目は見当たりません。
もちろんデータベースをのぞけば分かりますが、それではあまりに手間ですね。

そこで今回は、簡単にシステム案件IDを確認する方法をご紹介します。

intra-martの隠しコマンド

intra-martの標準画面である『未処理一覧画面』を開いてください。

未処理一覧画面
未処理一覧画面
この状態で、キーボードから『Ctrl + Shift + i』を入力してみましょう。
隠しコマンド入力後イメージ
隠しコマンド入力後イメージ
一覧の一番左に、システム案件IDを表示する列が現れました。
また、『Ctrl + Shift + o』を押すと元に戻ります。

簡単にシステム案件IDを確認できましたね!
これは開発の中で非常に重宝するのではないでしょうか。
同じコマンドを使って、他の一覧(一部を除く)でも同様の操作が可能です。

intra-martの隠しコマンド?

このコマンドは、intra-martをご存じな方でも、あまり知る機会のないものではないでしょうか。
隠しコマンドと言いつつ、実は公式ドキュメントにも載ってはいるのですが、
有用性の割に、トラブルシューティングの片隅にTipsとして記されているのみです。

intra-mart IM-Workflow トラブルシューティング
https://www.intra-mart.jp/download/product/iap/im_workflow/im_workflow_troubleshooting/texts/assemble_information/index.html#tips

それから、確認した限りでは、intra-mart開発者ブログの昔の記事にて紹介されていたりしました。

intra-mart Developer Site
https://dev.intra-mart.jp/intra-mart_advent_calendar_2013_14im-workflow/

もう五年も前の記事ですか……。
2013年ということは、『intra-mart Accel Platform』の初期には既に存在していたコマンドなのですね。

Tips
intra-mart Accel Platform(iAP)は、2012年秋からの呼称となります。
以降、春夏秋冬(後に秋がなくなる)のシーズンごとにバージョンアップする今のスタイルに変わりました。

もちろん、現時点での最新バージョン(2018Summer)でもこのコマンドは有効です。

いずれにせよ、intra-martのガイドラインを隅々まで読破していないと、あとあとまで気づけない可能性は大いに有り得そうです。

おわりに

intra-mart開発ブログいわく、今回ご紹介したようなコマンドは他にもあるようです。
ゲームの裏技を探すかのごとく、片っ端から調べてみるのも面白いかもしれません。
が、そういった調査に割ける時間もなかなかありませんし、ぱぱっと一覧にして見せてもらえたらうれしいナ、というのが正直なところですね。

それでは、今回はこれまで。
時節柄、体調も崩しやすいかと思われますので、みなさまくれぐれもご自愛くださいませ。