SEEDS Creator's Blog

slackに学ぶ、デザインシステムを作る重要性

こんにちは。デザイナーの河野です。

突然ですが、「デザインシステム」ってご存知ですか?

先日、Slackのデザイナーによって下記のような記事が公開されていました。
slack.engineering
slackは長らくユーザーの満足度を最優先でサービスを展開していましたが、
デザインはその時々の問題解決を優先で対応していたため、
結果、デザインに一貫性がなくなっていたり矛盾点が生じていたようです。
(スケールは全然違いますが、似たような体験をしたことが。あるあるですね。)

それを今回、時間をかけてコンポーネントからUIの見直しなど一から再構築し
デザイナー、エンジニアみんなが使える「Slack Kit」という
デザインシステムが出来上がったよ、という内容の記事になります。

f:id:seeds-std:20190924122735p:plain
引用:The Gradual Design System: How We Built Slack Kit


シーズの今までのデザイン体制

私はここ1年半ほど、ほぼ1人でデザイン業務を担当していたので
全く意識していなかったのですが、
新しく入ったデザイナーに既存のサイトの一部業務の引き継ぎをする時に
どうもイメージや認識の違いを覚えるようになり、
今までルールが自己で完結していたなぁ...と、その重要性を痛感しました。

デザインは複数人が行うと、バラつきやぶれが発生してしまいがちです。
同じサービス内なのにボタンやバナーのディティールや
使っている色、余白にバラつきがあったら
一貫性がなくなり、おのずとサービスとしての質も下がって見えてしまいますね。
またエンジニアへのコミュニケーションコストなども考えると大きな損失になります。
結果、会社全体の生産性の話にも繋がってきます。

デザインシステムを作るメリット

こんな時、デザインシステムを予めしっかり作っておくと、
今後また新しいチームメンバーが参加しても
それまでのルールに乗っ取ったデザインができるようになります。

また、どのようにデザインすべきかが明示されているので、
極論、デザイナーでない人もデザインできるようなります(!)
例えばちょっとしたアラートを出すとか、バナーを追加するとなった時は
コーダーやフロントエンジニアが直接作ってしまうというのも可能なわけです。

実はシーズのフロントエンドエンジニアチームでは、すでに似たようなものが導入されていて、
リーダーのNさん(あだ名:師匠)によって
コーディングガイドサイトのようなものが社内向けで共有されています。

f:id:seeds-std:20190924122752p:plain
中身はナイショでございます。



デザインシステムに入れると良い要素とは?

ということで、うちもデザインシステム作ろうじゃないの!
と、早速PCに向かったはいいものの、
具体的に何を作ればいいんだっけ・・
と、路頭に迷ったので、まずは色々と調べてみました。

①カラー

f:id:seeds-std:20190924131222p:plain

コーポレートカラーやブランドカラーといった意匠的なところから、
アプリやサービスなどの場合はBootstrapのカラーユーティリティーのような
状況に応じたルールも必要になります。
色は配色バランスによって印象が大きく変わりますので、
「この色は画面全体の●●%まで」と割合も決めた方が良いかもしれません。

②各種コンポーネントなど

f:id:seeds-std:20190924145220p:plain
色に続いて、これはエンジニアの作業と紐付けやすいと思います。
各種ボタンなどデザインパーツ、統一されてないと非常にモヤモヤしますよね。。(職業病)
Sketchの場合symbolを使用すれば解決する話でもあります。

③余白

f:id:seeds-std:20190924145230p:plain
これ、盲点ですね。
例えば既存のサイトで新しいセクションやページを追加するとなった時、
別のデザイナーに依頼したらmarginが違う...とかありそうですよね。

④レイヤーの命名規則

f:id:seeds-std:20190924161419g:plain
作業中のレイヤーって気づくと混沌とした世界になっている事がありますね。。
これも共通ルールなど徹底すると、エンジニアの作業も楽になるのではないでしょうか?

⑤テキスト

f:id:seeds-std:20190924145251p:plain
これもルール化しておけば良かった...と後悔したものの上位になります。
何も指定していないと100%デザイナーによって変わります。
フォント、行間、サイズ、行揃えなどなど、ただのテキストでも留意点がたくさんありますね。

⑥ページレイアウト

f:id:seeds-std:20190924145325p:plain
例えば、一口にレスポンシブデザインと言っても、
リキッドなのかフレキシブルなのかで作業ボリュームが全然違います。 対応端末は何パターンか、ブレイクポイントはいくつか、など 段階別に何パターンかルールを分けておくと良いかもしれないです。


<おまけ>著名な企業のデザインシステムの実例

最後に、著名な企業のデザインシステムをご紹介します。
近頃では、デザインシステムをただの社内用のルールとしてではなく、
ブランディングの一環として公表している会社が結構あることが分かりました。

Atlassian

f:id:seeds-std:20190924123201p:plain エンジニアの皆さんご存知のAtlassianです。
なんとデザインシステムを作る前は同じドロップダウンのパーツなのに45種類の異なるデザインがあったとか。。

Airbnb

f:id:seeds-std:20190924123203p:plain 創業者がデザイナーということもあってか、ブランディングの徹底ぶりが半端ない Airbnb。
2018年にオリジナルフォントを出したことも記憶に新しいです。


Uber

f:id:seeds-std:20190924123208p:plain Uberは2018年にリブランディングを行いました。
それについて詳しく書かれています。
一見自由な線のイラストもGridに沿って作成されていて、その仕事の精密さに感動すら覚えます。

Salesforce

f:id:seeds-std:20190924123211p:plain 最後にBtoB企業も紹介します。Salesforceは「Lightning」と言うデザインシステムを公開しています。
「分かりやすさ」「効率性」「一貫性」「美しさ」をデザイン原則として作られています。

まとめ

調べるほど奥が深いデザインシステム。
まさかこうやってネット上に公表している組織があることが少し驚きでした。

また、デザイナーはセンスや感性が命!と思われている人も多いかもしれないですが、
こういった理念に基づいた細やかなルールを守り、
効率的にプロダクトの品質を守っていくというのも大事な仕事なんですね。

シーズは現在デザイナーは2名しかいないですが、
いつかメンバーが入れ替わったり大きな組織になった時、
未来のデザイナーが困らないよう、ちょっとずつ頑張っていきたいものです。

AWS Lambda関数をKotlin/JSで書いてみる

