SEEDS Creator's Blog

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位くらい 本戦出場!!


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

AWS サーバレスでlibreofficeを使ってExcelをPDF変換

クラウド事業部の川勝です。

弊社のプロジェクトのいくつかでは、帳票のPDF出力にExcelで作成したものをPDF変換かけて使用するというものがあります。
Excel->PDF変換にはlibreoffceのheadlessモードを使用しているのですが、今回その仕組みを汎用的に使えるようにサーバレスで構築してみたのでご紹介いたします。

要件

  • ExcelファイルをWEB APIにPOSTしたらPDF変換されて返ってくる
  • 今回はbase64エンコードしたデータをjsonでやり取りする方式(理由は後述)

構成

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

Serverless libreoffice

まずAWS Lambdaでlibreofficeが使えるのか?(外部コマンドあるか)というところからはじめました。
外部コマンドでsofficeがあるかどうか。
ちょっと古い2014年の記事ですがどうもなさそうです...

AWS Lambdaをいろいろ暴く - Qiita

(ちなみにamazon linuxベースとのことなので大体の外部コマンドは使用可能ですね。)

自力でインストールするしかない、、できるのか、、?と調べていたところ以下を発見いたしました。

serverless-libreoffice/STEP_BY_STEP.md at master · vladgolubev/serverless-libreoffice · GitHub

こちらのStep:1ではlibreofficeをコンパイルするところから始まっていますが、リポジトリ内にコンパイル済みのファイルがありますのでそちらを使用します。(Step:2)

S3にserverless-libreofficeをアップロード

$ aws s3api create-bucket --bucket lambda-libreoffice-demo
$ curl -L https://github.com/vladgolubev/serverless-libreoffice/releases/download/v6.1.0.0.alpha0/lo.tar.gz -o lo.tar.gz
$ aws s3 cp lo.tar.gz s3://lambda-libreoffice-demo/lo.tar.gz --acl public-read-write

これで前準備は完了。 

Lambda実行時にアップロードしたファイルを展開して使用できるようにします。
今回はgoで実装していきます。

// main.go
func init() {
    cmd := "curl https://" + os.Getenv("S3_BUCKET_NAME") + ".s3.amazonaws.com/lo.tar.gz -o /tmp/lo.tar.gz && cd /tmp && tar -xf /tmp/lo.tar.gz"
    exec.Command("sh", "-c", cmd).Run()
}

main.goのinitでs3から取得したファイルを /tmp に展開します。
initで呼び出すことでlambdaコンテナの起動時のみ実行されます。
注意点はLambdaでは /tmp 以下にしか書き込み権限がないのと容量制限(500MB)があります。

よくある質問 - AWS Lambda |AWS

Q: AWS Lambda 関数のためにディスクにスクラッチスペースが必要な場合はどうすればよいですか?

各 Lambda 関数では /tmp ディレクトリに 500 MB の容量 (非永続型) を割り当てることができます。

ひとまずこれで /tmp/instdir/program/soffice として実行可能になります。 受け取ったExelファイルは /tmp/sample.xlsx にファイルとして保存しているとして、以下の様な感じで実行できました。

const convertCommand = "/tmp/instdir/program/soffice --headless --invisible --nodefault --nofirststartwizard --nolockcheck --nologo --norestore --convert-to pdf --outdir /tmp %s"
command := fmt.Sprintf(convertCommand, "/tmp/sample.xlsx")
exec.Command("sh", "-c", command).Run()

/tmp/sample.pdf が生成されるので、これをbase64エンコードしてjson形式で返却します。

...がしかし、実行はできましたが、このままだと日本語が文字化けてしまいます。
日本語フォントに対応が必要そうです。

日本語フォント

「lambda 日本語フォント」とかで調べていると以下の記事にあたりました。

AWS LambdaでPhantomJS日本語フォント対応 | RCO Ad-Tech Lab Blog

Lambdaの実行環境にフォントを追加する - Qiita

ようはfont-cache生成できればよさそうです。
上記記事では生成したfont-cacheをデプロイパッケージに含めていましたが、initでlibreofficeの展開もしているのでそこでfont-cache生成したらいいかな、と思ったのでその方向で実装しました。

initに追加します。

// main.go
func init() {
    cmd := "curl https://" + os.Getenv("S3_BUCKET_NAME") + ".s3.amazonaws.com/lo.tar.gz -o /tmp/lo.tar.gz && cd /tmp && tar -xf /tmp/lo.tar.gz"
    exec.Command("sh", "-c", cmd).Run()

    os.Setenv("HOME", os.Getenv("LAMBDA_TASK_ROOT"))
    cmd := "mkdir -p /tmp/cache/fontconfig && fc-cache " + path.Join(os.Getenv("HOME"), ".fonts")
    exec.Command("sh", "-c", cmd).Run()
}

.fonts ディレクトリに使用するfontファイルをデプロイパッケージに含めてlambdaにアップロードします。

以下ディレクトリ構成でzipに固める

