GrafanaとPostgreSQLをDocker Composeで動かしたときに接続先の設定でハマった

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 連休中にGrafana + PostgreSQLをDocker Composeで動かしたときになかなか連携できずにハマってしまいました。 そのときに起きた問題点と解決策を共有します!

導入

今回使用したdocker-compose.ymlファイルです。 GrafanaとPostgreSQLを使用しています。

version: '3'
services:
  grafana:
    image: grafana/grafana
    container_name: sample_grafana
    ports:
      - 8888:3000
    volumes:
      - ./grafana:/var/lib/grafana

  postgres:
      image: postgres:12.7
      container_name: sample_postgres
      ports:
        - 5432:5432
      environment:
        POSTGRES_USER: sample_user
        POSTGRES_PASSWORD: sample_pass
      volumes:
        - ./initdb:/docker-entrypoint-initdb.d
        - ./postgres:/var/lib/postgresql/data

GrafanaのConfiguration > Data Sources > Add data sourceから接続先の設定をすることができます。 その際、ホストをlocalhost:5432で設定すると接続することができませんでした。

結論

f:id:excite-kazuki:20210805231418p:plain

docker-compose.ymlにあるcontainer_nameをホストに記述する必要があります。 Dockerコンテナを使用している場合は、localhostのままでは繋がらないため注意しなくてはなりません。

上記のとおり接続先の設定ができると、PostgreSQLに格納したデータを可視化することができるようになります!

f:id:excite-kazuki:20210806004454p:plain
Grafanaでメモリ使用量を可視化

Djangoで複数アプリケーション構成にするときのTips

iXIT株式会社 小長谷です。

DjangoはWebアプリの各種設定情報を管理するための仕組みとして、「プロジェクト」を使用します。さらに「プロジェクト」内で、Webアプリのためのモジュールを作成します。

1つのアプリケーションに対し1つのフォルダが作成されるため、複数のアプリケーションを作成する場合管理がしづらいと考え、新たにappsフォルダを作成してまとめる構成で開発を行いました。
また、ビュー用のtemplateファイルもtemplatesフォルダに、その他静的ファイルもstaticフォルダにまとめました。

開発時にsettings.pyに追加するものなど、つまずいた箇所を記載していきます。

バージョン

Python: 3.9.6
Django: 3.2.5

プロジェクト、アプリケーションを作成

プロジェクト名を仮に、bookProjectという名前にします。
django-admin startproject bookProject
cd bookProject
startappコマンドでフォルダを指定する場合、予めフォルダを作成する必要があります。
mkdir -p apps/book
python manage.py startapp book apps/book
アプリケーションを追加する場合、上記のようにapps配下に作成していきます。

フォルダ構成図

templatesフォルダ、staticフォルダを作成した後の全体図は以下のようになります。

├── bookProject
│   ├── __init__.py
│   ├── __pycache__
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── apps
│   ├── book
│       ├── __init__.py
│       ├── __pycache__
│       ├── admin.py
│       ├── apps.py
│       ├── migrations
│       ├── models.py
│       ├── tests.py
│       └── views.py
├── db.sqlite3
├── manage.py
├── static
│   └── css
│       └── base.css
└── templates
    ├── base.html
    └── book
        └── index.html

settings.py

bookProject/settings.py

# 追加
import os

# ~~ 途中省略 ~~

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # 追加
    'apps',
]

# ~~ 途中省略 ~~

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [
            # 追加
            os.path.join(BASE_DIR, 'templates'),
        ],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

# ~~ 途中省略 ~~

STATIC_URL = '/static/'
# 追加
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static'),)

ルーティング

bookProject/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('book/', include('apps.book.urls')),
]

bookフォルダ内にurls.pyを作成します。
apps/book/urls.py

from django.urls import path
from . import views

app_name = 'book'

urlpatterns = [
    path('', views.IndexView.as_view(), name='book_index'),
]

ビュー

apps/book/views.py

from django.shortcuts import render
from django.views.generic import TemplateView


class IndexView(TemplateView):
    template_name = 'book/index.html'

テンプレート

templates/base.html

{% load static %}

<!-- 省略 -->

<link rel="stylesheet" href={% static 'css/base.css' %}>

templates/book/index.html

{% extends 'base.html' %}

<!-- 省略 -->

<a href={% url 'book:book_index' %}>bookへ</a>

javaのSpringBootでパラメーターの日付をLocalDateTimeで受け取る方法

エキサイトのエンジニア藤沼です。 JavaのSpringBootにて、Getパラメーターで日付をLocalDateTimeで受け取る方法の覚書です。

やりたい事

下記のようなAPIにて、日付パラメータをLocalDateTimeで受け取りたい

/api/sample/?start_date_to=20201010000000

何故か取れない

@GetMapping("sample")
public Mono<LocalDateTime> sample(
        @ModelAttribute @Validated SampleForm sampleForm, BindingResult bindingResult
) {
    return Mono.defer(
            () -> {
                return Mono.just(sampleForm.getStartDateTo());
            }
    ).subscribeOn(Schedulers.boundedElastic());
}
@Data
public class SampleForm {

    /**
     * 開始日時to
     */
    private LocalDateTime startDateTo;

    @ConstructorProperties({
            "start_date_to"
    })
    public SampleForm(
        LocalDateTime startDateTo

    ) {
        this.startDateTo = startDateTo;
    }
}

実行しても何故か日付はnullになる

/api/sample/?start_date_to=20201010000000

f:id:aya_excite:20210804155117p:plain

※捕捉 ConstructorPropertiesは、 getメソッドでパラメーターをどう受け取るかを指定できます。

LocalDateTimeで受け取るには@DateTimeFormatが必要だった