f:id:seeds-std:20190925123639p:plain

WEB事業部の西村です。

唐突ですが皆さんはLambda関数を作成する際にどのような言語を用いていますでしょうか?
用途に合った言語や自分で書きやすいと思う言語などの要因で決めていると思われます
私はKotlinという言語が好きでKotlinで書こうと思ったのですが、Kotlinで書かれている記事はそのほとんどがJavaで実行することを前提とした元なっています
しかし、Javaではコールドスタートした際に実行時間がかかってしまうというデメリットがあります
KotlinはJavaだけでなくJavaScriptでも動作させることができるので今回はNode.jsで実行させる関数の作成を行いたいと思います

目次

この記事の目標

Kotlin/JSを用いたLambda関数の作成とそのレスポンス確認

この記事で取り扱わないこと

この記事では AWS LambdaAmazon API Gateway については詳しく解説いたしません
あらかじめLambda関数とその関数を呼び出すためのAPIの作成を行っておいてください

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクトの作成

プロジェクトの作成を行います
任意のディレクトリにプロジェクト用のフォルダを作成します(この記事では KotlinLambda としています)
その後そのディレクトリ内に build.gradle.ktssettings.gradle.kts の2つのファイルを作成します

KotlinLambda
├─build.gradle.kts
└─settings.gradle.kts

作成したらIntelliJ IDEAでプロジェクトのフォルダを開きます
続いて、 build.gradle.ktssettings.gradle.kts を開き下記の内容を入力します

build.gradle.kts

plugins {
    kotlin("js") version "1.3.50"
}

repositories {
    mavenCentral()
}

kotlin {
    target {
        nodejs()
        useCommonJs()
    }

    sourceSets["main"].dependencies {
        implementation(kotlin("stdlib-js"))
    }
}

settings.gradle.kts

rootProject.name = "kotlin_lambda"

その後画面右下に出ている Gradle projects need to be importedImport Changes をクリックします
CONFIGURE SUCCESSFUL と出てきたら準備完了です

ハンドラソースコードの追加

次にソースディレクトリを作成します
プロジェクトのディレクトリに src/main/kotlin でディレクトリを作成します
またパッケージ名を追加する場合、パッケージを作成します(この記事では jp.co.seeds_std.lambda.kotlin とします)
作成したディレクトリに handler.kt というファイルを作成し下記の内容を記述します

handler.kt

package jp.co.seeds_std.lambda.kotlin

@JsExport
@JsName("handler")
fun handler(event: dynamic, context: dynamic, callback: (Error?, dynamic) -> Unit) {
    val response: dynamic = object {}
    response["statusCode"] = 200
    response.body = "Hello from Kotlin Lambda!"
    callback(null, response)
}
  • @JsExport JavaScriptのexportを行うアノテーションです
  • @JsName(name: String) 記述したクラスや関数をトランスパイルした際 name に指定した名前となるよう変換してくれるようになります
  • dynamic JavaScriptのオブジェクトをそのまま取り扱います(. [] でその中の関数や変数にアクセスできます)

デプロイ

デプロイパッケージの作成を行います
圧縮を自動化するためのタスクを追加します
build.gradle.kts を再度開き下記の内容を追加します

build.gradle.kts

import com.google.gson.JsonParser
import java.io.OutputStream
import java.util.zip.ZipOutputStream
import java.util.zip.ZipEntry

plugins { ... }
repositories { ... }
kotlin { ... }

open class CompressTask : DefaultTask() {

    lateinit var buildDirectory: File
    lateinit var projectName: String
    var rootProjectName: String? = null

    private val blackLists = setOf("kotlin-test-nodejs-runner", "kotlin-test")

    @TaskAction
    fun compress() {
        val projectPrefix = if(rootProjectName != null && rootProjectName != projectName) "$rootProjectName-" else ""
        val zipFile = File(buildDirectory, "../compress.zip")
        val outputStream = ZipOutputStream(zipFile.outputStream(), Charsets.UTF_8).apply {
            setLevel(9)
        }

        val jsDir = File(buildDirectory, "js")
        val projectDir = File(jsDir, "packages/$projectPrefix$projectName")
        val nodeModuleDir = File(jsDir, "node_modules")
        addDependencies(outputStream, File(projectDir, "package.json"), nodeModuleDir, mutableSetOf(*blackLists.toTypedArray()))
        addZipEntry(outputStream, File(projectDir, "kotlin/$projectPrefix$projectName.js"))

        outputStream.close()
    }

    private fun addZipEntry(zipOutputStream: ZipOutputStream, file: File, addDirectory: String = "") {
        val name = if(addDirectory.isEmpty()) file.name else "$addDirectory/${file.name}"
        if(file.isDirectory) {
            file.listFiles()?.forEach {
                addZipEntry(zipOutputStream, it, name)
            }
        } else {
            val zipEntry = ZipEntry(name)
            zipOutputStream.addZipEntry(zipEntry) {
                it.writeFile(file)
            }
        }
    }

    private fun addDependencies(zipOutputStream: ZipOutputStream, packageJsonFile: File, nodeModuleDir: File, addedDependencies: MutableSet<String> = mutableSetOf()) {
        val packageJson = JsonParser().parse(packageJsonFile.reader()).asJsonObject
        if(!packageJson.has("dependencies")) return
        val dependencies = packageJson.getAsJsonObject("dependencies")
        dependencies.keySet().forEach {
            if(!addedDependencies.contains(it)) {
                addedDependencies.add(it)
                val dependenciesDir = File(nodeModuleDir, it)
                addZipEntry(zipOutputStream, dependenciesDir, "node_modules")
                addDependencies(zipOutputStream, File(dependenciesDir, "package.json"), nodeModuleDir, addedDependencies)
            }
        }
    }

    private inline fun ZipOutputStream.addZipEntry(entry: ZipEntry, crossinline block: (OutputStream) -> Unit) {
        try {
            putNextEntry(entry)
            block(this)
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            closeEntry()
        }
    }

    @Suppress("NOTHING_TO_INLINE")
    private inline fun OutputStream.writeFile(file: File) {
        file.inputStream().use {
            it.copyTo(this)
        }
    }
}

tasks.register("noMainCall") {
    doFirst {
        kotlin.target.compilations.all { compileKotlinTask.kotlinOptions.main = "noCall" }
    }
}