.
├── .fonts
│   ├── hoge.ttc
│   └── fuga.ttc
├── .fonts.conf
└── main

main goのbuild済みファイルです。 .fonts.confは参考URLのまま以下の設定にしています。

<fontconfig>
  <cachedir>/tmp/cache/fontconfig</cachedir>
</fontconfig>

これで実行するとPDFに日本語が含まれていてもOKになりました!

Timezone

構築後に社内で試してもらっていると日付関係が変?という報告がありました。
LambdaのデフォルトtimezoneはUTCなのでExceleで =Now() とかするとUTCの時刻になるようです。
これは対応が簡単で、lambdaの環境変数設定で解決。
TZ をkeyにして設定可能でした。

key: TZ
value: Asia/Tokyo

ちなみに日付の並び順は変わらなかったので、Excel側でフォーマット指定してもらうことにしました...

=TEXT(NOW(),"yyyy/MM/dd hh:mm:ss")

これにて無事日本語が含まれるExcelデータもPDF変換可能です!

その他ハマったところ

バイナリメディアタイプ

要件のところで書いていた話

  • ExcelファイルをWEB APIにPOSTしたらPDF変換されて返ってくる
  • 今回はbase64エンコードしたデータをjsonでやり取りする方式(理由は後述)

API Gatewayの設定でバイナリメディアタイプを設定することで、ファイルアップロードしたらPDFがそのままダウンロードできる...と思ってやっていたのですが、どうにもうまくいかず断念しました。(これは自分のgo言語の力量不足かもしれませんが..)

あと、ブラウザからではリクエストヘッダーに Accept: application/pdf が付与されないためCloudFrontをかまさないといけない、、、ということなので今回はjsonでやり取りでいいかな、となったのも理由です。

送信するときにExcelデータをbase64エンコードして、変換したPDFもbase64エンコードして返却させています。
送信側でプログラム書く場合はこの方がシンプルに構築できるのでいいかな、と感じています。

API Gatewayのタイムアウト

API GatewayのタイムアウトはLambdaより短いです。
Lambda ・・・最長15分

よくある質問 - AWS Lambda |AWS

Q: AWS Lambda 関数はどれくらいの時間実行できますか?

AWS Lambda 関数は、1 回あたりの実行時間を最長 15 分に設定することができます。タイム> アウトは 1 秒から 15 分までの間で任意に設定できます。

API Gateway ・・・最長29秒

Amazon API Gateway の制限事項と重要な注意点 - Amazon API Gateway

統合のタイムアウト・・・Lambda、Lambda プロキシ、HTTP、HTTP プロキシ、AWS 統合など、すべての統合タイプで 50 ミリ秒~29 秒。

また制限の解除も対象外です。

Lambdaのデフォルトのメモリ(128MB)だとタイムアウトすることが多かったのでしたので256MBで実行するようにしました。

最後に

サーバレス開発で今回のように外部コマンドもLambda上で展開してしまえば結構なんでもできそうだなと思いました。
API gatewayのバイナリメディアタイプに関しては引き続き調べていきたいところです。

あと今回はサーバーレスアプリ自体にはあまり触れませんでしたが SAM CLI を使用して開発、デプロイしています。

GitHub - awslabs/serverless-application-model: AWS Serverless Application Model (SAM) is an open-source framework for building serverless applications

このあたりの話はまたの機会に...

以上です!

Amazon Elastic File System (Amazon EFS) におけるバーストとプロビジョニングの変更を自動化する

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

AWSのEFSが発表されてから「これを待っていた!!」というエンジニアの方も多かったと思いますが、割と落とし穴的な制限が多く、 その中の一つとしてEFSの「バースト」と「プロビジョニング」の2つのスループットモードがあります。

バーストスループットモードとは何か。

誤解を恐れず簡単に説明しますと、スマフォの帯域制限みたいな機能となります。 EFSではバーストクレジットと呼ばれる値があり、このクレジットがある間は非常に高速なスループットでデータのやり取りが可能なのですが クレジットがなくなると制限がかかりめちゃくちゃ低速なファイルストレージになってしまいます。 これはプロダクション環境ではサーバ障害とも言えるレベルでの低速さになります。

バーストクレジットを回復するには?

基本的にファイルストレージを使っていなければ回復していきます。 そのため、EFSの利用頻度がクレジットの回復力を下回る場合はクレジットは減る事がありません。

また、使っているEFS内のサイズによってもクレジットのそもそもの量が異なります。 大きいファイルが多数あるなど、EFS内のデータ量が多ければクレジットのそのものの絶対量が多く付与されます。

これを理解してない場合に起こる問題

基本的には運用時、EFSはバーストスループットモードとして動作する事がほとんどですが、 よくある事例としては「テスト段階では高速で快適に動いていたのに、本番運用していたら急激に低速になり障害があった」という事例です。

CloudWatchのメトリクスに「BurstCreditBalance」というメトリクスがありますが、こちらがバーストクレジットで、 ゼロになると低速なストレージになってしまいます。

