月別: 2019年9月

Bitbucket PipelinesでDocker Composeプロジェクトをインテグレーションテストする

はじめまして。小國です。

今回は、Docker Compose で構築されたプロジェクトを、Bitbucket Pipelines を使ってインテグレーションテストする方法をご紹介したいと思います。

はじめに

シーズのプロジェクトの開発環境・手法は、主に以下のような構成となっています。

  • VCS に Git を使い、Bitbucket にホスティングしている
  • 各プロジェクトは Docker Compose を使い、ローカル環境を構築し、開発を行っている
  • PHPUnit などを使って、テストケースを書いている

Bitbucket Pipelines で Docker Compose を使うには

Bitbucket には Pipelines という CI/CD サービスがあり、Docker を使ったテストが行なえます。

ですが、Pipelines で Docker Compose を使うためには、自身で Docker Compose バイナリを作成しなければなりません。

参考: https://ja.confluence.atlassian.com/bitbucket/run-docker-commands-in-bitbucket-pipelines-879254331.html

今回は、Docker Compose バイナリの作成・公開し、サンプルプロジェクトを使って、Pipelines で PHPUnit のテストを実行するまでを行います。

やってみましょう

主な流れは以下のようになります。

  1. Docker Compose バイナリの作成と、作成したバイナリを Docker Hub に公開
  2. サンプルプロジェクトの作成
  3. bitbucket-pipelines.yml の作成
  4. Bitbucket で Pipelines の有効化

Docker Compose バイナリの作成と、作成したバイナリを Docker Hub に公開

なお、https://hub.docker.com/r/seedsstd/seeds_bitbucket_pipelines にほぼ同様のイメージを公開していますので、こちらを使用する方は、このステップは不要です。

  • Docker Compose バイナリの作成
$ mkdir seeds_bitbucket_pipelines && cd seeds_bitbucket_pipelines
$ cat <<EOF > Dockerfile
FROM docker:stable
# Add python pip and bash
RUN apk add --no-cache py-pip
RUN apk add --no-cache python-dev libffi-dev openssl-dev gcc libc-dev make
RUN apk add --no-cache bash
# Install docker-compose via pip
RUN pip install --no-cache-dir docker-compose
EOF
  • 作成したバイナリを Docker Hub に公開
docker build -t <YOUR_ACCOUNT>/seeds_bitbucket_pipelines:stable .
docker push <YOUR_ACCOUNT>/seeds_bitbucket_pipelines:stable

<YOUR_ACCOUNT> には、自身の Docker Hub アカウント、またはオーガニゼーションを指定してください。

docker build -t seedsstd/seeds_bitbucket_pipelines:stable .
docker push seedsstd/seeds_bitbucket_pipelines:stable

サンプルプロジェクトの作成

サンプルプロジェクトを作成ます。ファイル構成、内容は以下のとおりです。

$ tree
.
├── composer.json
├── composer.lock
├── composer.phar
├── docker-compose.yml
├── php-apache
│   └── Dockerfile
└── tests
└── SampleTest.php
2 directories, 6 files
  • composer.json
{
"name": "seeds-std/blog_bitbucket_pipelines",
"authors": [
{
"name": "SEEDS Co.,Ltd",
"email": "info@seeds-std.co.jp"
}
],
"require": {},
"require-dev": {
"phpunit/phpunit": "^8.3"
}
}
  • docker-compose.yml
version: "3"
services:
web:
build: php-apache # php7.2-apache に git が入っていなかったため作成
volumes:
- ./:/var/www/html
  • php-apache/Dockerfile
FROM php:7.2-apache
RUN apt-get update -y && apt-get install -y git
  • tests/SampleTest.php
<?php
class SampleTest extends \PHPUnit\Framework\TestCase
{
/**
     * @return void
     */
public function testTrueIsTrue()
{
$this->assertTrue(true);
}
}

ここでのポイントは docker-compose run --rm web bash -c "php composer.phar install && vendor/bin/phpunit tests/SampleTest.php" というような形で、ホスト側から PHPUnit のテストが実行できることです。

ためしに、実行してテストが通ることを確認しましょう。

$ docker-compose run web /bin/bash -c "php composer.phar install && vendor/bin/phpunit tests/SampleTest.php"
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Nothing to install or update
Generating autoload files
PHPUnit 8.3.5 by Sebastian Bergmann and contributors.
.                                                                   1 / 1 (100%)
Time: 545 ms, Memory: 4.00 MB
OK (1 test, 1 assertion)