tasks.register<CompressTask>("compress") {
    dependsOn("compileKotlinJs", "noMainCall")
    this.buildDirectory = rootProject.buildDir
    this.projectName = project.name
    this.rootProjectName = rootProject.name
}

tasks["compileKotlinJs"].mustRunAfter("noMainCall")

追加後、画面右下に出ている Gradle projects need to be importedImport Changes をクリックします
CONFIGURE SUCCESSFUL と出てきたら画面右側の Gradle タブを開きます
kotlin_lambda -> Tasks -> other の順に開き、その中の compress をダブルクリックして実行します
するとプロジェクトのディレクトリに compress.zip というファイルが生成されます
この生成されたファイルをコンソールからアップロードします
その後の ハンドラkotlin_lambda.{パッケージ名}.handler に変更します (この記事では kotlin_lambda.jp.co.seeds_std.lambda.kotlin.handler となります) アップロードと変更が完了したら保存を行います

動作確認

あらかじめ作成しておいたAPI GatewayのURLにアクセスし Hello from Kotlin Lambda! と表示されれば成功です

最後に

いかがでしたでしょうか
Javaの時より動作も軽くなったと感じるのではないかと思います
次回はもう少し使いやすくなるような記事も書きたいと思います
ここまで読んでいただきありございました

Kotlin/JSのAWS Lambda関数を便利にしてみる

SlackAPIを使って簡単なTODOチェックアプリを作成してみた

WEBエンジニアの石田です。 さて、僕は前回もSlackネタでしたが、今回もSlackネタです。

弊社では、お掃除部という部活(?)がありまして、拭き掃除・ゴミ出し・換気などのオフィス内の掃除、あと朝一のコーヒー作りを有志が毎日行っています。
当番とかは決まっておらず、できるメンバーでやろう!というスタンスなのですが、部員の大半を占めるエンジニア達はフレックス制で出社時間がバラバラ…
となると、チェックリストは欲しいですよね。

紙とかホワイトボードでチェック!となるのがまあ普通だと思いますが、弊社はIT企業。そして社内のコミュニケーションにSlackを使ってるんだから、せっかくならオンラインで済ませてしまいたい。という欲が僕の中で沸々と湧き上がってきたので、SlackAPIを使ってアプリ作りました(・ω・)b

まずは出来上がったものをご紹介します。

f:id:seeds-std:20190425000202g:plain
ボタンをクリックすると名前が登録され、同じボタンを押すと登録が解除されます

仕組み

  • AM9:00になるとタスク一覧と、項目ごとの絵文字のボタンが並んでいるメッセージが自動投稿される。
    • cronで平日毎日9:00にpostするAPIを叩く
  • タスクの絵文字ボタンをクリックすると、クリックしたユーザーのIDがタスク一覧の下に表示される。
  • 既にボタンをクリック済みのユーザーが、もう一度同じ絵文字のボタンをクリックするとタスク一覧の下に表示されていた名前が消える。
    • 絵文字のボタンを押すとSlack から指定したURLにリクエストが送られ、リクエストに応じて処理し、メッセージを書き換えるAPIを叩く

今回つかったもの

  • PHP
  • MySQL
  • Slack API
    • Interactive Components

やってみる

1. Slack APIでAppを作成

  • Interactive Components メッセージにボタンやメニューなどを付与し、ユーザーからのアクションに応じて応答できる機能です。 GitHubやBitBucketのAppをはじめ、最近とても頻繁に使われているので多分見たことあるのではないでしょうか。

https://api.slack.com/apps?new_app=1

f:id:seeds-std:20190610212654p:plain
AppNameは後から変更も可能でした。
上記のURLにアクセスすると、SlackAppの作成画面が出るので AppName (アプリ名) と Development Slack Workspace (使用するSlackのワークスペース) を指定し、 Create App します。
SlackでAPIを作成すると、FeaturesにInteractive Componentsという項目があり、Onにすると設定が可能になります。 f:id:seeds-std:20190610213057p:plain 設定は色々とありそうですが、今回は Request URLに任意のURL( example.com/api/hogehoge.php など)を貼ればOK。

あと、投稿用にSlackのトークンを取得しておきます。 OAuth & Permissions からScopesを絞って Install App。 Permission Scopeはchat:write:botを選択でOKです。

f:id:seeds-std:20190725210251p:plain
権限はchatだけでok
f:id:seeds-std:20190725210349p:plain
Permissionを設定したらInstall App

これでSlack上の設定は完了。

2. サーバー側の設定

とりあえずmysqlでDBを用意。型は適当につけてます

CREATE TABLE polls
(
    id          INT AUTO_INCREMENT PRIMARY KEY,
    message_ts  VARCHAR(255) NOT NULL,
    channel_id  VARCHAR(255) NOT NULL,
    text        TEXT         NOT NULL,
    answers     TEXT         NOT NULL,
    attachments TEXT         NOT NULL,
    created_at  TIMESTAMP    NULL
) DEFAULT CHARSET = utf8 AUTO_INCREMENT = 1;

CREATE TABLE votes
(
    id          INT AUTO_INCREMENT PRIMARY KEY,
    channel_id   VARCHAR(255) NOT NULL,
    message_ts   VARCHAR(255) NOT NULL,
    user_name    VARCHAR(255) NOT NULL,
    user_id      VARCHAR(255) NOT NULL,
    action_name  VARCHAR(255) NOT NULL,
    action_value VARCHAR(255) NOT NULL,
    created_at   TIMESTAMP    NOT NULL
) DEFAULT CHARSET = utf8 AUTO_INCREMENT = 1;

貼り付けた任意のURLではなく、まずpublicの外にでも投稿用のファイルを作成します。 postの第一引数の連想配列が選択肢です。nameに表示する文字を入れて、 iconはslackのアイコンの名前を入力(::はとる)でOK。なくてもOK。

<?php

post([
    ['name' => '換気',    'icon' => 'wind_blowing_face'],
    ['name' => 'ゴミ出し',    'icon' => 'wastebasket'],
    ['name' => '拭き掃除',   'icon' => 'sparkles'],
    ['name' => 'コーヒー', 'icon' => 'coffee'],
] , '今日のTODO');