@GetMapping("sample")
public Mono<SampleListResponseModel> sample(
    @ModelAttribute @Validated SampleListForm sampleListForm
) {
@Data
public class SampleListForm {

    /**
     * 開始日時to
     */
    private LocalDateTime startDateTo;

    @ConstructorProperties({
        "start_date_to"
    })
    public SeriesListForm(
        //↓このアノテーションをつける
        @DateTimeFormat(pattern = "yyyyMMddHHmmss")
        LocalDateTime startDateTo

    ) {
        this.startDateTo = startDateTo;
    }
}

日付が受け取れるようになった

/api/sample/?start_date_to=20201010000000

f:id:aya_excite:20210804155755p:plain

捕捉:一つずつパラメーターを受け取る場合
@GetMapping("sample")
public Mono<SeriesListResponseModel> sample(
    @RequestParam("start_date")
    @DateTimeFormat(pattern = "yyyyMMddHHmmss")
    LocalDateTime startDate
) {

RDSのパラメータグループが「デフォルト」に戻せない原因?

こんにちは。 エキサイト株式会社の三浦です。

みなさん、Amazon RDSは使っているでしょうか? RDSでは「パラメータグループ」という機能を使ってDBのパラメータを変更するのですが、今回はそのパラメータグループに関するちょっとしたTipsを書いていきます。

Amazon RDS とは

Amazon RDS は、AWSが提供しているマネージドなDBサービスです。

Amazon Relational Database Service (Amazon RDS) を使用すると、クラウド上のリレーショナルデータベースのセットアップ、オペレーション、スケールが簡単になります。ハードウェアのプロビジョニング、データベースのセットアップ、パッチ適用、バックアップなどの時間がかかる管理タスクを自動化しながら、コスト効率とサイズ変更可能なキャパシティーを提供します。

DBである以上 sort_buffer_size などの細かいパラメータも変更できる必要があるのですが、これはRDS内の「パラメータグループ」という機能によって提供されています。

f:id:excite-takayuki-miura:20210802112656p:plain
RDSのパラメータグループ

各種パラメータには最初はデフォルト値が設定されていて、このパラメータグループ上で値を変更することで変更値をDBに反映させることができます。

パラメータに変更を加えた後でもデフォルト値に戻すことができるのですが、実はたまに戻らないことがあります。

デフォルト値に戻らない原因?

ある日、パフォーマンステストのためにいくつかのパラメータを変更し、テストが終わった後にデフォルト値に戻そうとしたら一部パラメータが戻らない事がありました。 Webコンソール上ではエラーが起きている様子はなく、DB自体にもエラーは見受けられないのですが、パラメータはデフォルト値に戻っていないのです。

その後、色々な検証をしていく中で一つの可能性に行き当たりました。

RDSにはステータスがあり、通常は「利用可能」となっています。

f:id:excite-takayuki-miura:20210802113545p:plain
ステータス

パラメータグループでパラメータを変更すると、これが変更中を示す状態になるのですが、どうやら変更中にデフォルト値に戻そうとすると正しく戻ってくれないようです。

一見当然のことのように思えるのですが、

  • パラメータを変更した画面からはすぐにはDBのステータスが見られない
  • デフォルト値に戻すのに失敗しても、戻っていないのは確認できるが原因は表示されない
  • 場合によっては、パラメータを変更しても変更中を示す表示にならないことがある?
    • 変更自体は終わっているが、裏側でまだ副次的な処理が続いているとか?

ために見落とすことが意外と多いのではないでしょうか。 こういった場合は、しばらく時間をおいて再度実行してみると良いでしょう。

最後に

今回書いた理由以外にも、もしかしたら戻らない理由があるかもしれないのでご了承ください。

なお、もしTerraformで管理している場合は、Terraformから変更すればそのあたりも込み込みで変更してくれるのか、こういったことを気にすることなくデフォルト値に戻ります。 限定的な場面ですが、

  1. Terraformで管理しているRDSについて
  2. テストのために手動でパラメータグループをデフォルト値から変更して
  3. いざ手動で戻そうとしたら戻らなかった

場合は、Terraformから戻してみるのも手かと思います。

length()の値が違う

エキサイト株式会社 メディアサービスエンジニアのしばたにえんです。
早速以下のコードを見てください

    void getStringLength() {
        String str = "𩸽";
        System.out.println(str.length());
    }

実はこの結果は2です。 原因はサロゲートペアにあります。

サロゲートペアとは

通常、Unicodeでは1文字あたり2バイトのデータ量を使います。 2バイトですから65536通り(0x0000~0xFFFF)のビットを表現できます。 この約6万字で世界中の文字を表現しようというのがUnicodeの本来の思想でした。 ところが、近年、Unicodeに組み込みたいという文字の要望がいろいろと増えてきました。 結果的に従来の2バイト(65536文字)では文字が足りない状況になってしまったのです。 そこで、解決策としてサロゲートペアという方法が導入されました。 「1文字=2バイト」の基本は維持しつつ、一部の文字については「1文字=4バイト」にする方法です。

    void getCharLength() {
        String str = "𩸽";
        System.out.println(Integer.toHexString(str.charAt(0)));
        System.out.println(Integer.toHexString(str.charAt(1)));
    }
// 実行結果
d867
de3d

"𩸽"は見た目上は1文字ですが、サロゲートペア文字で4バイトで表した文字でchar2つで表されます。length()はchar1つにつき1とカウントされるため値が違ったのです。

本当の文字数を取得する

    void getRealLength() {
        String str = "𩸽";
        System.out.println(str.codePointCount(0, str.length()));
    }
// 実行結果
1

codePointCountメソッドでは正しく1とカウントできていることが確認できます。 codePointCountメソッドでは、文字列をカウントする際の開始/終了位置が必須なので、文字列全体をカウントするには、それぞれ0(先頭)、str.length(末尾)を指定しています。

length()を使う際は気をつけましょう。

Nim言語を使って簡単に文章の類似度を計算してみる

皆さんは「Nim」という言語をご存知でしょうか?
Nimは「Pythonをブラッシュアップして、秘伝の悪魔のタレをかけたような感じ」と比喩されるような言語です。
そしてNimはC言語などにコンパイルされ、C言語などのコンパイラを使ってバイナリにコンパイルされます。
そんなNim言語を触っていきましょうか。

Nim言語について

公式サイトは以下です。
https://nim-lang.org/

文法はPythonに影響を受けており、インデントが意味を持ついわゆる「オフサイドルール」というものを採用しています。
また静的型付けです。

Hello World

とてもシンプルです。

echo "Hello World"

コンパイル

こちらも簡単です。
以下のような形になります。

$ nim c -r 保存ファイル名

nim cで「C言語コンパイル」を意味します。
-rコンパイル後即実行です。これを省略すると、バイナリコンパイルだけが行われます。

蛇足ですが、nim cC言語に、
nim cppC++に、
nim objcObjective-Cに、
nim jsJavaScriptコンパイルされます。

遊んで見る

少し自然言語処理のようなことをして遊んでみましょうか。
n-gramを自作してみましょう。

自然言語におけるn-gram

n-gramのnは変数です。
例えば、uni-gram、bi-gram、tri-gram、4-gram…のような感じになります。

例を見てみましょう。
例文:「今日はいい天気」

uni-gram

uni-gramでは以下のように分解されます。

今
日
は
い
い
天
気

bi-gram

bi-gramでは以下のように分解されます。

今日
日は
はい
いい
い天
天気

このような感じになります。

n-gramを作成するプログラムを書いてみる

以下のコードをtest.nimとして保存します。

import unicode

proc createNGram*(n: int, text: string): seq[string] =
    ##
    ## n-gramデータを作成します
    ## 
    ## n: n-gramのnに当たる数値
    ## text: n-gramに分解(コーパス)したい文字列
    
    # マルチバイト文字列を扱えるようにテキストをルーン化する(Goとかにもある、アレ)
    let runeText = text.toRunes()

    # 何文字目まで連結させたかを保持しておく変数(Runeは1文字ずつ扱うため、indexでどこまで扱ったかをカウントしておく)
    var index = 0
    # n-gramで何文字ずつの文字列(コーパス)にするかを決めるためのカウント変数
    var cnt = 0
    # n-gramでの文字列を作成する際に利用するtmp変数
    var tmp: string

    while true:
        # cntがn-gramの指定文字数を超えたらそこで切り出す(安全のため<=としている)
        if n <= cnt:
            # resultは暗黙変数(nimでは返り値を定義すると自動的にresult変数が生成される)
            result.add(tmp)

            tmp = ""
            cnt = 0

            # n-gramの特性上一つ前の文字をもう一度利用するので、n-1をしている
            index = index - (n - 1)
            
        
        if text.runeLen() <= index:
            break
        
        # 1文字ずつ連結していく
        tmp = tmp & $runeText[index]

        cnt = cnt + 1
        index = index + 1

echo createNGram(2, "今日はいい天気")

コマンドプロンプトやターミナルで以下のコンパイルコマンドを実行すると、
「今日はいい天気」という文字列がn-gramに分解されて表示されると思います。

$ nim c -r test.nim

@["今日", "日は", "はい", "いい", "い天", "天気"]

いい感じですね!
コメントにも書きましたが、このように分割したものを「コーパス」と呼んだりします。

さて、続きはこのコーパスをどのように利用していくかを見ていきましょう。

TF

Term Frequencyといえばなんかかっこよさげですが、意味は「その単語の出現回数」です。
「今日」という単語が何回出てきた?「天気」という単語が何回出てきた?
そんな意味です。

ではNimでそれを計算してみましょうか。 test.nimを書き換えます。

import unicode
import tables

proc createNGram*(n: int, text: string): seq[string] =
    ##
    ## n-gramデータを作成します
    ## 
    ## n: n-gramのnに当たる数値
    ## text: n-gramに分解(コーパス)したい文字列
    
    # マルチバイト文字列を扱えるようにテキストをルーン化する(Goとかにもある、アレ)
    let runeText = text.toRunes()

    # 何文字目まで連結させたかを保持しておく変数(Runeは1文字ずつ扱うため、indexでどこまで扱ったかをカウントしておく)
    var index = 0
    # n-gramで何文字ずつの文字列(コーパス)にするかを決めるためのカウント変数
    var cnt = 0
    # n-gramでの文字列を作成する際に利用するtmp変数
    var tmp: string

    while true:
        # cntがn-gramの指定文字数を超えたらそこで切り出す(安全のため<=としている)
        if n <= cnt:
            # resultは暗黙変数(nimでは返り値を定義すると自動的にresult変数が生成される)
            result.add(tmp)

            tmp = ""
            cnt = 0

            # n-gramの特性上一つ前の文字をもう一度利用するので、n-1をしている
            index = index - (n - 1)
            
        
        if text.runeLen() <= index:
            break
        
        # 1文字ずつ連結していく
        tmp = tmp & $runeText[index]

        cnt = cnt + 1
        index = index + 1


proc tf*(corpus: seq[string]): Table[string, int] =
    ##
    ## コーパスの中のTFを計算します
    ## 
    ## corpus: コーパスが格納されたseq配列を指定します

    for c in corpus:
        # 連想配列にその単語があれば1加算、なければその連想配列のキーを作成し、1を代入
        if result.hasKey(c):
            result[c] += 1
        else:
            result[c] = 1


# コーパスを取得
let corpus = createNGram(2, "今日はいい天気")

# TF値を計算
let tfTable = tf(corpus)
echo tfTable

これを先ほどと同じようにnim c -r test.nimコンパイルすると以下のような出力を得ることができます。

{"いい": 1, "はい": 1, "い天": 1, "日は": 1, "天気": 1, "今日": 1}

コーパスの出現回数が取得できました。
全部出現回数は1ですね。
そして順番は担保されていませんが、今回は順番は関係ないのでこのまま進めます。

文章の類似度を求める

2つの文章の類似度を求めてみましょう。
ここではメジャーなコサイン類似度を利用してみましょうか。
コサイン類似度の他にも、ユークリッド距離、マンハッタン距離などでも求めることができます。

コサイン類似度とは?

ざっくり説明します。本当にざっくりと。

コサイン類似度はその名の通りコサインを利用します。
コサインは、角度が0度に近づけば1、90度に近づけば0になるという特性があります。

要は2つの要素が近ければ(似ていれば)1に近づき、
離れていれば(似ていなければ)0に近づくといえます。

詳しくは数学系のサイトを見てみると良いでしょう。

コードを改変

以下の様にコードを改変していきます。

import unicode
import tables
import math

proc createNGram*(n: int, text: string): seq[string] =
    ##
    ## n-gramデータを作成します
    ## 
    ## n: n-gramのnに当たる数値
    ## text: n-gramに分解(コーパス)したい文字列
    
    # マルチバイト文字列を扱えるようにテキストをルーン化する(Goとかにもある、アレ)
    let runeText = text.toRunes()

    # 何文字目まで連結させたかを保持しておく変数(Runeは1文字ずつ扱うため、indexでどこまで扱ったかをカウントしておく)
    var index = 0
    # n-gramで何文字ずつの文字列(コーパス)にするかを決めるためのカウント変数
    var cnt = 0
    # n-gramでの文字列を作成する際に利用するtmp変数
    var tmp: string

    while true:
        # cntがn-gramの指定文字数を超えたらそこで切り出す(安全のため<=としている)
        if n <= cnt:
            # resultは暗黙変数(nimでは返り値を定義すると自動的にresult変数が生成される)
            result.add(tmp)

            tmp = ""
            cnt = 0

            # n-gramの特性上一つ前の文字をもう一度利用するので、n-1をしている
            index = index - (n - 1)
            
        
        if text.runeLen() <= index:
            break
        
        # 1文字ずつ連結していく
        tmp = tmp & $runeText[index]

        cnt = cnt + 1
        index = index + 1


proc tf*(corpus: seq[string]): Table[string, int] =
    ##
    ## コーパスの中のTFを計算します
    ## 
    ## corpus: コーパスが格納されたseq配列を指定します

    for c in corpus:
        # 連想配列にその単語があれば1加算、なければその連想配列のキーを作成し、1を代入
        if result.hasKey(c):
            result[c] += 1
        else:
            result[c] = 1

proc cosineSimilarity*(text1: string, text2: string, ngramNum: int): float =
    ##
    ## 文章の類似度を調べます
    ## 
    ## text1: 1つ目の文章
    ## text2: 2つ目の文章
    ## ngramNum: 何gramにテキストを分解するか
    ##

    # 文章をそれぞれコーパスに分解します
    let text1Copus = createNGram(ngramNum, text1)
    let text2Copus = createNGram(ngramNum, text2)

    # text2のTF値を求めます
    let text2Tf = tf(text2Copus)

    # コサイン類似度の計算に必要な分子分母の変数
    var c = 0.0
    var m1 = 0.0
    var m2 = 0.0

    for t1c in text1Copus:
        # text2のコーパスにtext1のコーパスがあるかないかで類似度を計算することにします
        # text2のコーパスにtext1のコーパスがあれば1、なければ0を使います
        var n = 0.0
        if text2Tf.hasKey(t1c):
            n = 1.0
        
        # コサイン類似度に利用する分子分母の数値を計算
        c += (1 * n)
        m1 += 1 * 1
        m2 += n * n
    
    # コサイン類似度の計算
    if m1 == 0 or m2 == 0:
        return 0
    result = c / round(sqrt(m1) * sqrt(m2))

# 2つの文章のコサイン類似度を計算する
# ------------------------------------

# bi-gramでは同じ文章なので完全一致(1.0)
echo cosineSimilarity("今日はいい天気ですね", "今日はいい天気ですね", 2)

# bi-gramでは文章の構成ワードが一緒なので完全一致になる(1.0)
echo cosineSimilarity("今日はいい天気ですね", "いい天気ですね今日は", 2)

# bi-gramでは土地名が違うだけなのでやや類似(0.8)
echo cosineSimilarity("渋谷でお買い物", "新宿でお買い物", 2)

# bi-gramでは完全に違う文章(0.0)
echo cosineSimilarity("スイカは果物", "ピーマンは嫌い", 2)

これで文章の類似性をざっくり出すことができました。
もっと丁寧に文章を解析する手法として「形態素解析」や「構文解析」などを用いる方法があります。
これをやると精度が上がります。

今回は入門ということでn-gramを使ってみましたが、
機会があればやってみたいですね。

終わりに

ちょっと詰め込みすぎましたが

  • Nim言語は良いぞぉ!
  • n-gramを使って文章の類似度を出す

ということを行いました。
これを機会に、よかったらNim言語に手を出してみてください!

それでは今回はこのへんで。
また次回!

第2回定期勉強会「機械学習勉強会」

エキサイトの たからだ です。

先月から社内定期勉強会が弊社で始まりました(前回の様子はこちら)。第2回の今回はAWSスペシャリストの方に機械学習勉強会」をリモートで実施していただきました!

内容

機械学習初学者を対象に座学とハンズオンを実施していただきました。
座学では以下のような機械学習の基礎と AWS で利用する機械学習のサービス概要について学びました。

ハンズオンでは、 Amazon SageMaker のノートブックインスタンス環境と深層学習フレームワークの Tensorflow を利用し、MNIST とよばれる手書き数字の画像データセット機械学習のライブラリである scikit-learnで画像分類を行いました。

f:id:excite-takarada:20210727124110p:plain

k-近傍法(k-nearest neighbor)や決定木(Decision Trees)等の機械学習の手法をプログラムを書きながら試し、精度の評価も行いました。

ハンズオンの後半では、深層学習 (Deep Learning) への入り口としてニューラルネットワークと畳み込みニューラルネットワークについても実際に手を動かして試しました。

全体的な感想

個人的な勉強会の感想としては、座学で機械学習の全体像やユースケースを丁寧に説明していただき、ハンズオンではユースケースの一例として画像分類を試すという流れだったので、内容を理解しながら進めることができたと思いました。

他にも勉強会後のアンケートでは以下のような声がありました。

  • 興味はあったものの手をつけられていなかったのでハンズオンで体験できてよかった
  • ハンズオンだけだと、なんとなくわかった気になってそこで満足して終わりというのが多いので、座学からのハンズオンという流れが良かった
  • 内容がとても丁寧で、この時間集中して受ければ機械学習自体の勉強にもなり、AWSの機能がどんな感じかざっくり把握できた
  • 具体的な箇所まで教えてもらえたので、実際の仕事などで使うための足がかりとすることができた

ハンズオンができてよかったという声が多く、未知のことを学ぶときはハンズオンが大事ということを再認識させてもらいました。参加していただいた方がAWSを使って機械学習を試してみるという土台はできたかなという印象です。
また、座学やハンズオンでの質問タイムでは時間いっぱいまでスペシャリストの方に質問していて有意義な時間になったと感じました。

勉強会の内容とは直接関係ないですが、今回の勉強会の構成は1.5時間の座学と3時間のハンズオンでした。両方を1日で実施すると業務的に参加できない人が出てくると見込み、座学とハンズオンは週をまたいで実施していただきました。参加人数は座学が29人、ハンズオンが18人で座学だけ参加していただいた方もいたので、座学とハンズオンを週またぎで実施した試みも悪くはなかったかなと思いました。

最後に

機械学習初学者向けにとても分かりやすい座学とハンズオンで、機械学習は何も分からない状態から脱出することができました。
勉強会を調整していただいた日頃からお世話になっているSAの方、今回登壇していただいたスペシャリストの方に感謝申し上げます。
引き続き、社内の定期勉強会のレポートを紹介していきますので見ていていただけると幸いです。

ThymeleafをController以外の場所で使う方法

こんにちは。 エキサイト株式会社の三浦です。

JavaでSpring Bootを使っている時にHTMLを扱いたい場合、Thymeleafというテンプレートエンジンを使うことが多いのではないでしょうか。 多くの場合はControllerから返却するときに自動でテンプレートファイルを紐付ける方法で事足りると思いますが、まれにController以外の場所で明示的に呼び出して使いたい場合もあります。

今回は、その方法を紹介します。

Thymeleafとは

Thymeleafは、Java用のテンプレートエンジンです。

Thymeleafは、ウェブとスタンドアローンどちらの環境でも利用できる、モダンなサーバーサイドJavaテンプレートエンジンです。HTML、XMLJavaScriptCSS、さらにプレーンテキストも処理することができます。

例えばHTMLを扱いたい場合、変数部分以外をテンプレートファイルとして最初から作っておき、最後に変数部分をコード側から注入することで、コード内で文字列からHTMLを組み立てるよりも圧倒的に見通しの良いコードにすることができます。

<div xmlns:th="http://www.thymeleaf.org">
    <span th:text="${title}"></span>
    
    <span th:text="${story}"></span>
</div>

通常使う場合は、Controllerからの返却時に自動的にテンプレートファイルを紐付け、最終的にHTMLをレスポンスとして返す方法を取りますが、これだと例えばコード内で一部のHTMLのみを組み立てたい、ということができません。 ですが実は、一部の設定を変更することで、明示的にテンプレートファイルをController以外の場所から呼び出して使用することができるようになります。

明示的に呼び出す方法

まずは、テンプレートエンジンの設定をカスタマイズしてBean化します。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;

@Configuration
public class SpringTemplateEngineConfig {
    /**
     * SpringTemplateのBean化
     * @return 設定済みSpringTemplate
     */
    @Bean
    public SpringTemplateEngine springTemplateEngine() {
        var resolver = new ClassLoaderTemplateResolver();

        // HTMLとして使用
        resolver.setTemplateMode(TemplateMode.HTML);

        // resources配下で、どんなプレフィックスのファイルを使うか
        resolver.setPrefix("templates/");
        // resources配下で、どんなサフィックスのファイルを使うか
        resolver.setSuffix(".html");

        // 文字列エンコード
        resolver.setCharacterEncoding("UTF-8");

        // キャッシュするかどうか
        resolver.setCacheable(true);

        var engine = new SpringTemplateEngine();
        engine.setTemplateResolver(resolver);

        return engine;
    }
}

設定できたら、使用します。

resources/templates/sample.html に以下を配置

<div xmlns:th="http://www.thymeleaf.org">
    <span th:text="${title}"></span>
    
    <span th:text="${story}"></span>
</div>
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine;

@Component
@RequiredArgsConstructor
public class SampleImpl implements Sample {
    private final SpringTemplateEngine springTemplateEngine;

    @Override
    public String sample() {
        var context = new Context();
        context.setVariable("title", "サンプルタイトル");
        context.setVariable("story", "サンプル本文");
        String html =  springTemplateEngine.process("sample", context);

        return html;
    }
}

これで、 sample() メソッドを実行することで、指定のHTMLを文字列で取得することができるようになります。

最後に

上記のサンプル程度なら文字列から組み立てても大して問題はないでしょうが、これが複雑になっていくに連れテンプレートファイルで分けることで見通しが相当良くなっていきます。 ぜひ使ってみていただけると幸いです。

なおこの方法を使うと、今度はControllerでの自動紐付けができなくなるそうなので、両方で使いたい場合はController用と明示的呼び出し用の2つ分を設定する必要があることを注意してください。

Pythonがインストールできない状態から機械学習ライブラリを使うまで

iXIT染谷です。
機械学習で遊んでみようと思ったのにPythonがインストールできなかったので、そこから機械学習でテキスト分類するところまでやってみようと思います。
今回は仮想環境でPythonのインストールをしたときのメモです。

環境

MacOS 10.15.7

経緯

自身が参加するプロジェクトはゲーム系のアプリで、ユーザーが意見を書き込む目安箱のような場所がアプリ内にあります。
しかし、そこに集められた意見をまとめて見る機会は少なく、充分に活用されているとは言い難い。

そこで簡易的な機械学習でも使って、そこに集まった意見を振り分けようと考えていました。
機械学習を行うなら先人が多いPythonがいいのでPythonをインストール。

(ハイパー初心者諸兄に優しい「Pythonの開発環境を用意しよう!(Mac)」)

問題発生

brew install を実行しようとしたところ書き込み権限がないと怒られました。
会社から借用しているPCなのでadmin権限がないんですかね。。。
前までPython使えていたのにど う し て。。

 Error: The following directories are not writable by your user

機械学習のライブラリはほとんどがPythonパッケージなので、Pythonがないと何もできません。
情シスの方に相談してみてもいいけど、トンチンカンなこと言ったら嫌なので代替案がないか模索。

Dockerで環境を作る

仮想環境なら権限必要ないのでは?と思い、Dockerで環境を作る記事を検索。

普段Pyhtonを使うときはJupyter Notebookを使って実行しているので、できれば今回も使いたい...!
というより、たくさんのファイル使ったり試行錯誤するならJupyter Notebookを使うのがかなり楽なので、使えないのは嫌です。
GUI大好き。

qiita.com

上記の記事を参考にしながら環境構築が無事完了。

DockerでPython環境を作るのではいけない理由

DockerでPython3環境自体作る記事がありましたが、それではJupyter Notebookは使えません。
(もしかしたら設定とかいじればいけるのかも...?)
Jupyter Notebookのインストール自体はそこからできますが、Jupyter Notebookはローカルから立ち上げを行なってしまうので、結局Docker(仮想環境)でPython環境を作った意味がなくなってしまいます。 今回はJupyter Notebookを使いたかったのでこちらを選びませんでした。 (一応Python3環境を作りたいという方はこちらが分かりやすかったのでぜひ)
dockerで簡易にpython3の環境を作ってみる - Qiita

次の予定

① ユーザーの意見のデータを取ってくる
② ローデータを眺めながらどんな分類にするか考える
③ fastText(機械学習ライブラリの1つ)を使ったテキストの分類を行う

Jetpack Composeにおける状態ホイスティング

こんにちは。エキサイト株式会社 Androidエンジニアの克です。

今回はJetpack Composeにおける状態ホイスティングの概念についてお話します。

Composeの状態とは

Composeの状態の分類として、Composeの内部で状態を持つ「ステートフルなCompose」と、Composeの内部には状態を持たない「ステートレスなCompose」の2種類が存在します。

例えば下記のようなComposeは、内部で状態を持っているためステートフルとなります。

@Composable
fun NameInput() {
    var name by remember { mutableStateOf("") }

    TextField(
        value = name,
        onValueChange = { name = it }
    )
}

ステートフルなComposeの短所

先に例示したようなステートフルなComposeには、下記のような短所があります。

  • 内部に状態を持っているので、外部から状態に干渉できない
  • 内部に状態を持っているので、テストが容易ではない
  • 状態は変数のため、どこで変化するのかを追う必要がある

例えば上記のNameInputでは、ボタンを押した時にTextFieldを空にしたいということはできません。(NameInput内で実装すれば可能ですが、それはこのComposeの責務を超えてしまいます)

そのため、基本的にはステートレスなComposeの方が取り回しがよく汎用性があります。

このステートフルなComposeをステートレスなComposeに変えるための手法が状態ホイスティングです。

ただし、ステートフルなComposeの短所は長所にも言い換えられます。

  • 内部に状態を持っているため、外部からはComposeの状態を気にする必要がない

このため、場合によってはあえてステートフルなComposeとして扱うという選択肢もあるということを留意しておきましょう。

状態ホイスティングとは

ホイスティング(Hoisting)とは「巻き上げ」という意味で、Composeの状態ホイスティングとはすなわち「Composeの状態を外部に巻き上げる」という意味となります。

これは「Comopseの内部には状態を持たない」ようにするのと同義であり、結果としてステートレスなComposeにすることができるというわけです。

上記のステートフルなComposeであるNameInputに状態ホイスティングを適用したものが下記になります。

@Composable
fun NameInput(name: String, onNameChange: (String) -> Unit) {
    TextField(
        value = name,
        onValueChange = onNameChange
    )
}

@Composable
fun NameScreen() {
    var name by remember { mutableStateOf("") }

    NameInput(
        name = name,
        onNameChange = { name = it }
    )
}

状態ホイスティングとは具体的には、Composeが使用する値およびComposeで発生するイベントを外部に巻き上げることを指します。

上記のコードにおいてはTextFieldに表示する valueが使用する値、TextFieldで発生する onValueChangeが発生するイベントです。

こうすることでステートフルなComposeにあった短所を解消することができました。

  • 内部に状態を持たないので、外部から状態を変更できる
  • 内部に状態を持たないので、テストが容易である
  • 状態は不変なため、変更されることはない

例えばボタンを押した時にTextFieldを空にしたい場合は下記のような実装になります。

@Composable
fun NameScreen() {
    var name by remember { mutableStateOf("") }

    NameInput(
        name = name,
        onNameChange = { name = it }
    )

    Button(onClick = { name = "" }) {
        Text(text = "削除")
    }
}

最後に

Jetpack Composeは宣言型UIの思想で作られているため、カプセル化や状態ホイスティングによって責務の分離が行いやすく、部品としての管理がしやすくなっています。

再利用性も高く、不具合の生まれる余地を減らしながら効率的に画面の構築を行うことができます。

既存のレイアウトxmlとは概念が大きく変わりはしますが、Jetpack Composeはこういった面でのメリットが大きいのでぜひ使っていきたいですね。

【IoT/Arduino】1.5m以内に人が来たらWebAPIを叩く仕組み (材料費1900円)

iXIT株式会社 岡崎です。

去年は短時間でシステム化できるようにノーコードを試して実戦投入しました。

今年は、システム化できる範囲を広げようとIoTを試し始めました。

概要

人感センサーで1.5m以内に人が来たことを検知したら、

WebAPIを叩くという仕組みを作ってみます。

 

人が廊下を通ったら、WebAPIを叩いて回数記録&閾値で通知する感じ。

 

今回はWebAPIを叩くところまで。

使い道

・一人暮らしの親が元気にしているかをリモートから自動で把握

・防犯として、夜間に人が通たら通知

材料

追加購入した物 合計1,870円(税込み)

 ①人感センサー M5Stack用PIRセンサユニット(583 円)

  (赤外線の変化によって人の接近を検知するセンサー。

   1.5m以内に人が来たら検知します。)

 ②マイコンボード ATOM Lite (1,287 円)

  (CPU 240MHz×2、メモリ0.5MB、フラッシュメモリ4MB、Wi-FiBluetooth

 f:id:ixit-okazaki:20210719131411j:plain

手元にあったもの

 ③何らかのWebAPI

  ※今回はノーコード(Bubble.io)で作ったLINEメッセージ送信のWabAPIを使います。

 ④USB-Cのケーブル、USB充電器

作り方

  1. ①②④をそれそれケーブルでつなげます
  2. 「②マイコンボード」にプログラムを書きこむ

 ざっくり以上です。

システム内の流れ

【人が廊下を通る】

  ↓ 赤外線

【①人感センサーが検知する】1.5m以内に人が近づいたら検知

  ↓ Goveケーブル

【②マイコンボードがWebAPIを叩く】

 ※デバッグ用に、検知したらLEDがからに変わるように設定

  ↓ Wifi

WIFIルーター

  ↓ インターネット

【③何らかのWebAPI】

 ※今回は、検知したらLINEメッセージを送ります。   

ここまで動かした動画 (10秒)

youtu.be

プログラム

マイコンボードのプログラムは、C言語風の「Arduino言語」という言語で書きました。

 ①マイコンボードの初期化処理(setup)

 ②マイコンボードが起動中にループ実行する処理(loop)

 ③WebAPI呼び出し(send_line_message)

 ④マイコンボードのLEDの色変更(display_LED)

※【 】の中は環境によって変更

#include "M5Atom.h"
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

WiFiMulti wifiMulti;

uint8_t DisBuff[2 + 5 * 5 * 3];

// ①マイコンボードの初期化処理
void setup()
{
    M5.begin(true, false, true);

    delay(10);
    display_LED(0x00, 0x00, 0x00);
    
    //人感センサーと接続
    pinMode(32, INPUT);
    
    //Wifi接続
    wifiMulti.addAP("【SSID】", "【PASSWORD】");
}

// ②マイコンボードが起動中にループ実行する処理
void loop()
{

    if(digitalRead(32)==1){
        //人感センサーが検知中は、LEDを赤に変更
        display_LED(0xff, 0x00, 0x00);

        //WebAPIを呼び出し(今回はLINEメッセージ送信)
        send_line_message();

        delay(10000);

    }else{
        //人感センサーが検知していない場合、LEDを緑に変更
        display_LED(0x00, 0xff, 0x00);
    }
      
    delay(100);
    M5.update();
}

// ③WebAPI呼び出し
void send_line_message() {
    
    if((wifiMulti.run() == WL_CONNECTED)) {      
        //送信する情報を作成
        DynamicJsonDocument doc(JSON_OBJECT_SIZE(2));
        doc["uuid"] = "【LINEアカウントの内部ID】";
        doc["message"] = "廊下を通ったよ";
        String json;
        serializeJson(doc, json);

        // WebAPIを呼び出し
        HTTPClient http;
        http.begin("https://no-template01.bubbleapps.io/version-test/api/1.1/wf/line_send_mes/");
        http.addHeader("Content-Type", "application/json");
        http.POST(json);

        http.end();
        
        delay(50);  
     }  
     
}

// ④マイコンボードのLEDの色変更
void display_LED(uint8_t Rdata, uint8_t Gdata, uint8_t Bdata)
{
    DisBuff[0] = 0x05;
    DisBuff[1] = 0x05;
    for (int i = 0; i < 25; i++)
    {
        DisBuff[2 + i * 3 + 0] = Rdata;
        DisBuff[2 + i * 3 + 1] = Gdata;
        DisBuff[2 + i * 3 + 2] = Bdata;
    }

    M5.dis.displaybuff(DisBuff);
}

「Best Creator Award」と「審査員特別賞」をいただきました!

こんにちは、エキサイトでエンジニアをしている おおしげ(@_ohshige) です。
この度(4月の話ですが...)、大変光栄なことに、XTechグループ総会で「Best Creator Award」、部署の総会で「審査員特別賞」をいただいたので、ご紹介させてください。

昨年の話ではありますが、主にチーム内での技術力向上のために様々な取り組みを行ってきました。
例えば、輪読会や勉強会などの開催であったり、あらゆるプロジェクトのあらゆるPullRequestに首を突っ込んでは指摘したり、新規プロジェクトのあらゆる設計に携わったり、自動テストの推進であったり、色々やってきました。
光栄にもそのような取り組みが評価されて2つの賞をいただくことができました。

文字が少し読みづらいですが、画像の右側が「Best Creator Award」、左側が「審査員特別賞」のトロフィーです。
緊急事態宣言がずっと続いていて出社できなかったためこれらを受け取ることができず、その結果3ヶ月以上も経った今になってブログを書いています。
(トロフィーの画像をどうしても載せたかったんです!)

f:id:excite-ohshige:20210719103429j:plain:w500

Best Creator Award

今年の4月にXTechグループ総会がありました。
XTechグループ総会とはその名の通り、エキサイト・iXITだけでなくXTechグループに属するすべての社員が参加して行われる定期総会です。
今回のXTechグループ総会では、

  • MVP
  • Best Manager Award
  • Best Player Award
  • Best Creator Award
  • Best Project Award
  • Best Rookie Award

が発表されました。

そして、私おおしげは、その中でもエンジニアとデザイナーを対象に選ばれる「Best Creator Award」で表彰していただきました!
クリエイティブな業務を行っている中でまさにクリエイティブ向けの賞をいただけるのは、大変光栄なことでとてもうれしく思います!

サプライズ精神が強いのか全く知らされておらず、当日まで「XTechグループ総会か〜」といった程度のことしか頭にありませんでした。
しかも、私の受賞に向けてチームメンバーからのお祝いの言葉があり、事前に用意しておいたようで、チームで何も知らないのは自分だけの状態だったようです。
賞をいただいたこともチームメンバーからのお祝いの言葉もとても嬉しかったのですが、総会当日まで何も知らなかった自分が少し恥ずかしかったです。

また、実は、XTechグループ全体が受賞対象であるにも関わらず、「Best Project Award」には私の所属するチームが選ばれ、「Best Player Award」には私と同じ部署のメンバー(非技術職)が選ばれ、素晴らしい出来だったのかなと思います。
さらには、「Best Manager Award」にはエンジニアであり私と同期入社であるRadiotalk社CTOの斉藤が選ばれ、近しい人ばかりだなと思いました。

審査員特別賞

XTechグループ総会よりも少し前の4月に、部署全体での総会がありました。
1年に1回行われ、様々な発表があるのですが、そこでも表彰が行われます。
今年は、

  • MVP
  • 準MVP
  • 3位MVP
  • 審査員特別賞

が発表されました。

そして、私おおしげは、部署内での技術的な活動が認められ、「審査員特別賞」をいただきました!
昨年はノミネートに含まれていたものの表彰されるまでは至らずだったので、今年は表彰していただいて大変光栄でとてもありがたいです。
(時系列としては、この部署総会で受賞したあとにXTechグループ総会での受賞があったので、部署総会の時点では「このあとのことはまさに知る由もなく」という状態でした)

ちなみに、MVPに選ばれたのは、XTechグループ総会で「Best Player Award」に選ばれたメンバーでした。素晴らしい!

受賞にあたって

部署とグループ全体の両方で賞をいただけるのは、とても光栄であり、とてもありがたいことです。素直にめちゃくちゃ嬉しいです。
いずれの賞も、基本的にはチームとしての技術力向上のために行ってきた様々なコトや日々の取り組みを評価していただいた結果だと思います。
ですが、何をやってきたにしても独りよがりなことだけでは誰もついてきてくれませんし、ただの自己満足で終わってしまいます。
そういう点では、私がやりたいといったことを許可してくれた上長や、やりたいことについてきてくれたチームメンバーや、良かれと思って言っている指摘をウザい怖い先輩だと思わずに場合によっては素直に場合によっては反論した上で受け入れてくれた後輩たちなど、みんながいてくれた結果だと思っています。

「壮大な賞をとって十分に満足しました」みたいになってしまっていますが、そんなこともなく、日々精進していきたいと思っています。
XTechグループ総会ではMVPや連続でのCreator賞受賞を、部署総会ではMVPを、貪欲に狙っていこうと思います。

そして、その先では、私おおしげのチームでの技術力向上のための取り組みを行った結果、その中から次の「Best Creator Award」受賞メンバーが出てくれると、さらに冥利に尽きます。

ECSの定期ローリングアップデートをJenkinsでやった話

こんにちは。 エキサイト株式会社の三浦です。

突然ですが、ずーっと同じコンテナを使い続けていると問題がある、という状況になった方はいないでしょうか? 例えばメモリリークなどで、時間が経てば経つほどメモリ使用率が上がっていってしまい、最後にはパフォーマンスが出なくなってしまうなどです。

今回は、AWS ECSにてそれに対応する方法の1つとして、定期ローリングアップデートをJenkinsで行った話をしていきます。

ずっと同じコンテナを使い続けると起きる問題

アプリケーションの作り方の問題で、同じコンテナをずっと使い続けていると問題が発生する場合があります。 例えばメモリリークでメモリ使用率が際限なく上がってしまう、ゴミデータがたまり続けてストレージを圧迫してしまうなどです。

アプリケーションの修正によってそれらの問題に対応できるならそれで問題はないのですが、あまりに古いアプリケーションだったりして修正が困難な場合もあるでしょう。

弊社でも、あるアプリケーションでコンテナを更新せずにずっと使い続けるとメモリ使用率が上がっていってしまい、パフォーマンスが下がってしまうという問題が発生していました。 そこで弊社では、コンテナごと定期的に更新する方法で対処しました。

定期ローリングアップデート法

やり方は簡単で、Jenkins上で、定期的に対象ECSを強制ローリングアップデートするだけです。

ローリングアップデートは、AWS CLIを以下のように使用しました。

stages {
    stage("Rolling Update: ECS") {
        steps {
            // ローリングアップデート実行
            sh """
            aws ecs update-service \
                --cluster $CLUSTER_NAME \
                --service $SERVICE_NAME \
                --platform-version 1.4.0 \
                --force-new-deployment
            """

            // アップデートが完了するまで待つ
            sh """
            aws ecs wait services-stable  \
                --cluster $CLUSTER_NAME \
                --service $SERVICE_NAME
            """
        }
    }
}

それを、以下のように定期的にアップデートします。

H(0-30) 10 * * *

毎朝10:00~10:30の間で、よしなに実行してくれます。

さいごに

根本的な解決になっているとは言い難いかもしれませんが、とりあえずの処置としては十分に機能するのではないでしょうか。

state_notifierとfreezedのプロジェクトでMockitoを用いたunit test

エキサイト株式会社の高野です。
弊社では最近Flutterでテストを書き始めました。state_notifierとfreezedを用いたプロジェクトをどうやってテストしようか考えていた所Mockitoを見つけ、よさそうでしたので書いてみたコードの流れの紹介になります。

各バージョン

Flutter: 2.0.6
iOS: 14.5
Android: 11.0

使用ライブラリ

mockito

実装

僕が今開発しているローリエプレスにおける最も重要たるロジックはAPIから受け取ったものをView側に流してあげることです。
プロジェクト自体は、state_notifierとfreezedを用いて実装していますのでrepositoryから受け取ったものをstateに渡せているかをテストしていきます。

@GenerateMocks([RepositoryImpl])
void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  final _mockRepositoryImpl = MockRepositoryImpl();
  final controller = Controller(
      controller: controller,
   );
}

以上のようにRepositoryをMockitoを用いてモック化し、Controllerをイニシャライズします。

test('記事を20件取得する', () async {
      when(RepositoryImpl.fetchArticleList(category: Pickup().package, page: 1, size: 20))
          .thenAnswer((_) async =>
          Result.success(List.generate(20, (_) => Article())));
      await controller.initArticleList();
      expect(controller.state.maybeMap(
          success: (s) => s.articles.length,
          orElse: () {},
      ), 20);
      expect(pickupArticleListController.page, 1);
    });

つづいて以上のようにrepositoryのメソッドに必要な引数を設定し、Futureメソッドですので thenAnswer を用いて返り値を指定してあげます。今回ですとArticle型のListを要素20で生成しました。
その後メソッドを実行し、stateのarticlesの長さが20に変更されているかを確認します。
僕のプロジェクトですと、stateをsuccess, loading, errorの3つの状態で用意していますので今回はAPIがうまくいっているものを前提としているのでsuccessに通ってくるのでmaybeMapを使用しています。

まとめ

Futureメソッドで返り値がResultかつそれをstate.copyWithで変数を変更した場合のUnitTestの紹介でした。これを元に他のファイルもテストしていこうと思います。

www.wantedly.com

SlackにAPI経由でメッセージ・画像を送信するための権限設定

こんにちは。 エキサイト株式会社の三浦です。

Slackは、多くの人が使っているメッセージングアプリケーションです。 また、単に人間がメッセージを送るだけでなく、Slack APIを使うことでプログラムから自動でメッセージを送信することができます。

今回は、Slack APIからSlackにメッセージ・画像ファイルを送信する際に必要となるSlack APIの設定を説明していきます。 なおこちらは、2021年7月現在の方法となっていますので、あらかじめご了承ください。

Slack APIとは

Slack API は、使用することでSlackに対してプログラム等を通じて自動でメッセージやファイルの送信をすることができるようになる仕組みです。 弊社ではこの仕組みを使って、スプレッドシートにまとめているサービスの各種KPIをGoogle App Scriptを通じて毎朝ピックアップしてSlackに流すようにしており、ざっくりとしたサービスの数字把握に役立てています。

このSlack APIですが、使用する際に細かい権限設定をする必要があり、少し複雑です。

文字列・ファイルを送信するためのSlack APIの設定方法

まずは、 「アプリ作成」 画面に行きます。

f:id:excite-takayuki-miura:20210712103026p:plain

Create New App ボタンを押し、 From scratch を選びましょう。

f:id:excite-takayuki-miura:20210712153833p:plain

App Name には自由な名前を、 Pick a workspace to develop your app in では、Slack APIを使用したいworkspaceを選びます。

アプリの作成をすると、 Basic Information というページに飛びます。 ここから、今作成した App に必要な権限等を付与していくことになります。

権限の付与のために、 Permissions を選択しましょう。

f:id:excite-takayuki-miura:20210712112231p:plain

なお、 Incoming Webhooks でもSlackにメッセージを送信することはできますが、ファイルの送信はできないようです。 メッセージの送信だけなら構いませんが、もしファイルも送信したいのであれば、 Incoming Webhooks は選ばないようにしてください。

Permissions を選択すると、左ナビの OAuth & Permissions というページに飛びます。 下にスクロールしていくと、 Scopes という項目があるので、そこから権限を設定していきます。

Bot Token Scopes の権限を設定しましょう。

f:id:excite-takayuki-miura:20210712113526p:plain

メッセージ送信のために chat:write を、ファイルの送信のために files:write を選択します。

f:id:excite-takayuki-miura:20210712113625p:plain

選択後、一番上にスクロールすると Install to Workspace というボタンがクリック可能になっているので、クリックします。

f:id:excite-takayuki-miura:20210712113711p:plain

リクエストの許可を求めるページに遷移するので、許可をするとこのアプリケーションが使用可能になります。

許可をすると Bot User OAuth Token が表示されているはずです。 ただし、この時点ではまだメッセージやファイルを送信することはできません。

最後にSlack本体にて、Slack APIを使用したいチャンネルの「ショートカット」か、チャンネル詳細の「インテグレーション」から今回作ったアプリを追加してください。

f:id:excite-takayuki-miura:20210712114120p:plain

f:id:excite-takayuki-miura:20210712114303p:plain

これで準備が整いました。

Bot User OAuth Token を使った具体的なメッセージ・ファイル送信方法は今回は省略します。

こちら など、色々なところに情報が載っていると思いますので、そちらでご確認ください。

最後に

Slackの権限回りは、時間が経つと変わっている場合があります。 私も、昔やった設定方法とは異なっているような気がして少し戸惑いました。

この方法もいずれは違うやり方になっている可能性はありますが、それまでは役に立てば幸いです。