良さそうですね。

bitbucket-pipelines.yml の作成

以下のような bitbucket-pipelines.yml を作成し、リポジトリの直下に含めます。

内容は、上記で作成した Docker Compose バイナリを使い、そのホストからコンテナを起動しテストする内容になっています。

image: seedsstd/seeds_bitbucket_pipelines:stable
options:
docker: true
default_script: &default_script
- docker-compose run --rm web /bin/bash -c "php composer.phar install && vendor/bin/phpunit tests/SampleTest.php"
- docker-compose down
pipelines:
default:
- step:
script: *default_script

Pipelines の有効化

最後に、Bitbucket の画面から Pipelines を有効化します。

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

Pipelines を有効化すると、以下のように bitbucket-pipelines.yml で指定したテストが実行されます。

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

テストが全て正常に終わり、以下のようになればおkです。

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

最後に

Bitbucket Pipelines で Docker Compose を使用したプロジェクトのテストを実行することができました。

LaravelのPivotのincrementingがfalseになっていた件

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

みなさま、初めまして。

WEB事業部の李です。 どうぞよろしくお願いいたします。
最近、カップヌードルの味噌味にハマっています。
昼は基本おむすびです。

www.nissin.com

本題

さて、本日は、
Laravelを5.6から5.8にアップグレードした際に、
Pivotクラスのidをデフォルトでは取得できなかった話を書きたいと思います。

結論

先に結論ですが、LaravelのPivotのincrementingが5.8からdefaultでfalseに設定されていたためでした。
trueにオーバーライドすると取れました。
オーバーライドもどうだろうというのはありますが。。

経緯など詳細

Laravelのバージョンが5.6の時に、中間テーブルを複数またぐような構造のデータを作成する必要がありました。
実装としては、pivotクラスのormを利用して登録後のidを取得し、さらに中間テーブルを作成するというプログラムが既に作られていました。

モデル->Hoge(※中間テーブル)->Hoge2(※中間テーブル)->….

上の例で、Hoge2に、Hogeのidを登録するという流れですが、
5.8へアップグレードするとhogeのレコードを作成後idが取れなくなっていました。。。
※Hogeのidは、auto_incrementで定義されています。

$hoge = Hoge::create($data); // $hoge->id 取れない。。
$hoge = new Hoge;
・
・
・
$hoge->save();  // $hoge->id 取れない。。

まじかよ。。と、もう一度アップグレードガイドを見たりしたのですが、
特に記述はなく、あの手この手と試してみたりしてたのですが、やはり取れず。。
まさかのlastInsertIdメソッド記述しないといけないのかと若干気持ちが沈んでいた頃、
あるエンジニアの方から、もしかしてこれじゃないですか?と指摘を受け、

venderの方のpivotクラスをのぞいてみると、

public $incrementing = false;

の記述がありました。
そこで、Modelクラスのsaveメソッドを少し追いかけてみると、ありました。

performInsertメソッド!これやん!!と。。

この中で、下記のような記述がありました。

//・・・省略
// If the model has an incrementing key, we can use the "insertGetId" method on
// the query builder, which will give us back the final inserted ID for this
// table from the database. Not all tables have to be incrementing though.
$attributes = $this->getAttributes();
if ($this->getIncrementing()) {
$this->insertAndSetId($query, $attributes);
}
// If the table isn't incrementing we'll simply insert these attributes as they
// are. These attribute arrays must contain an "id" column previously placed
// there by the developer as the manually determined key for these models.
else {
if (empty($attributes)) {
return true;
}
$query->insert($attributes);
}
//・・・省略

“which will give us back the final inserted ID”<-まさにですね。

getIncrementingメソッドは下記のような記述でした。

public function getIncrementing()
{
return $this->incrementing;
}

念の為、Laravelframeworkリポジトリで、バージョン5.6のpivotクラスを見てみると、
incrementingプロパティの記述がありません。
PivotクラスはModelクラスを継承しており、Modelクラスのプロパティには、$incrementing=trueとあったので、
バージョン5.6では、idを取得出来ていたのですね。

github.com

ちなみに、create()メソッドは、Illuminate\Database\Eloquent\Builderクラスのcreateメソッドで、結局saveを使っているみたいです。

public function create(array $attributes = [])
{
return tap($this->newModelInstance($attributes), function ($instance) {
$instance->save();
});
}

というわけで、問題のHogeクラスに、

public $incrementing = true;