function post($questions, $description) {
    $attachments = [];
    $buttons = [];
    $text = $description . "\n";

    // answersが表示テキスト、buttonsが実施ボタンになる
    $answers = [];
    foreach ($questions as $key => $button) {
        $answers[] = ':' . $button['icon'] . ': ' . $button['name'];
        $buttons[] = [
            'name' => $button['icon'] ?? ($key + 1),
            'text' => ':' . ($button['icon'] ?? ($key + 1)) . ':',
            'type' => 'button',
            'value' => $button['name']
        ];
    }

    // ボタンは5つを超えるとattachmentがスマホで表示できなくなるためを分割しておく
    $block = array_chunk($buttons, 5);

    // タスクごとに改行する
    $text .= implode("\n", $answers);

    // ボタンのブロックごとにattachmentを作成する
    foreach ($block as $key => $action) {
        $attachments[] =
            [
                'fallback' => 'daily' . $key,
                "callback_id" => "daily",
                "color" => "#3AA3E3",
                "attachment_type" => "default",
                'actions' => $action
            ];
    }

    // Slackに投稿を行う
    $curl = curl_init();
    curl_setopt_array($curl, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_URL => 'https://slack.com/api/chat.postMessage',
        CURLOPT_HTTPHEADER => [
            'Content-Type: application/json; charset=utf-8',
            'Authorization: Bearer ' . '【SLACKのトークン】'
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'channel' => '【投稿チャンネル】',
            'text'    => $text,
            'attachments' => $attachments
        ])
    ]);
    $response = curl_exec($curl);
    $response = json_decode($response, true) ?? null;

    if ( !$response ) {
        return false;
    }

    //投稿情報をMySQLに保存しておく
    $db = new PDO(
        'mysql:host=' . '【接続するホスト】' .';dbname=' . '【DB名】' . ';charset=utf8',
        '【DBユーザー名】', '【DBパスワード】', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
    $query = $db->prepare('INSERT INTO polls (message_ts, channel_id, text, answers, attachments, created_at) VALUES (:message_ts, :channel_id, :text, :answers, :attachments, :created_at)');
    $query->execute([
        ':message_ts'  => $response['ts'],
        ':channel_id'  => $response['channel'],
        ':text'        => $description,
        ':answers'     => json_encode($answers),
        ':attachments' => json_encode($response['message']['attachments']),
        ':created_at'  => date('Y-m-d H:i:s')
    ]);

    return $response;
}

上記のPHPファイルを実行すると指定したチャンネルに下記のようなフォームみたいなものが投稿されます。

f:id:seeds-std:20190809175228p:plain

各ボタンをクリックすると、先程のURLのhoge.phpにPOSTが走るので、下記を配置しておきます。

<?php

if (!empty($_POST['payload'])) {
    $vote = json_decode($_POST['payload'], true);
    $message = $vote['message_ts'];
    $channel = $vote['channel']['id'];
    $user_id = $vote['user']['id'];
    $username = $vote['user']['name'];

    $db = new PDO(
        'mysql:host=' . '【接続するホスト】' .';dbname=' . '【DB名】' . ';charset=utf8',
        '【DBユーザー名】', '【DBパスワード】', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
    $query = $db->prepare('SELECT * FROM polls WHERE message_ts = :message_ts AND channel_id = :channel_id');
    $query->execute([
        ':message_ts'   => $message,
        ':channel_id'   => $channel
    ]);
    $data = current($query->fetchAll());

    if ($data) {
        $attachments = json_decode($data['attachments'], true);

        //既に存在する実施TODO・実施者IDとボタン押した人の実施TODO・IDが一致したら存在フラグをたてる
        $votes = selectVotes($db, $message, $channel);
        $exist = false;
        foreach ($votes as $answer) {
            if ($answer['user_id'] === $user_id && $answer['action_value'] === $vote['actions'][0]['value']) {
                $exist = $answer['id'];
            }
        }

        //存在フラグが立ってる場合は削除・ない場合は新規挿入
        if ($exist) {
            $db->prepare('DELETE FROM votes WHERE id=:id')->execute([':id' => $exist]);
        } else {
            $db->prepare('
                INSERT INTO
                  votes
                   (channel_id, message_ts, user_name, user_id, action_name, action_value, created_at)
                VALUES
                   (:channel_id, :message_ts, :user_name, :user_id, :action_name, :action_value, :created_at)')
            ->execute([
                ':channel_id'   => $channel,
                ':message_ts'   => $message,
                ':user_name'    => $username,
                ':user_id'      => $user_id,
                ':action_name'  => $vote['actions'][0]['name'],
                ':action_value' => $vote['actions'][0]['value'],
                ':created_at'   => date('Y-m-d H:i:s')
            ]);
        }

        // 投稿したTODOの現在の実施者一覧を出す
        $votes = selectVotes($db, $message, $channel);
        $result = [];
        $answers = json_decode($data['answers']);
        foreach ($answers as $answer) {
            $result[$answer] = '';
            foreach ($votes as $vote) {
                if ($answer === ':' . $vote['action_name'] . ': ' . $vote['action_value']) {
                    $result[$answer] .= ' <@' . $vote['user_id'] . '>';
                }
            }
        }

        // 実施者一覧をメッセージに反映する
        $text = $data['text'];
        foreach ($result as $answer => $voters) {
            $text .= "\n" . $answer . "\n";
        $text .= empty($voters) ? '' : $voters . "\n";
        }

        $update = [
            'channel' => $data['channel_id'],
            'ts' => $data['message_ts'],
            'text' => $text,
            'attachments' => $attachments
        ];

        // Slackのメッセージを更新する
        $url = 'https://slack.com/api/chat.update';
        $curl = curl_init();
        curl_setopt_array($curl, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_URL => $url,
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json; charset=utf-8',
                'Authorization: Bearer ' . '【SLACKのトークン】'
            ],
            CURLOPT_POSTFIELDS => json_encode($update)
        ]);
        return curl_exec($curl);
    }
}