f:id:cs_sonar:20190826124939p:plain

上記のようなグラフの場合は運用で徐々にクレジットが減っていってるのでずっと運用しているといつかは障害となりそうです。

プロビジョニングスループット

さて、バーストクレジットを使い果たして低速となってしまった場合はどうすればいいのでしょうか?

もちろん「大きなファイルを作ってバーストクレジットを増やす」というのも一つの解決策ですが、 一般的にはバーストモードからプロビジョニングスループットというモードへ変更する事で対策できます。

プロビジョニングスループットは帯域保証のような機能で50(MB/秒)といった値を設定する形でスループット値を保証させるモードです。 このモードではクレジットなどに関係なく設定した速度は必ず保証されます。

しかし・・・このプロビジョニングスループットは非常に高額です。 1MB/秒 の保証にあたり月額7.2USDもかかります。 件のように50MB/秒を保証させると月額360USD ≒ 38,000円。 ただでさえEFSは高額なのにこの価格は無視できる額ではありませんよね。

プロビジョニングとバーストクレジットを交互に設定をする事でコスト削減を図る

実はバーストクレジットからプロビジョニングスループットに変更すると プロビジョニングスループット中はバーストクレジットが回復していきます。

f:id:cs_sonar:20190826125825p:plain

8/23の途中からプロビジョニングスループットとしていますが、3日ほどでクレジット最大値まで回復していますね。

つまり、最もコスト効率がいいのは

クレジットを限界まで使い直前でプロビジョニングスループットに変更 。 クレジットが回復したらバーストスループットに変更。

という形となります。

CloudWatch + SNS + lambda を使ったバーストとプロビジョニングの変更を自動化

ここからが本題。 上記の形がコスト効率が最もよいのですがこんなの人の手でぽちぽちやってれないので自動化します。

まずは、バーストとプロビジョニングの変更を行うlambda関数を作成します バーストモードにするものとプロビジョニングにするものの2つ

EFS-to-bursting

console.log('Loading function');
var https = require('https');
var http = require('http');

const aws = require('aws-sdk');
var efs = new aws.EFS();

exports.handler = (event, context) => {
    (event.Records || []).forEach(function (rec) {
        if (rec.Sns) {
            var message = JSON.parse(rec.Sns.Message);
            
            var params = {
                  FileSystemId: message.AlarmDescription, /* required */
                  ThroughputMode: 'bursting'
            };
            efs.updateFileSystem(params, function(err, data) {
                if (err) console.log(err, err.stack); // an error occurred
                else     console.log(data);           // successful response
            });
            // ここでslack等に通知するといいと思う
        }
    });
    
};

EFS-to-provisioned-iops

console.log('Loading function');
var https = require('https');
var http = require('http');
const aws = require('aws-sdk');
var efs = new aws.EFS();

exports.handler = (event, context) => {
    (event.Records || []).forEach(function (rec) {
        if (rec.Sns) {
            var message = JSON.parse(rec.Sns.Message);
            
            var params = {
                  FileSystemId: message.AlarmDescription, /* required */
                  ProvisionedThroughputInMibps: '10', // プロビジョニング10MB
                  ThroughputMode: 'provisioned'
            };
            efs.updateFileSystem(params, function(err, data) {
                if (err) console.log(err, err.stack); // an error occurred
                else     console.log(data);           // successful response
            });
            // ここでslack等に通知するといいと思う
        }
    });
};

FileSystemId: message.AlarmDescriptionの部分ですが 受け取ったCloudWatchアラームにはどのEFSがアラームを出したか、というものが取得しずらかったので CloudWatchアラームのDescriptionにEFSのIDを載せる形にしました

SNS(Simple Notification Service)のトピックを作成してlambdaに連携します

SNSは通知先にメールやhttpなどだけでなくlambdaに通知できます。 ですので、バーストモードにするlambdaに連携するトピックと、 プロビジョンドにするlambdaに連携するトピックの2つを作成します。

f:id:cs_sonar:20190826131112p:plain

cloudwatchにてアラームを作成

BurstCreditBalanceを監視し、上記作成のSNSに通知するアラームを作成します。 こちらも、
・バーストクレジットが一定数以上の時 -> プロビジョンモードにするSNSに発火
・バーストクレジットが一定数以下の時 -> バーストモードにするSNSに発火
という形で2つ作成します。

f:id:cs_sonar:20190826131630p:plain

lambdaの所でも書いた通り、DescriptionにはEFSのIDを入力します

f:id:cs_sonar:20190826131750p:plain

バーストとプロビジョニングの変更を自動化できました

このように複数EFSあると便利さがわかりますね!

f:id:cs_sonar:20190826132003p:plain

クレジットが減るとプロビジョニングになり、クレジットが充足するとバーストモードになる形です

注意点

EFSの制限で、スループットモードの変更は1日に1回までしかできません! そのため、1日でバーストクレジットを使い果たしてしまうような環境ではずっとプロビジョニングモードで動かす必要があります。

本日は以上で!