と、オーバーライドするとidが取れるようになりました。

frameworkでの詳しい経緯は下記のPRご参考ください。

github.com

以上です。
すでに6.xがLTSとなってますが、Pivotクラスのincrementingプロパティのデフォルトはfalseになっていますので、
この記事が皆様のご参考になればと思います。

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ツールが用意されています。次回はこれらを使って継続的デリバリーの環境を作ってみたいと思います。

ISUCON 9 予選に京都スイーツ(b・ω・)bで参加して本戦出場できました!(15,490イスコイン / PHP / 総合11位?)

クラウド事業部の原口です。

毎年恒例のISUCONに参加してまいりました。使用言語はPHPです 。

isucon.net

今年は同僚のkawakattunとkuuと僕(cs_sonar)の3人で参加してまいりました!
予選の参加者合計 1561名だそうで、、、すごく大きいイベントになりましたね。
僕は9回目。kawakattunは3回目、kuuは初めてのisuconです。
毎年「今年こそは!」と心に闘志を燃やして参加し、いつも予選敗退するのですが今年はなんと本戦出場できました!

やったーーー嬉しい!

まがいなりにもCTOなので毎年ISUCON出るたびに社内人権が脅かされるのですが、、、今年は人権があってよかったです。
最終的な構成は WEB+Proxy / DB の2台構成でした

前日 && 前々日

業務終わりにISUCON4で練習。ISUCON4を選んだのはAWSにAMIがあってベンチ内包型だったからです。
役割分担などは正式に決めてないのですが、流れで以下のようになっていました

僕 -> インフラ全般
kawakattsun -> アプリ全般
kuu -> 実装と仕様チェック、レギュレーションチェックの情強

当日の流れ

設営をさくっと終わらせて開始!10分遅れたおかげで朝ごはんをたべきれました。
とりあえずアリババクラウドで共有されたイメージで5台ほど立ち上げ。
それぞれのメンバーで触れるようにしました

ベンチを流しても同じ点数しか出ないと思ったら、トランザクション成功数でなるほどな、と思った記憶があります。
ひとまずDB側のitemsにインデックス貼りを実施。

ALTER TABLE items
ADD INDEX seller_id_idx (`seller_id`),
ADD INDEX status_idx (`status`),
ADD INDEX buyer_id_idx (`buyer_id`);

mysqlの最低のチューニング

innodb_flush_log_at_trx_commit = 0
innodb_buffer_pool_size=1000M
innodb_flush_method = O_DIRECT

上限をあげておく

systemctl

net.ipv4.ip_local_port_range = 10240 65000
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_wmem=4096 65536 16777216
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 3
net.core.netdev_max_backlog = 30000
net.ipv4.tcp_no_metrics_save=1
net.core.somaxconn = 262144
net.ipv4.tcp_syncookies = 0
net.ipv4.tcp_max_orphans = 262144
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_max_tw_buckets = 56384

limits.confは既に65535に設定されていました。

これらを実行してもスコアは全然変わらなく、、、だけど明らかにmysqlの負荷などは減っていいたので
一旦還元率を1にしてみたら6410イスコインになってこの時のTOPに!

引き続いてアプリ側の改修を実施。
ここはkawakattunとkuuがやってくれました
このあたりですでに13:00くらい。

・categoryをすべて配列でプログラム側で持つ
・statusのSQLのINを消す
・セッションにユーザーデータを入れる
・transaction で外部api叩かなくする

などの改修を行ったのですが、特にスコアに変化なく…
同時に行っていたnginxの改修を反映したところスコアが10,000まで上昇。
インフラで止まってた部分を修正する事でアプリの修正も活きた感じがします

nginx

・静的ファイルをnginxで返すように修正
・Cache-Controleでmax-ageとかpublicとかつける
・gzip圧縮するよう設定
・worker_connections 100000
・worker_processes 8
・アクセスログ破棄

しかし、ここからが低迷。
発生する503のエラーのログがどこに吐かれるのかわからずphpのログ出し設定をいろいろいじいじしてたのが結局時間がかかってしまいました。
php実装にはmonologが入ってる事に気づいたのが後半で…
エラー内容がログなくてもかろうじて気づけるSQLのシンタックスエラーじゃなければ死んでたかもしれないです。
またこの時にアクセスログを切ってしまったのは早すぎた。意外とtail -fしてると気づける事ってあるよなーと後から思いました。
/buyが詰まってたのは多分気づけてたと思います。