function selectVotes(PDO $db, string $message_ts, string $channel_id)
{
    $query = $db->prepare('
          SELECT 
              *
          FROM
              votes
          WHERE
              message_ts = :message_ts AND
              channel_id = :channel_id
          ORDER BY
              action_value
          ');
    $query->execute([
        ':message_ts'   => $message_ts,
        ':channel_id'   => $channel_id
    ]);
    return $query->fetchAll();
}

送られてきたpostの中にユーザーID・ボタンを押したmessageの情報・押したボタンの情報があるので、それを照合し、あれば削除(MySQLから既存レコードをDELETE)・なければ作成(MySQLにINSERT)し、元のメッセージを状況に合わせて更新することで投票を実現しています。

所感

プログラムの部分で長くはなってしまいましたが、これでTODOアプリは完成です。シーズでは毎日の日次タスクと、月曜日に週次タスクを投稿する形で運用してますが、今のところ問題なく稼働しています。 こういったTODOリストの共有やリマインダ、GitHub/BitBucketの連携など、SlackAPIを応用することで、手軽に様々な機能を日々のコミュニケーションの中に自力で追加できるのはとても魅力的だと感じました。

アイデア次第ではもっと面白いことができそうなSlackAPI、もっと色々な活用方法を見出して社内に還元していきたいなあ…と目論んでおります!

Systems Manager セッションマネージャーを利用したEC2へのリモート接続

クラウド事業部の上野です。

AWSのプライベートなネットワーク(インターネット上から直接アクセスできないネットワーク)上に立てたEC2インスタンスへのsshアクセスはどのように行われていますでしょうか?よくある構成としてはパブリックなネットワーク(インターネット上からアクセスできるネットワーク)上にあるEC2インスタンスを経由してアクセスといったものがあります。俗に踏み台サーバやBastionサーバと呼ばれるものです。弊社でも踏み台サーバを用意することが多いですね。

ところがAWSにはEC2インスタンスへのアクセスをサポートするSystems Manager セッションマネージャーという機能があります。 これはEC2にインストールされているSSM エージェントを利用してリモート接続を行います。SSMエージェントを利用すればネットワーク的に接続できないEC2インスタンスへ接続することも可能ですし、sshを使わないのでsshdのサービスを停止してもアクセスすることができるという優れものです。長年サーバ管理者をやっていますが、sshdを止めれる日が来るなんて思いもしなかったです。

それでは試してみましょう。 プライベートネットワーク上にEC2インスタンスを起動し、セッションマネージャーを利用してアクセスするということをやってみたいと思います。

まず、AWSアカウント作成後に標準で準備されているVPCにプライベートネットワークを用意します。

f:id:seeds-std:20190917124214p:plain
ssm01

このプライベートネットワーク上にEC2インスタンスを立てます。このインスタンスはパブリックIPを持たないため外部から直接ssh接続することはできません。

f:id:seeds-std:20190917124322p:plain
ssm02

次にSystems Manager をEC2が利用できるようにIAMロールを作成してEC2インスタンスに設定します。 ロールに設定するポリシーは AmazonEC2RoleforSSM になります。

これで準備が整いました。では早速インスタンスに接続してみましょう。 AWSマネジメントコンソールより、Systems Manager -> セッションマネージャーを開きます。 セッションの開始というボタンを押すとインスタンスの一覧が表示されます。インスタンスを選択し、セッションの開始ボタンを押します。

f:id:seeds-std:20190917124836p:plain
ssm04

するとWebブラウザでコンソールの画面が開きます。 ユーザはssm-userというユーザで接続されているようですね。sudoコマンドを使ってroot権限得ることもできます。

f:id:seeds-std:20190917125109p:plain
ssm05

それでは試しにsshdのサービスを止めてみましょう。 リモートからsshdを止めるなんていう暴挙は初めてです。sshdを停止してもアクセスできていることが確認できます。

f:id:seeds-std:20190917125237p:plain
ssm09

セッションの履歴からどのIAMユーザで接続されたかを確認することができます。

f:id:seeds-std:20190917140113p:plain
ssm06

セッションの詳細な情報をログとして出力することも可能です。 CloudWatch Logsに出力するように設定するとこのようになります。実行されたコマンド一つ一つまで記録されていますね。

f:id:seeds-std:20190917135950p:plain
ssm007

S3にファイルとして保管させることも可能です。

f:id:seeds-std:20190917140412p:plain
ssm08

いかがでしたか。

比較的容易にセッションマネージャーを使ってアクセスすることができました。 セッションマネージャーのいいところは踏み台サーバが不要であるということだけではなく、EC2インスタンスへのアクセスをIAMユーザで管理できるということにあります。 ログもコマンドレベルで出力されていますので、いざという時の監査ログとしても利用できそうですね。 今後は踏み台サーバではなく、セッションマネージャーを使うことも考慮に入れていきたいと思います。

Laravelを学ぶ為、DockerでLaravelを動かせる環境を構築した

こんにちは、西山です。

今回からは麻雀に負けずともブログ記事を書こうと思います。

エンジニア35歳定年説を気にせず
PHPフレームワークとして有名なLaravelを勉強し始めましたので
学んだ事や経験した事を記載していきます。

会社ではDockerを立ち上げるだけで完全に自動化され
ファイルの修正が必要なく、Laravelが動作するDockerリポジトリが存在します。

それとは別に、自分で一からDockerの開発環境を作成してみました。

便利な物を効率よく使うのも重要ですが
学習の為、自分で作成したDocker環境にLaravelをインストールし
シンプルな内容のリポジトリを作成しました。

github.com

この作成過程や実際に作成したリポジトリの使い方を元に話を進めます。

尚、開発環境はMacで、Dockerは 「Docker Desktop for Mac」 を使用しました。

docs.docker.com

※Windowsでは、「Docker Toolbox」を使用しDockerの環境を用意しました。

■1. Laravelの学習

リポジトリを作成するにあたり、まずは下記の2つを取り組みました。

ドットインストール 「Laravel 5.5入門」
書籍 「PHPフレームワーク Laravel入門」

ドットインストールは短い動画が複数あり取り組みやすく分かりやすいですが
細かく気になった点が省かれていると感じました。
その後で、書籍で丁寧に一通り説明されているのを読み、理解が進みました。

全体の流れをドットインストールで学んでからだったので、書籍の細かい点も分かりやすく感じました。

Laravelはインストールも簡単ですし、ベースのファイルを作成する為の便利なコマンドも多く用意されています。

■2. リポジトリの作成「DockerへのLaravelのインストール」

元々PHPが動作するシンプルなDockerを作成していまして
そこにLaravelのインストールとプロジェクト作成を行い、今回のリポジトリを作成しました。

作業の流れを記載します。

Dockerのコンテナを作成・起動

docker-compose up -d

「web」という名前のコンテナを私は用意しています。

Webサーバーのコンテナに入る

docker-compose exec web bash

Webサーバーに入って、Laravelをインストールします。
まずはComposerのインストールが必要です

https://getcomposer.org/download/

下記の記載があるので、それぞれ1行ずつコマンドを叩き実行します

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"

php -r "if (hash_file('sha384', 'composer-setup.php') === 'a5c698ffe4b8e849a443b120cd5ba38043260d5c4023dbf93e1558871f1f07f58274fc6f4c93bcfd858c6bd0775cd8d1') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"

php composer-setup.php

php -r "unlink('composer-setup.php');"

ここまで実行すると下記のファイルが作成されています。
composer.phar

ここで、ComposerからLaravelをインストールします。
laravel_appという名前でプロジェクトを作ります。

プロジェクト作成

php composer.phar create-project --prefer-dist laravel/laravel laravel_app

インストールが完了しましたら、プロジェクトのディレクトリに移動しLaravelのバージョンを確認します。

ディレクトリ移動

cd laravel_app

Laravelのバージョン確認

php artisan --version

この artisan のコマンドはモデル、コントローラー、マイグレーション等の様々なベースファイルの作成ができたりなど
Laravel開発に置いて、非常に便利なコマンドです。
artisan は職人という意味なので、指示を出して使いこなすエンジニアは親方ですね。

下記のようにバージョンが表示されればOKです。

Laravel Framework 5.8.34

laravel

親方デビューです。

追加されたLaravelのファイルを git add して今回のリポジトリが作成できました。

■3. リポジトリの使い方「Laravelのリポジトリを git clone して動かす時の注意点」

しかし、別PCに git clone してdockerを立ち上げて動かそうとした所、動きませんでした。
別の現場では、すんなり親方になれませんでした。

エラーメッセージ

Warning:  require(/app/laravel_app/public/../vendor/autoload.php): failed to open stream: No such file or directory in /app/laravel_app/public/index.php on line 24

Fatal error:  require(): Failed opening required '/app/laravel_app/public/../vendor/autoload.php' (include_path='.:/usr/local/lib/php') in /app/laravel_app/public/index.php on line 24

先ほど自分で名前を決めて作成したLaravelのプロジェクトのディレクトリ「laravel_app」
下記にvendorディレクトリが存在していませんでした。

/laravel_app/vendor

artisanコマンドでプロジェクトを作った時には存在しましたが
vendorディレクトリが無いのでインストール

■【現在はDockerのwebサーバーのディレクトリ「laravel_app」にいる状態】

※ /composer.phar に「composer.phar」は存在します。

インストール

php ../composer.phar install

vendorディレクトリが作成されましたが、500エラーと表示されます。

500 Server Error

【Laravel】の環境変数設定ファイル「.env」が作成されていなかったので作成します。
Gitで管理されない状態になっていました。

/laravel_app/.envを作成

cp .env.example .env

Dockerで開発していますので、Dockerのデータベースの設定を確認します。

/docker-compose.yml の設定を確認

version: '3'

services:
  web:
    build: ./docker/web/
    depends_on:
      - db
    volumes:
      - ./:/app
      - ./docker/web/php.ini:/usr/local/etc/php/php.ini
      - ./docker/web/000-default.conf:/etc/apache2/sites-enabled/000-default.conf
    working_dir: /app
    ports:
      - ${WEB_PORT}:80
  db:
    build: ./docker/db/
    volumes:
      - ./docker/db/mysql:/var/lib/mysql
      - ./docker/db/init:/docker-entrypoint-initdb.d
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
      - MYSQL_DATABASE=${MYSQL_DATABASE}
      - MYSQL_USER=${MYSQL_USER}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD}
    ports:
      - ${DB_PORT}:3306

上記の {MYSQL_ROOT_PASSWORD} の記載は
【Docker】の環境変数の設定ファイル「.env」で管理しています。
※Laravelの環境変数の設定ファイルとは別ファイルで、リポジトリ直下に存在します。

Dockerの .env 場所

/.env

/.env の設定を確認

WEB_PORT=50012
DB_PORT=50013
MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=testdb
MYSQL_USER=test
MYSQL_PASSWORD=testpassword

Dockerのデータベースに接続する必要があるので
Laravelの.envのデータベース接続の箇所を修正

Laravelの .env 場所

/laravel_app/.env

Dockerからの接続になるので、/docker-compose.yml を確認し
「DB_HOST」は「db」とし「DB_PORT」は「3306」であることに注意してください。

/laravel_app/.env 修正内容

DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=testdb
DB_USERNAME=root
DB_PASSWORD=rootpassword

まだエラーが続きます。しぶといですね。

No application encryption key has been specified.

No application encryption key has been specified.

下記のコマンドを実行

キー作成

php artisan key:generate

キャッシュクリア

php artisan config:clear

これで、ようやくトップページが表示されました。
git cloneして、すぐに使えると思っていましたが、何点か注意が必要な状況でした。

私が作ったリポジトリに限らず、Laravelの .env はGit管理されない設定になっている為
他のLaravelのリポジトリを git clone して使えなかった場合には
上記の手順を参考にしていただければ幸いです。

あとがき

いくつになっても、新しい事を知ることは重要なのでめげずに進んでいきます。

健康診断もあり、30半ばになるとより一層健康が気になるところです。
では、そろそろ自分探し(再検査)に行ってまいります。

以上、西山でした。

P5.sketchpluginを使ってビジュアルプログラミングを学ぶ[超初級編]

こんにちは。WEB事業部デザイナーの河野です。

いつものように趣味でPinterestとTumblrの徘徊をしていると、
めちゃくちゃクールでかっこいいグラフィックを見つけました。

「これどうやってできているんだろう」と辿ってみるとそこには「processing」という言葉が。。

恥ずかしながら今までその言葉を聞いたことがある程度で実態を分かっていませんでした。
processingとはビジュアルデザインのためのプログラミング言語のことで初心者でも比較的始めやすいとのこと。

初心者でも始めやすいとはいえ、されど「プログラミング」。
もともと自分は紙媒体のグラフィックデザイナー出身で、
黒い画面とコードを見ただけで頭痛と鳥肌、冷や汗が全身の毛穴から噴き出るほどの拒絶反応がありました。
(よくWEBデザイナーになれたものだ…。シーズの採用陣、器が広い!)

しかし!
色々調べていくとp5.jsというProcessingをJavaScriptで書けるライブラリを
WEBデザインツールのSketchで再現できるプラグインがあることがわかりました。