この間、ボトルネックは完全にphp側に傾いていたので、DBサーバーを分ける事を決意。
分けた時に以下の点にはまり1時間くらいはかかってしまったかもしれないです

・プログラム内のmysqlサーバーのIP指定
・env.shでのmysqlサーバーの指定 ←はまる
・アリババクラウドのセキュリティグループ修正 ←はまる
・my.cnfでbind 127.0.0.1 になっているのを0.0.0.0に修正 ←はまる

普段RDSとかELBで楽してるツケが来ている感じがしました。
これでメモリに余裕ができたのでphp-fpmの設定を変更

php-fpm

以下を60くらいに設定
pm dynamic
pm.max_children
pm.start_servers
pm.min_spare_servers
pm.max_spare_servers
・読まれてるxdebugを切る
・OPCacheを有効化

ここで13,000くらいでした。
また、redisを入れようとしたのですが、うまく立ち上がらず諦め。。。(IPv6系の問題だったそうです)
php-fpm側のネックだったので、残り1台を使って処理を2台に分けようとしましたが、上記のredisが入らないのでセッションどう共有するかという問題と 画像をどう共有するかという問題があって時間的に不可能と思い、断念しました。
/loginのURLに絞る事を考えれたら良かったのになー、と感想部屋見て悔しかったです。

この間にアプリチームが結構長い間、行っていた改修がついに反映されます

・/users/transactions , /new_itemsのn+1をsqlのjoinで回避
・新着一覧から自分のitemとsold outをsqlで除外

自分のitemとsold outをsqlで除外する改修は、最初はカテゴリーのitemsのほうも一緒の対応してたらベンチでitemがありませんってなってしまい。
この改修はすごく時間がかかりました。
Kuuがベンチのエラーをきっちり見てくれて、エラーはカテゴリーの方だけだ、と気付き、新着一覧からのみ除外する事でベンチが通りました。
これが、15790で今回の最高スコア。

最後の17:30からはもう作業は停止。
再起動試験とベンチガチャ回しに勤しみました。
結果は残り5分くらいで 15,490 が出て作業を完全にやめました!

振り返り

f:id:cs_sonar:20190909231445p:plain

考えてみたら、サーバーのチューニング以外はほとんど何もできていません。
感想部屋でのお話を見て「なるほどー」という感じで… /buy の対策などは何もできていなかったりしていました。
本戦までにはプロファイラーをきちんと使い、ボトルネックをきちんと計測できるようにしたいと思いました。
ただ、今回のISUCONがプログラムが膨大だったので、まともに計測してたら何もできずに終わってたかもしれないな、とも思いますので…結果オーライです

弊社のエースエンジニアのkawakattunはもちろんの大活躍でしたが、
今回初参加のkuuがレギュレーションにしっかり目を通し、それを踏まえた案をだしてくれるのはとてもよかったです
僕とkawakattunは多分、ゲームの説明書読まずに始めるタイプなので…w
そういう意味でのチームとして相性がよく、仲間に恵まれた、というのを実感しています。

今まで予選を突破した事がなかったので嬉しいです。
これまでのISCUONを振り返ると惜しい回も結構多かったなーとしみじみ…


ISUCON1 京都スイーツ : fail

ISUCON2 京都スイーツ : (記録がなかったので不明)
#isucon2にていいかんじにスピードアップできなかった話 – SEEDS Creator's Blog

ISUCON3 うさぎ工房 : 5300 予選敗退
isucon3 予選で敗退しました(うさぎ工房) – SEEDS Creator's Blog

ISUCON4 京都スイーツ : 37513 予選敗退&失格
#isucon 4 予選に参加しました(スコア 37513) – SEEDS Creator's Blog

ISUCON5 京都スイーツnext : 13094 予選敗退
ISUCON5に「京都スイーツnext」で参加してきました – SEEDS Creator's Blog

ISUCON6 京都スイーツ : 17000くらい 予選敗退

ISUCON7 ガトリンガー葉の仲間たち : 72,285 55位くらい 予選敗退
ISUCON7に「ガトリンガー葉の仲間たち」で参加して今年も惨敗しました – SEEDS Creator's Blog

ISUCON8 ドラえもんズ : 30,699 27位 予選敗退

ISUCON9 京都スイーツ : 15,490 11位くらい 本戦出場!!


毎度の事ながら、本当に楽しかったです。
運営の皆様、本当にありがとうございました!
そして、今年は本戦でも宜しくお願い致します!

© SEEDS Co.,Ltd.