その名も「P5.sketchplugin」というプラグインです。
www.jacopocolo.com

Sketchだったら睡眠時間よりも長い時間毎日触っているツールなので やりやすいかもしれない!と、早速使ってみました。

使ってみたらビックリ。

前述の通りプログラミングが全くわからない自分でも ビジュアルプログラミングを体形的に学べつつ、デザインツールとしても便利だったのです!

ということで、今回は「P5.sketchplugin」をご紹介します。

まずはインストール

1. ここから最新バージョンをダウンロード

2. zipファイルを解凍してプラグインをインストール。

3. Sketchを起動してメニューにPlugins > p5が表示されたらインストール完了です。

f:id:seeds-std:20190913173424p:plain
インストールできた状態

基本:簡単なグラフを作成する

無事インストールできたら、早速触ってみます。 Plugins > p5 > Edit and runを選択してプラグインを立ち上げます。

f:id:seeds-std:20190913173527p:plain
プラグインを立ち上げました!

最初は何も表示されていませんが、ここにコードを入力しPlay(実行)でアートボードに反映されます。

早速プリセットのデータを見てみましょう。

[Presets]のプルダウンメニューから[Pie chart]を選択して実行すると、、

f:id:seeds-std:20190913173651p:plain
円グラフができました!

コード部分の下記の数値を編集して実行すると円グラフに反映されます。

var percentages = [30,60,10];

f:id:seeds-std:20190913174108p:plain
編集してみました

他にもプリセットには棒グラフも用意されています。 管理画面のUIデザインなどで役立ちそうですね。

実践:コードを編集してモザイクパターンを作成する

ここからは応用で、パターンを作ってみたいと思います。
[Presets]の中から[Generative grid]を選択します。

f:id:seeds-std:20190913180011p:plain

これを元に、アートボードのサイズ、色、円の表示の確率などなど、
各種設定していくとモザイク模様ができます!

試しにシーズのみんなが大好きなRedBullカラーのモザイクを作ってみました。

f:id:seeds-std:20190913182648p:plain
Redbullカラーモザイク

上にテキストを乗せたりと、バナーやアイキャッチ画像制作にいろいろと応用できそうです。
f:id:seeds-std:20190913183035p:plain

今まで視界に入ると拒否反応が出ていた数字や関数たちも、
「ここを動かすための記述なのか!」と理解できれば慣れてくるものだということが分かりました。
これから仲良くなっていきたいです。

注意すべきこと

  • あくまでも表現できるのは静止画のみ(Sketchの競合、Figmaはアニメーション表現も可能らしい。。)
  • 制約が多いのでリファレンスを読んだ上で操作すると良いかも。
  • Sketchでグラフィックを編集してしまうと、コードには反映されないので注意。

まとめ

普段デザイン業務しかしていないので、数値をいじるだけでビジュアルが変化するということがそもそも新鮮でした。
これがビジュアルプログラミング・・!
長らくコード大嫌いデザイナーでしたが、
これを機にprocessingを本気で勉強してみようと思います!

次回(未定)、初級編で簡単な図形を自分で作ってみたいと思います! それではまた。

CircleCI + GitHub + ECR + ECS (+ Fargate) で継続的デリバリー環境を構成する

クラウド事業部の上野です。

AWSにあるコンテナサービスを使ってみたい!今後の弊社のサービスで活用できるかも!ついでにCIツールでデプロイまで自動化したい! ということでECS(Amazon Elastic Container Service)とECR(Amazon Elastic Container Registry)で継続的デリバリー環境を作ってみました。 今回はCIツールとしてCircleCIを利用してみます。

簡単に各サービスを説明しますと、

CircleCIはCI/CD(継続的インテグレーション/継続的デリバリー)を行うサービスです。

ECSはDocker コンテナをサポートするAWSのコンテナオーケストレーションサービスです。

ECRはAWS完全マネージド型のDockerコンテナレジストリです。

これらのサービスを使って、GitHubにプッシュしたら自動的にDockerイメージをビルドし、最終的にECSのコンテナにデプロイされるという環境を作ってみたいと思います。

今回の構成としてはこのような形です。

f:id:seeds-std:20190911152413p:plain
構成図

では、環境を作っていきましょう。

ECRの作成

まずはDockerのコンテナを保管するECRを作成します。 AWSマネジメントコンソールよりECRのダッシュボードを開き、リポジトリ名を入力して作成します。 今回はお試しということでnginxのDockerイメージを使います。

ECRにリポジトリを作成すると”プッシュコマンドの表示”というボタンが押せるようになります。 これはDockerイメージの作成からECRへのプッシュまで、具体的にどういうコマンドを実行すればいいか教えてくています。 親切ですね、具体的には下記のコマンドになります。

$(aws ecr get-login --no-include-email --region ap-northeast-1)
docker build -t seeds-test .
docker tag seeds-test:latest XXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/seeds-test:latest
docker push XXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/seeds-test:latest

上記コマンドを実行してECRにプッシュするとこのようになります。

f:id:seeds-std:20190911153941p:plain
ecr_push

ECSクラスター作成

ECRのリポジトリにDockerのイメージを準備できましたので、次はECSを準備していきます。 まずは土台となるECSクラスターを作成します。ECSクラスターとはコンテナインスタンスの集合体のことです。 コンテナインスタンスにはAWSがマネージドしてくれるAWS Fargateと自分自身で管理するEC2インスタンスの2種類がありますが、今回はAWSが管理してくれるFargateを利用します。 クラスター作成時にコンテナを動作させるVPCを新たに作成するか聞かれますが、今回は既存のVPCを使用するためVPCの新規作成は行わずにクラスター名だけ記入して作成します。

f:id:seeds-std:20190911155655p:plain
ecs-cluster

ECSタスク定義の作成

次はECSタスク定義を作成します。 ECSタスク定義とはアプリケーションの設計図です。どういったコンテナをどの程度のスペック(CPU、メモリ)で起動するかといった内容を定義します。 タスクの定義には起動タイプをFargateかEC2のいずれかを選択する必要があります。今回はFargateを選択します。

タスク定義名とタスクメモリとタスクCPUを指定し、それ以外はデフォルトのままにします。 設定の中段あたりにコンテナの定義という項目がありますので、そこで「コンテナの追加」ボタンを押してタスクで起動するコンテナの設定を行います。 コンテナ追加の画面でコンテナのイメージを選択する部分がありますので、ここでECRリポジトリに登録したイメージのURIを指定します。 ポートのマッピングは今回はnginxのコンテナですのでhttpの80番ポートをマッピングします。

f:id:seeds-std:20190911161504p:plain
ecs

これでタスク定義の作成は完了です。 タスク定義は今後リビジョンとして管理され、更新する度にリビジョンの数値が増えていきます。

ECSサービスの作成

ECSサービスとはECSクラスター上で起動させるタスクの数やAutoScalingの設定を管理します。 起動タイプはFARGATEを選択し、タスク定義とクラスターは事前に作成したものを指定します。 タスクの数の項目でサービス上で何個のタスクを起動させるかを指定できますので、今回はタスクの数を2に指定して、nginxのコンテナが2つ(タスクごとに1つのコンテナ)起動するようにします。

f:id:seeds-std:20190911165301p:plain
ecs-service

次にネットワーク構成を定義します。 ここでECSサービスが起動するVPCや利用されるセキュリティグループ、ロードバランサーを指定します。 VPCやセキュリティグループ、ロードバランサは事前に用意しておいたものを指定しています。

f:id:seeds-std:20190911165942p:plain
ecs-service
f:id:seeds-std:20190911165957p:plain
ecs-service

サービスを作成するとサービスで定義した内容でコンテナが起動してきます。

f:id:seeds-std:20190911173503p:plain
ecr-service

この状態でELBのDNS名にアクセスするとnginxのウェルカムページが表示され、コンテナが正常に稼働できていることを確認できます。

ここまででECRとECSの構築は完了です。

CircleCIとGitHubの設定

ここまでの作業でAWSを利用したコンテナサービスとしては稼働していますが、CircleCIとGitHubを使って継続的デリバリーな環境を作ります。 まず、CircleCIからECRとECSを操作するためのIAMユーザ(CircleCI用のアクセスキー)を作成します。ポリシーは下記のものを付与してください。作成時に表示されるアクセスキーとシークレットアクセスキーは後ほど利用しますのでメモしておいてください。

ユーザ名 circleci
ポリシー AmazonEC2ContainerRegistryFullAccess
     AmazonEC2ContainerServiceFullAccess

次にGitHubにDocker用のリポジトリを作成します。

f:id:seeds-std:20190911174844p:plain
github

GitHubでリポジトリが用意出来たらCircleCIにアクセスします。CircleCIではGitHubのアカウントを利用してサインアップできます。 GitHubのアカウントを利用してサインアップするとGitHubに用意したリポジトリが表示されていますのでFollowします。

f:id:seeds-std:20190911175327p:plain
circleci
これでGitHubとCircleCIの連携の準備ができました。

次にCircleCIのジョブの設定を行います。CircleCIのジョブの画面よりEnvironment Variablesを開き、環境変数をセットします。

  • AWS_ACCESS_KEY_ID(circleciユーザのアクセスキー)
  • AWS_SECRET_ACCESS_KEY(circleciユーザのシークレットアクセスキー)
  • AWS_ACCOUNT_ID(AWSのアカウント番号)
  • AWS_ECR_ACCOUNT_URL(ECRのリポジトリURL XXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/seeds-test)
  • AWS_REGION(ap-northeast-1)

GitHubにプッシュされた場合にCircleCIの動作を制御するための設定ファイルを用意します。 .circleciというフォルダを作成し、その中にconfig.ymlというファイルを作成します。 config.ymlでCircleCIの動作を制御するのですが、ECRリポジトリへのアップロードやECSのタスク定義やサービスを更新するためのOrbs(※ジョブ、コマンドなどの設定要素をまとめた共有可能なパッケージのこと)をCircleCIが公式に提供しています。 これらを利用してGitHubにプッシュされた場合、DocerkイメージをビルドしてECRにアップロードし、アップロードされたイメージを元にECSのタスク定義とサービスを更新するといった内容のconfig.ymlを作成します。

circleci/aws-ecr@6.3.0

circleci/aws-ecs@0.0.11

config.ymlの内容

version: 2.1

orbs:
  aws-ecr: circleci/aws-ecr@6.1.0
  aws-ecs: circleci/aws-ecs@0.0.8

workflows:
  build_and_push_image:
    jobs:
      - aws-ecr/build-and-push-image:
          region: AWS_REGION
          account-url: AWS_ECR_ACCOUNT_URL
          repo: 'seeds-test' # GitHubのリポジトリ名
          tag: "${CIRCLE_SHA1}"
      - aws-ecs/deploy-service-update:
          requires:
      - aws-ecr/build-and-push-image
          family: 'seeds-test-task' # ECSのタスク定義名
          cluster-name: 'seeds-test-container' # ECSクラスター名
          service-name: 'seeds-test-service' # ECSのサービス名
          container-image-name-updates: 'container=seeds-test,tag=${CIRCLE_SHA1}' # タスク定義で指定しているコンテナ名

それではnginxのDocerfileと作成したconfig.ymlをGitHubにプッシュしてみましょう。

f:id:seeds-std:20190911193036p:plain
github

CircleCIをみるとプッシュを検知してジョブが動いていることを確認できます。

f:id:seeds-std:20190911193415p:plain
circleci

ECRの画面をみると新しいイメージが登録されていることを確認できます。

f:id:seeds-std:20190911193818p:plain
ecr

ECSのタスク定義も更新されています。

f:id:seeds-std:20190911194011p:plain
ecs

ECSのサービスで指定されるタスク定義も新しいものに更新され、自動でAutoScalingが実行されています。

f:id:seeds-std:20190911194141p:plain
ecs

これでCircleCI + GitHub + ECR + ECS で継続的デリバリー環境が構築できました。 今回構築した環境はとりあえず動く環境という状態を作りましたが、細かい設定をしていけばより柔軟な環境が作り上げることができます。例えばdevelopブランチにプッシュした場合は開発環境のECSにデプロイ、prodcutブランチにプッシュした場合は本番環境のECSにデプロイするといったことも可能です。

今回は案件の関係でCircleCIを利用する機会があったため、CIツールにCircleCIを利用しましたが、AWSにはもともとAWS CodeBuildやAWS CodePipelineなどのCIツールが用意されています。次回はこれらを使って継続的デリバリーの環境を作ってみたいと思います。