asoview! TECH BLOG

アソビュー株式会社のテックブログ

精算システムについて

アソビュー Advent Calendar 2019の19日目。
こんにちは。アソビューで精算システムやSREチームを担当している並木です。
マイクラ歴3年になるのに、いまだに豆腐建築から抜け出せません。
建築センスをください。

簡単な経歴

・SIerとして、銀行の勘定系システムを担当
・Webベンチャーの営業っぽい雑用係を担当
・アソビューにJOIN
元々銀行のシステムを担当していたこともあり
アソビューでは主に精算周りのシステムに関わらせていただいております。
今回は、精算システムにおける計算処理の注意点などを書きたいと思います。
※アソビューのシステムに関連する部分もありますが、基本的には一般論として書かせていただきますので、ご容赦ください。

法改正対応

記憶にも新しい

消費税の改正 8% → 10%

実際にシステムにどんな影響があるか?

税率計算の処理

  • 増税のたびに影響範囲の確認、修正規模が多くなりがちな悪い実装例
  • 影響範囲は変わらないが、修正規模が小さく、結果テストなどに掛かる工数も減らせる良い実装例
悪い実装例
  • ※言語はCOBOLにて実装した場合のイメージとなります。あくまでイメージです。

料率をDBでの管理ではなく、code にベタ打ち固定で記載している場合は
全ての固定箇所に修正が必要になり、修正範囲の確認から修正、テストと掛かる工数も大きくなりがちです。
このような例はミスも起こりやすく、「お金を取り扱うシステム」としてはよくない例となります。

  WORKING-STORAGE  SECTION.
     01  ONIGIRI     PIC 999 VALUE 150.
     01  SYOUHIZEI    PIC 9999.
     01  ZEIKOMI     PIC 9999.
     01  ZEIRITSU    PIC 9   VALUE 8.  -- 消費税率 計算時は /100 で使用
~~
         COMPUTE     SYOUHIZEI =  ONIGIRI * (ZEIRITSU / 100).
         COMPUTE     ZEIKOMI =  ONIGIRI + SYOUHIZEI
~~

実際に計算してみると
150円のおにぎりの消費税を固定で8%税率で計算し
それを加算することで税込金額として算出。

   150 + 12 = 162円 

が計算結果となります。

良い実装例

レコードの構成イメージ

f:id:yusuke-namiki:20191215221008p:plain

DBで料率を管理し、増税時に掛かる作業としては

最新レコードの挿入(inesrt)
直近レコードの終了日の更新(update)

のみであり、このDBを呼び出している箇所全てに適用されるため
影響範囲の特定も容易で、テストも機械的に実施できることになります。

  WORKING-STORAGE  SECTION.
     01  ONIGIRI     PIC 999 VALUE 150.
     01  SYOUHIZEI    PIC 9999.
     01  ZEIKOMI     PIC 9999.
     01  ZEIRITSU    PIC 99.  -- 消費税率 計算時は /100 で使用
~~
     select zeiritsu
       from db-syouhiziei
        where now() between start_day and end_day 
-- 今日がどの税率なのかを判定させる条件
     MOVE zeiritsu TO ZEIRITSU

         COMPUTE     SYOUHIZEI =  ONIGIRI * (ZEIRITSU / 100).
         COMPUTE     ZEIKOMI =  ONIGIRI + SYOUHIZEI
~~

これであれば今の日付を見て、最新の税率を取得して計算しているので
税率の最新レコードの insert と一つ前のレコードの update 処理さえ忘れていなければ
実際の計算処理には影響が出なくなります。

帳票類(レシート、振替用紙などなど)

消費税(税率8%) ¥108 

といった形で固定で出力している場合には、計算処理同様に
最新の税率を取得して出力するなどの工夫が必要になります。
キャッシュレスに先んじてペーパレスは事前に対応していることが多かったため
そこまで大掛かりな回収は必要ではなかった認識でいます。

今回は触れていませんが 軽減税率の場合のレシート出力については、政府からも対応例が出ていますね。 https://www.gov-online.go.jp/tokusyu/keigen_zeiritsu/jigyosya/kubunkisai.html

まとめ

いかがでしたでしょうか。
今回は消費税のことにのみ触れていますが、法改正に関わるシステム改修に関して
一部ではありますが、サンプルを記載してみました。

昨今では法改正に関わるシステム改修がテレビなどでも取り沙汰されるようになりましたので
「どんなことしてるんだろう?」の理解の一助になれば幸いです。

最後に

記載した内容について、開発と運用の責任者を募集中です!
※COBOLは必須スキルではありません。むしろいらな・・ry
成長著しいベンチャーで社内基幹システムの開発・運用責任者大募集! | アソビュー株式会社

今すぐやめようssh! AWS Session Managerを検証・導入してみた

この記事は アソビュー! Advent Calendar 2019 - Qiita 17日目の記事です。

アソビューにてバックエンドおよびSREを担当している寺岡(@toda_kk)と申します。よろしくお願いします!

最近はリングフィットアドベンチャーをやり始め、スクワットをする度に「いいぞ!」「すごいぞ!」と褒められることで自己肯定感を高めています。

はじめに

さて、SREの業務というと、どの企業でも多岐にわたるかと思います。インフラアーキテクチャの設計および構築、モニタリング環境の整備、開発フローや運用業務の効率化……などなど、ビジネス上あまり目立たないけれど実は大事な役割を担っていたりします*1

そんな中で、今回は開発や運用の効率化のためにAWSのSession Managerという機能を検証・導入した話を取り上げたいと思います。

Session Managerとは?

AWS Systems Manager Session Managerとは、マネジメントコンソールやAWS CLIを介してEC2インスタンスを管理するための機能です。IAMポリシーでアクセス制御を設定できるなど、セキュリティ面での向上が期待できます。

その他、サービスの特徴や導入のメリットについては公式ドキュメントをご参照いただきたいのですが、多くの場合はEC2インスタンスに対するssh接続の代替となる機能として認識されることが多いのではないかと思います。

課題感

弊社ではインフラリソースとしてAWSを利用しており、特に最近はECSやEKSの導入をはじめアプリケーションのコンテナ化を進めています。

しかしながら、サービス開始当初から動いている一部のアプリケーションについてはまだコンテナ化が追いついておらず、EC2インスタンス上で直接 jarファイルを実行してアプリケーションを起動しているケースがあります*2

こうしたケースでは、サービスの再実行やインフラリソースの整備などでインスタンスにsshして作業することが少なくありません。そんな中で、日々の業務のなかで下記の点で課題が発生していました。

  • EC2インスタンスにssh接続するための秘密鍵の管理が必要になる。
  • 誰がどういった作業を実施したのかロギングしづらい。

ssh認証の秘密鍵(pemファイル)の管理

EC2インスタンスへのssh認証方式として、多くの場合はパスワードを使用する方式と公開/秘密鍵を使用する方式を用いると思います。特にEC2をそのまま使う場合、 hoge.pem のようなファイル形式で秘密鍵を使った認証を選択する場合が多いのではないでしょうか。どちらの方式を選択するにせよ、パスワードや秘密鍵をどのように管理するかというところが運用上の課題になるでしょう。

秘密鍵をどういう単位で生成するのか、個々のインスタンスで別にするのか、VPC内で同一にするのかなど、さまざまな方針があると思います。

しかしいずれにせよ、どのインスタンスがどの秘密鍵とひもづくのか、またどの秘密鍵をどのメンバーに共有しているのかなどの管理が煩雑になりがちです。スタートアップ創業当初で限られたメンバーでサービス開発をしているフェーズあれば大きな問題にはならないかもしれませんが、サービスや組織が大きくなるに連れてセキュリティ面を強化することは必須の課題となってきます。そうしたときに、ひとつの課題としてインスタンスにおける認証方法の管理という点が浮上してくることでしょう。

作業のロギング(監査ログの取得)

またセキュリティの強化を考えた場合、機密性(Confidentiality)を確実にすると共に責任追跡性(Accountability)を担保することも重要となってきます。そこで、インスタンス上でどのような作業を行ったのか、さらにいえば誰がどのような操作を行ったのか、という監査ログを取得することが必要になってくると思います。

Linux上であれば auditd を用いるのが一般的かと思いますが*3、企業によってはEC2上でWindows OSを動かしている場合もあるでしょう。このような場合も含めて、OS上の監査ログ取得の選択肢としてはさまざまな方針が考えられるかと思いますが、多くの場合はログの連携や確認が容易にできないといったケースもあるのではないでしょうか。

検証と導入

こうした課題に対して、弊社では解決策の一つとしてSession Managerの検証および導入を行いました。なお、今回の検証にあたっては下記の記事を大いに参考に実施しました*4

dev.classmethod.jp

導入の手順

導入の手順は非常に簡単で、下記の3ステップを実施するだけで完了します。

  • Session Manager自体の設定を行う。
  • EC2インスタンスの amazon-ssm-agent を最新化する。
  • EC2インスタンスにIAMポリシーを設定する*5

Session Managerの設定

まず、対象となるAWSアカウントでSession Managerを使えるように設定を行います。今回はマネジメントコンソール上から設定する方法を紹介します。

といっても、実はSession Manager自体の設定について手順は多くはありません。とりあえず導入するだけなら、下記のように設定するだけでOKです。

Session Managerの設定
Session Managerの設定

なお、Session Managerでセッションを開始すると通常は ssm-user というユーザー名が表示されるのですが、 Enable Run As support for Linux instances の設定を有効にしておくと任意のOSユーザーでSession Managerの利用が可能となります。

EC2インスタンスの設定

次に、対象のEC2インスタンスでSession Managerからセッションを開始できるようにするための設定を行います。

まずは、EC2インスタンス上で amazon-ssm-agent をインストール or 最新化しておきます。Amazon Linux 2 を利用している場合はデフォルトでインストールされていますが、古いインスタンスだと 2 ではなく以前の Amazon Linux で運用していることもあるかと思います。そのような場合は、新たにインストールしておく必要があります。

そして、Session Managerを利用するために必要となるIAMポリシーを設定します。AWSではあらかじめ AmazonSSMManagedInstanceCore というポリシーが用意されており AmazonSSMRoleForInstancesQuickSetup というロールにアタッチされています。このロールをそのままEC2インスタンスに適用することもできますが、Systems Managerの機能を全て許可する設定になっているので本番適用するのは控えた方が良さそうです。

Session Managerを導入するための最低限のIAMポリシーとしては、いろいろと試してみたところ下記のようになるようです。この記述を元に、用途に合わせて設定を変更していただくのが良いかと思います。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:UpdateInstanceInformation",
                "ssmmessages:CreateControlChannel",
                "ssmmessages:CreateDataChannel",
                "ssmmessages:OpenControlChannel",
                "ssmmessages:OpenDataChannel"
            ],
            "Resource": "*"
        }
    ]
}

監査ログの取得

Session Managerを導入するための手順は、上記で完了となります。この程度であれば、マネジメントコンソールからでもAWS CLIからでもサクッと導入できちゃいますね!

加えて、Session Managerで実行した操作のログを取得する方法を紹介します。

といっても、上述したSession Managerの設定にある項目を有効化するだけ済みます。めっちゃ簡単やんけ。

Session Manager監査ログの有効化
Session Manager監査ログの有効化

CloudWatch Logsにログ出力するように設定すると、指定したロググループに対してセッション終了後にログストリームが作成されます。

セッション履歴からCloudWatch Logsを確認できる
セッション履歴からCloudWatch Logsを確認できる

下図のように、Session Managerから作成されたログストリームの名前にはプレフィックスとしてIAMユーザー名が付きます。そのため、どのメンバーがどのような操作を実行したのかが簡単に追えるようになります。

ログストリームにIAMユーザー名が付く
ログストリームにIAMユーザー名が付く

今回はCloudWatch Logsに連携するように設定しましたが、設定画面にあるようにS3にログを送信することもできます。用途に応じて使い分けられると良さそうです。

ちなみに、CloudWatch Logsに連携する場合はEC2インスタンスに設定するIAMポリシーに下記のような権限を加える必要があります。ただ、通常はインスタンス上のOSログやアプリケーションログなどをCloudWatch Logsに出力している場合が多いかと思いますので、特別にポリシーを変更する必要はないかもしれません。

        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogStream",
                "logs:PutLogEvents",
                "logs:DescribeLogGroups",
                "logs:DescribeLogStreams"
            ],
            "Resource": "*"
        }

さらなる検証

Session Managerを導入することで、はじめに挙げた2つの課題を解決することができました!

まず、開発メンバーに対してpemファイルを配布する機会を減らし管理しやすくすることができました。特に新たに、ジョインしたメンバーに対しては、pemファイルを配布せずにEC2インスタンスの管理をお願いする準備を整えることができました。

また、どのメンバーがEC2インスタンス上でどういった操作をしたのか記録できるようになりました。こうした形でセキュリティ面の強化を着実に進めることができたと思います。

導入手順としては以上となりますが、弊社ではさらなる検証を実施しました。

ひとつは、EC2インスタンスに設定するセキュリティグループについてです。上記の導入手順ではセキュリティグループの設定はありませんでした。Session Managerの利用においてセキュリティグループは関係ないのでしょうか?

続いて、AWS公式がサポートしている session-manager-plugin についてです。これはAWS CLIを通じてSession Managerを介しEC2インスタンスへのセッションを開始するためのツールですが、今回の目的にどの程度適しているのかを検証してみました。

必要なセキュリティグループについて

Session Managerはsshの代替となるものではありますが、実際にはsshを実行しているわけではありません。詳細は公式ドキュメントを参照していただきたいのですが、sshしているわけではないのでセキュリティグループのインバウンドで 22ポート を開放する必要はありません

ところで、インバウンドで 22 以外のポートを閉じてしまうとSession Managerでも接続できなくなるのではないでしょうか? そこで、どの程度ポートを開けておく必要があるのか検証してみました。

結論としては、インバウンドのポートは全て閉じた状態でもSession Manager経由で接続できます

なお、当たり前ですがアウトバウンドのポートを全て閉じてしまうとSession Managerでも接続できませんでした。正確には、セッションは開始できるものの、出力を受け付けないためインスタンス上でなにが起きているのか把握できない状態となってしまっていました。アウトバウンドは基本的に全て許可にするケースが多いかと思いますが、Session Manager導入にあたって特別に設定変更する必要はないかと思います。

session-manager-pluginについて

基本的にSession Managerを利用する際にはマネジメントコンソールからセッションを開始します。ただし、ブラウザ上からコマンドを叩くのはぶっちゃけちょっと面倒ですし、人によってはつらい前世の記憶が呼び起こされることになるでしょう。

そこで、AWS CLIからセッションを開始する方法として session-manager-plugin というツールが公式でサポートされています。ローカルのマシンにインストールすることでCLIからSession Managerを利用できます。導入の手順などは公式ドキュメントをご参照ください。

ツールのインストール後は下記コマンドを実行するだけでセッションを開始できます。便利ですね!

# インスタンスのパブリックIPではなくインスタンスIDを指定するので注意!
aws ssm start-session --target [instance-id]

これで全部ええやん!……と思いきや、ひとつ問題が発生しました。

弊社ではAWSアカウントを複数所持しており、例えば本番環境と開発環境を分離するなどの目的でマルチアカウントで運用しています。そのため、マネジメントコンソールを利用する際には、まず大元となるAWSアカウントにログインして、そのあと別アカウントのロールとしてスイッチロールする形で運用しています。また、スイッチ先のロールはメンバーの権限に合わせて admin / developer / operator などを設定しており、IAMユーザーもそのように作成しています。

そこで問題になるのがスイッチロールの際にはスイッチ元のIAMユーザー名は引き継がれないという点です。

上述の通り、Session Managerを介した操作のログはCloudWatch Logsに出力しており、ログストリームのプレフィックスにIAMユーザー名が付くことで監査ログとして機能できると考えていました。しかし session-manager-plugin からスイッチロールした先のAWSアカウント上のインスタンスに対してセッションを開始すると、ログストリームのプレフィックスに一律で botocore-session という文字列が付与されてしまい、どのメンバーが操作したのか追いづらくなってしまいます。

スイッチロールした際のユーザー名
スイッチロールした際のユーザー名

ログストリームのプレフィックスにも付いてしまう
ログストリームのプレフィックスにも付いてしまう

plugin内部の処理を見てみると、どうやらセッション開始の度に botocore-session-XXXXXXXXXX という形式でIAMユーザーを作成している? ように見えます*6。そのため、ログストリームにそのユーザー名が入るようになってしまっているようです*7

Session Managerの導入後にこのような問題が発覚してしまいました。とはいえ、単にEC2インスタンス上の作業を記録する目的としては、上記の通りCloudWatch Logsへのログ出力で十分と考えることもできます。弊社ではひとまずこの状態まで持っていくことをゴールとして、Session Managerの導入と開発メンバーへの共有を進めました。

おわりに

以上、Session Managerを導入することで、開発メンバーに対してpemファイルを配布する機会を減らし管理しやすくすることができました。特に新たに、ジョインしたメンバーに対してはpemファイルを配布せずにEC2インスタンスの管理をお願いする準備を整えることができました。

また、どのメンバーがEC2インスタンス上でどういった操作をしたのか記録できるようになりました。こうした形でセキュリティ面の強化を着実に進めることができたと思います。

このようにアソビューSREチームでは、開発チームとの連携を強化したり運用を改善するための施策をたくさん進めております! とはいえ、手が足らずに思うように実行できていないのも事実です。

そこで、下記の通り新たなメンバーを積極的に募集中ですので、ぜひ応募いただけると嬉しいです!

hrmos.co

みんなどんどん応募してくれよな!!

*1:このアドベントカレンダーでもCloudFormationの話EKSの話など、SREチームのメンバーが書いた記事がいくつかあります。

*2:アソビューでは開発言語としてJavaを利用しているアプリケーションが多いです。Javaを選択する理由についてはこちらの記事をご参照ください。

*3:弊社では実施しませんでしたが、Amazon Linux上でauditdを使って監査ログを取得する際は、こちらの記事が参考になるかと思います。EC2でファイル監査を設定する(auditd設定編) | ナレコムAWSレシピ

*4:クラスメソッド様、いつもありがとうございます!

*5:正確には「IAMポリシーをアタッチしたIAMロールをインスタンスプロファイルとして設定する」ということになるでしょうが、記述の簡略化のため上記のように表現しています。

*6:この辺りはまだしっかりと検証できていない部分です。また私が調べた限りでは、この辺りの詳細について解説したページは見当たりませんでした。どなたかご存知の方がいらっしゃれば、ご教示いただけると助かります!

*7:公式ドキュメントのこの辺りを参照すれば解決しそうな気がしますが、まだ十分に検証できていません。

履歴管理ができるテーブル構造を考えてみた

アソビュー! Advent Calendar 2019 の16日目の記事です。

アソビュー株式会社でバックエンドエンジニアをしている土屋です。
最近はスマブラにハマっており、大会に向けて日々練習中です。

本記事の概要

DBでデータを効率的に管理するためのテーブル構造と、そのメリット・デメリットを考えてみたお話です。

背景

アソビュー!では様々なデータをRDBで取り扱っていますが、テーブル構造はメンテナンスが難しいです。
モノリシックなマスターテーブルが存在していて、更新日カラムが付属しているけどどこを更新したのかわからない。
また、過去のデータがほしいけどマスターテーブルなので元々がどんなデータだったかわからない。
ということがたまに発生したりします。

上記から履歴管理ができるテーブル構造を作る必要があったため、検討をしました。

検証

ショップと商品というデータの格納方法について、以下の2つの方法でメリット・デメリットを検討してみます。

  • マスターテーブルにまとめた場合
  • 履歴管理を視野に入れて設計した場合

マスターテーブルにまとめた場合

DDL

create table sandbox.SHOP_MASTER
(
    ID          serial primary key comment 'ショップID',
    NAME        varchar(128) not null comment 'ショップ名',
    INSERT_DATE datetime     not null default CURRENT_TIMESTAMP() comment 'システム用 登録日時',
    UPDATE_DATE datetime     not null default CURRENT_TIMESTAMP() comment 'システム用 更新日時'
) comment 'ショップ';

create table sandbox.SHOP_ITEM_MASTER
(
    ID          serial primary key comment '商品ID',
    SHOP_ID     bigint unsigned not null comment 'ショップID',
    NAME        varchar(128)    not null comment '商品名',
    PRICE       int unsigned    not null comment '店頭価格',
    NET_PRICE   int unsigned comment 'ネット価格',
    INSERT_DATE datetime        not null default CURRENT_TIMESTAMP() comment 'システム用 登録日時',
    UPDATE_DATE datetime        not null default CURRENT_TIMESTAMP() comment 'システム用 更新日時',
    constraint fk_shop_item_master_SHOP_ID foreign keY (SHOP_ID)
        references sandbox.SHOP_MASTER (ID)
) comment '商品';

ER

f:id:mtsu724:20191212155917p:plain

メリット

  • テーブル数が少ないので、ぱっと見でどういうデータ構造かがわかりやすい
  • テーブル数が少ないということは、検索のSQLが組みやすい

デメリット

  • 更新日をもっていても、何を更新したかがわからない
  • 情報を増減させるために、ALTER TABLE文を使う可能性がある
    • 例1:店頭料金、ネット料金以外に宅配料金を追加したい場合
    • 例2:店舗に住所を保持させたい
  • ALTERを続けることにより、データにnullが入る可能性が高くなります

履歴管理を視野に入れて設計した場合

DDL

create table sandbox.SHOPS
(
    ID          serial primary key comment 'ID',
    INSERT_DATE datetime not null default CURRENT_TIMESTAMP() comment 'システム用 登録日時'
) comment 'ショップID';

create table sandbox.SHOP_REVISIONS
(
    ID          serial primary key comment 'ショップリビジョンID',
    SHOP_ID     bigint unsigned not null comment 'ショップID',
    INSERT_DATE datetime        not null default CURRENT_TIMESTAMP() comment 'システム用 登録日時',
    constraint fk_shop_revisions_SHOP_ID foreign key (SHOP_ID)
        references sandbox.SHOPS (ID)
) comment 'ショップリビジョン';

create table sandbox.LATEST_SHOP_REVISIONS
(
    SHOP_ID     bigint unsigned not null comment 'ショップID',
    REVISION_ID bigint unsigned not null comment 'ショップリビジョンID',
    INSERT_DATE datetime        not null default CURRENT_TIMESTAMP() comment 'システム用 登録日時',
    constraint fk_latest_shop_revisions_SHOP_ID foreign key (SHOP_ID)
        references sandbox.SHOPS (ID),
    constraint fk_latest_shop_revisions_REVISION_ID foreign key (REVISION_ID)
        references sandbox.SHOP_REVISIONS (ID)
) comment '最新のショップリビジョン';

create table sandbox.SHOP_NAMES
(
    REVISION_ID bigint unsigned not null comment 'ショップリビジョンID',
    NAME        varchar(128)    not null comment 'ショップ名',
    INSERT_DATE datetime        not null default CURRENT_TIMESTAMP() comment 'システム用 登録日時',
    constraint fk_shop_names_REVISION_ID foreign key (REVISION_ID)
        references sandbox.SHOP_REVISIONS (ID)
);

create table sandbox.SHOP_ITEMS
(
    ID          serial primary key comment '商品ID',
    REVISION_ID bigint unsigned not null comment 'ショップリビジョンID',
    INSERT_DATE datetime        not null default CURRENT_TIMESTAMP() comment 'システム用 登録日時',
    constraint fk_shop_items_REVISION_ID foreign key (REVISION_ID)
        references sandbox.SHOP_REVISIONS (ID)
);

create table sandbox.SHOP_ITEM_NAMES
(
    ITEM_ID     bigint unsigned not null comment '商品ID',
    NAME        varchar(128)    not null comment '商品名',
    INSERT_DATE datetime        not null default CURRENT_TIMESTAMP() comment 'システム用 登録日時',
    constraint fk_shop_item_names_ITEM_ID foreign key (ITEM_ID)
        references sandbox.SHOP_ITEMS (ID)
) comment '商品名';

create table sandbox.SHOP_ITEM_PRICES
(
    ITEM_ID     bigint unsigned not null comment '商品ID',
    AMOUNT      int unsigned    not null comment '価格',
    INSERT_DATE datetime        not null default CURRENT_TIMESTAMP() comment 'システム用 登録日時',
    constraint fk_shop_item_prices_ITEM_ID foreign key (ITEM_ID)
        references sandbox.SHOP_ITEMS (ID)
) comment '商品店頭価格';

create table sandbox.SHOP_ITEM_NET_PRICES
(
    ITEM_ID     bigint unsigned not null comment '商品ID',
    AMOUNT      int unsigned    not null comment '価格',
    INSERT_DATE datetime        not null default CURRENT_TIMESTAMP() comment 'システム用 登録日時',
    constraint fk_shop_item_net_prices_ITEM_ID foreign key (ITEM_ID)
        references sandbox.SHOP_ITEMS (ID)
) comment '商品ネット価格';

ER

f:id:mtsu724:20191212155919p:plain

メリット

  • 履歴管理をすることで、いつどんな変更があったかを確認することができます
    • この情報を機械学習で利用することで、何らかの相関関係を見つけて業務に利用できる可能性もあります
  • 宅配価格や住所を追加したい場合、CREATE TABLEだけでOKになります
  • 1つのテーブル情報が肥大化しないので、変更容易性に富みます
  • データが独立しているので、nullのデータが入りにくいです

デメリット

  • テーブル数が増えるため、命名にルールがないと管理が大変になります
  • 履歴を管理するという要件の特性上、レコード数が膨大になりやすいです
    • リビジョンを上げるタイミングなどは要件として考慮する必要があります

おわりに

履歴管理を行うことで、どんな操作が行われたかわかるようになるので、利用者も開発者にも恩恵があると思っています。
とはいえ不要なデータを管理する必要はないので、履歴管理の対象はどれなのかを都度検討できるとよいかなと思います。

以上です。お疲れ様でしたー。

参考

gatlingで負荷試験を実施する方法

これはアソビュー Advent Calendar 2019の15日目です。

こんにちは、アソビューでBEエンジニアのThang(タン)です。
Gatlingで負荷試験を試したので備忘録です。

今回試したのは Reserve with Google APIの負荷試験です。
APIインタフェースはこちらでご参照して頂けます。

Gatlingとは

  • scalaベースの負荷試験ツールの一種です
  • 特徴:
    • テストシナリオが Scalaで書ける
    • テスト結果はhtmlで出力され、出力結果がキレイ
    • Scala知識が低くても意外と簡単に書ける

Gatling インストール

JDKが必要なので、インストールします。

$ brew install java

公式HPからzipファイルをダウンロードします。
私はバージョン 2.3.1で試しました。

Gatling 使い方

とりあえず bin にgatling.shがあるので実行して、サンプルの負荷試験を実施してみます。

$ bin/gatling.sh 

シナリオを選択します。1のAdvancedSimulationStep01を選択してみましょう。 f:id:dqth:20191215131913p:plain

実行が始まります。 f:id:dqth:20191215132015p:plain

実行完了され、結果が出力されます。
結果はこんな感じです。 f:id:dqth:20191215132106p:plain f:id:dqth:20191215132129p:plain

シナリオを書いてみる

gatlingはシナリオ自動生成機能を使っても良いのですが、今回は自分で1からシナリオを作成してみます。

シナリオの置き場所はデフォルトで user-files/simulationsになります。
ちなみに、置き場所は conf/gatling.confdirectory.simulations項目で変えられます。

書いたシナリオはこんな感じです。

class RecordedSimulation extends Simulation {

    val httpProtocol = http
        .baseURL("http://localhost:8080")
        .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
        .acceptEncodingHeader("gzip, deflate")
        .acceptLanguageHeader("ja,en-US;q=0.7,en;q=0.3")
        .doNotTrackHeader("1")
        .userAgentHeader("Mozilla/5.0 (iPhone; CPU iPhone OS 10_2_1 like Mac OS X) AppleWebKit/602.4.6 (KHTML, like Gecko) Version/10.0 Mobile/14D27 Safari/602.1")

    val order_feeder = csv("order.csv").queue

    val scn = scenario("RecordedSimulation")
        .feed(order_feeder)
        .exec(http("order_fulfillability")
            .post("/v3/CheckOrderFulfillability")
            .body(StringBody("""{"merchant_id":"${merchantId}","item":[{"service_id":"${serviceId}","start_sec":${startSec},"duration_sec":${durationSec},"tickets":[{"ticket_id":"${ticketId}","count":${count}}],"price":{"price_micros":${price},"currency_code":"JPY"}}]}"""))
        )

    setUp(scn.inject(constantUsersPerSec(1) during(5 seconds))).protocols(httpProtocol)

このシナリオは /v3/CheckOrderFulfillability APIに1秒に一回 postリクエストを送ります
リクエストbodyはStringBodyを使って設定します。
bodyのパラメータは CSVファイルのデータを使って設定します。
API IFはこんな感じです。

{
    "merchant_id": String,
    "item": [
        {
            "service_id": String,
            "start_sec": long,
            "duration_sec": long,
            "tickets": [
                {
                    "ticket_id": String,
                    "count": int
                }
            ],
            "price": {
                "price_micros": long,
                "currency_code": String
            }
        }
    ]
}

CSVファイルはこんな感じです。 ファイルの置き場所はuser-files/dataになります。

merchantId,serviceId,startSec,durationSec,ticketId,count,price
149990,service0000001,1569805200,36000,0001,2,3200000000
149990,service0000002,1569805200,36000,0002,2,3200000000
149990,service0000003,1569805200,36000,0003,2,3200000000
149990,service0000004,1569805200,36000,0004,2,3200000000
149990,service0000005,1569805200,36000,0005,2,3200000000

gatling.shを実行すると、シナリオ選択のところで今回書いたシナリオ(test)が追加されます。
それを選択すると、追加シナリオを実行できます。 f:id:dqth:20191215132458p:plain

アソビュー! CTOが考えるエンジニアの働き方

アソビュー !Advent Calendar 2019 の14日目の記事です。

こんにちは。アソビュー!で執行役員CTOをしている山崎賢(やまけん)です。 今日は偉そうにも、勝手に自分がずっと想像しているエンジニアの働き方を勝手にツラツラと発信しようと思います!

私が思う。エンジニアの定義

f:id:yk422:20191212164209p:plain

全ての職種においてそうですが、「エンジニア」「デザイナ」「プロダクトオーナー」などなどは、一般的に「職種」と言われていますが 産業構造の進化と労働環境の進化による分業効率性の観点で生まれたロールだと思っています。

それでは、職種ロールの違いってなんでしょうか・・・? 以下は私の個人的な解釈です。

■職種
それ単体で職業として成り立つ種別

■ロール
ある成果/結果を出すために必要となる機能的な役割

エンジニアという職種は、本来は機能的な役割(=ロール)であるはずが、ニーズが大きくそれ単体だけを実行する役割が市場的に必要とされているので 職種としてなりたっているのだと思います。

一方で人はそんな単純なものではなくて、たくさんの個性の集合体です。 得意/不得意、好き/嫌い、それらを含めた全てが個性であり、人だと思っています。

f:id:yk422:20191212150217p:plain
エンジニア
f:id:yk422:20191212150656p:plain
多様性


私が思う。職種の違和感

職種は社会ニーズに基づいて定義されています。「エンジニア」という職種が成り立っているのは、それ単体の塊で社会的なニーズがあるから。

ただ、いつも思います。誰かが決めた枠組みである「エンジニア」「デザイナ」「プロダクトオーナー」などなど。 その枠組みは心地よいものですが、仕事の本質は「課題の解決」です。

本質的な仕事の価値は、その心地よい枠組みを超えた先にあるのではないか。と思っています。


たとえばドラクエだったら。

f:id:yk422:20191212164018p:plain

ドラクエに例えると超シンプルです。

「世界が平和ではない」という課題の原因に「魔王の存在」があり、「魔王の存在を打ち消す」ために、「勇者と仲間を募集する」という手段があるわけす。 f:id:yk422:20191212151625p:plain

そして、ドラクエの世界だと以下のような職種が生まれるわけです。

職種 スキル
勇者 戦闘力が高い
攻撃魔法が使える
回復魔法が使える
武闘家 戦闘力が高い
戦士 戦闘力が高い
僧侶 回復魔法が使える
魔法使い 攻撃魔法が使える

あれ、、、ゲームだから魔王を倒すことが決まっていますが、もしかしたら魔王を改心させることが出来るコミュニケーターがいれば世界は平和になるかもしれません。

仮に魔王を倒すとしても、勇者が100人居たほうが課題解決かんたんですよね。


エンジニアよ。勇者であれ。

つまり、仕事の上では「エンジニアである」ということよりも「課題を解決するために、なんのスキルをどれだけ持っているか」の方が重要です。 そのためのロールの1つがエンジニアリングなのであり、エンジニアリングでなくてももっと簡単に課題解決出来るならそれでいいわけです。

「ソフトウェアを作る人」というエンジニア職種要件の見えない壁をぶち壊して、「課題解決人」としてエンジニアリング+αのαの部分をどれだけ積み重ねていけるか。

戦えて魔法も使える勇者のように、課題解決に最適な職種を自分で定義して自分がそれを目指していく。

それこそが本質的な成長であり、課題解決へのアプローチであり、エンジニアとしての成長であり、自分自身の市場価値の引き上げなのだと思っています!


最後に。

そんなアソビュー!では楽しく課題解決して成長しまくる仲間を募集しています。

www.wantedly.com

勇者になりたい方、ぜひ募集を!

EKSでの機密情報の隠蔽にkubernetes-external-secretsを導入してみた

アソビュー!SREチームの霧生です。
この記事はアソビュー! Advent Calendar 2019 13日目の記事となります。

アソビューでは最近一部のアプリケーションのインフラにEKSを導入しています。
その運用をしていく中でDBのuser/passやAPIキーといった機密情報を隠蔽したい要件が出てきました。 ECSでは下記のClassmethodさんの記事にある通り、タスク定義とパラメータストアによる隠蔽ができますが、EKSではまだ公式で対応していないためどうしようかと悩んでいたところ、kubernetes-external-secretsを見つけました。

dev.classmethod.jp

kubernetes-external-secretsとは

kubernetes-external-secretsはドメインレジストラサービスを提供しているGoDaddy社がOSSとして公開しているもので、AWS Secrets ManagerAWS Systems Manager パラメータストアを利用して、機密情報の引き渡しが行えます。

github.com

より詳細に知りたい場合はGitHubにもありますが、下記の公式ブログが参考になると思います。 jp.godaddy.com

導入の決め手でもありますが、kubernetes-external-secretsは引き渡したい機密情報をyaml管理できる点が優れていると感じています。

EKS環境にkubernetes-external-secretsをインストールする

install方法は公式のGitHubでも公開されていますが、今回は単にkubectlコマンドを利用したインストールを試していきたいと思います。
(バージョン2.1までは生のyamlもあったのですが、現在はHelmを利用したインストールが推奨されているようです。)

まずはkubectlが利用できる環境でHelmをインストールします。 helm.sh

インストールが完了したら、kubernetes-external-secretsをローカルに落としてきます。

$ git clone https://github.com/godaddy/kubernetes-external-secrets

kubernetes-external-secretsのルートディレクトリに移動し、下記のコマンドを実行すると./output_dir以下にyamlファイルが生成されます。

$ helm template -f charts/kubernetes-external-secrets/values.yaml --output-dir ./output_dir ./charts/kubernetes-external-secrets/

あとは生成されたyamlファイルをそれぞれapplyしていけばインストールは完了です。

インストール時の注意点

環境変数等の設定について

基本的にはデフォルトのままで問題ないと思いますが、AWS_REGIONの値が利用する環境と違っていると値の取得ができないため注意が必要です。 設定の一覧はここから確認できます。

権限について

AWS Secrets Managerを使う場合とパラメータストアを使う場合でそれぞれ必要な権限が変わります。 こちらもサンプルがGitHubのページにありますが下記にそれぞれ載せておきます。

  • AWS Secrets Manager
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetResourcePolicy",
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret",
        "secretsmanager:ListSecretVersionIds"
      ],
      "Resource": [
        "arn:aws:secretsmanager:ap-northeast-1:111122223333:secret:aes128-1a2b3c",
        "arn:aws:secretsmanager:ap-northeast-1:111122223333:secret:aes192-4D5e6F",
        "arn:aws:secretsmanager:ap-northeast-1:111122223333:secret:aes256-7g8H9i"
      ]
    }
  ]
}
  • パラメータストア
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ssm:GetParameter",
      "Resource": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/prod-*"
    }
  ]
}

実際に使ってみる

今回はパラメータストアを利用した方法を紹介します。

パラメータストアへの機密情報の登録は上記でも紹介したClassmethodさんの記事が参考になると思います。 dev.classmethod.jp

パラメータストアで機密情報を登録した後、次はkubernetes-external-secretsを利用してSecretsを作成します。
/staging/test-app/TEST_VALUEという変数をパラメータストアで登録した場合、yamlは下記のようになります。

apiVersion: 'kubernetes-client.io/v1'
kind: ExternalSecret
metadata:
  name: test-app
secretDescriptor:
  backendType: systemManager
  data:
    - key: /staging/test-app/TEST_VALUE  # パラメータストアで設定した変数の名前
      name: test-value                   # この変数の一意の名前を設定する

作成されたExternalSecretを例えばDeploymentで利用する場合は下記のように設定します。

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: test-app
spec:
~中略~
          env:
            - name: TEST_VALUE
              valueFrom:
                secretKeyRef:
                  name: test-app      #ExternalSecretのmetadata.nameで指定した値
                  key: test-value     #ExternalSecretのsecretDescriptor.data.nameで指定した値

注意点

これは導入した後に見えた問題なのですが、DBのuser/passといったアプリケーションの起動時に参照される情報をパラメータストアに登録し、同じyamlファイルでExternalSecretとDeploymentを定義すると、ExternalSecretがAWSからパラメータを取ってくる際のほんの少しのラグによってDeploymentの作成時にSecretsの取得に失敗し、初回のアプリケーションの起動に失敗します。
yamlを分けてデプロイの順番を整理することで対応しましたが、利用する状況によっては注意が必要です。

導入してみて

正直ここまで公式のREADMEの内容をなぞってるだけでは?という感じなのですが、それほど導入も利用も簡単です。
kubernetes-external-secretsを導入することで、ソースと機密情報を完璧に分離しつつ、利用するyamlは管理できる理想の状態が作れました。 また、利用するパラメータストアの変数はTerraformで作成することで人的ミスを減らしています。

いくつか注意点はあるものの、EKS環境では現状最適解ではないかと思っています。

最後に

ベンチャーやスタートアップでは優先したいことも多く軽視されがちな部分ではありますが、導入も利用も簡単なので十分に検討する価値があるかと思います。 バージョンアップも盛んに行われているようなので、今後にも期待です。

アソビュー!ではSREの募集も行っています。 少しでも興味をお持ちになりましたら下記のページをご覧いただければと思います! hrmos.co

それでは良いKubernetesライフを!

アソビューがサービスを成長させためJavaを選択する理由

これはアソビュー Advent Calendar 2019の12日目です。

アソビューでエンジニアをしている兼平🐼(@disc99)です。

現在Javaは世界中で広く使われる言語である一方で、古い技術と捉えられることも少なくありません。 特に社外の方からは、なぜJavaなのかと聞かれる機会があります。

そこで今回はアソビューでJavaを使う理由とその活用内容を紹介したいと思います!

なぜJavaなのか

f:id:disc99:20191211100241p:plain:w300

理由1. 多くの企業で支援される言語

Javaというと硬いイメージを持つ方も多いと思いますが、世界的に見るとGoogle、Amazon、Netflix、Airbnbなどは主要言語、またはその一つとしてJavaを使用しています。

国内でもLINE、楽天、ヤフーなどを始め多くの企業が選択している言語でもあります。

非常に多くの利用者がいるため、各サービスやフレームワークが優先的にサポートやSDK提供されることが多く、メジャーなものだけでなく、先進的なサービスやツールを使う場合でも、他言語よりも先行して開発に取り入れることが可能になります。

結果的に、多くの経験者やナレッジが揃い、サービス開発に還元しやすくなります。

理由2. 優れたランタイム

Javaの特徴の一つがJVMです。

Web、金融、基幹システム、ビッグデータ、スマートフォンから組み込み機器まで、幅広い環境で利用され、非常に高いパフォーマンスや安定性、信頼性などの厳しい条件をクリアしています。

またJavaだけでなく、多くの言語(Scala、Kotlin、Clojure、Groovy)の実行環境として利用されています。

アソビューが提供しているサービスも to C向けのアソビュー!アソビュー!Gift、 to B向けのアクティビティ、チケット管理システム、OTAへのデータ連携や、社内向けシステムの他にも、それらをつなぐAPIやバッチ、運用のためのツールなど様々です。

これらの幅広い、条件をクリアする上でJavaは非常に有効な選択肢となっています。

弊社同様に多くの企業で採用される理由としてJVMの存在は大きいと思います。

理由3. 進化を続けてる言語

言語そのものとしては決して新しい部類ではないものの、現在でも半年に1度のバージョンアップを行っています。 また、進化を続けながらも、Javaの特徴である高い後方互換性を維持しています。

これに関しては、サービスをローンチしてから5年以上経過している弊社のサービスを安心して運用を続けられる理由でもあります。

最近では、言語仕様だけでなく、現代的なクラウド環境で重視される、高速起動や低リソース対応など、従来のJavaでは難しかった課題もGraalVMの登場により、新たな可能性が見えてきています。

また、最近の言語では、優れた構文であることより、たとえ冗長でも誰でも読み書きしやすかったり、ランタイムやエコシステムなど総合的に扱いやすいことが重視されることもあり、Javaもその特性に近いと感じています。

細かな部分は他にもいろいろありますが、これらは大きな理由となっています。

どのようにJavaを使っているか

Web

Spring

現在アソビューでは大小合わせて、数十のアプリケーションが連携しています。 その中で特に多いのが、Webサービスや、Web API、gRPCサーバなどのアプリケーションです。

この分野は 理由1. 多くの企業で支援される言語 でも紹介した通り、非常に多く使われており、パフォーマンスや信頼性、ナレッジも含め非常に充実している分野です。

また、社内ではSpring Bootが多く使われているため、開発も素早く開始できます。 実際に今年1年でも10以上のSpring Bootアプリケーションが新規でローンチされています。

また、結果的にマイクロサービス化されたアプリケーションが増えたため、外部からの呼び出しにはSpring製のAPI Gatewayである、Spring Cloud Gatewayを活用しています。

Springのエコシステムはもとより、プログラマブルなルーティング、APIアグリゲーションやHTTP over JSON to gRPCの変換レイヤーなど、API Gatewayとして求められる機能を活用してます。

spring.io

サーバーレス

f:id:disc99:20191211005535p:plain

現在弊社で稼働しているアプリケーションの多くはコンテナベースの環境で稼働しています。

また、以前まではECSを採用していましたが、EKSが発表されてからEKSへの移行を進めています。 CaaSは柔軟性で優れてはいますが、要件次第ではもっと素早く、スケーラブルで運用コストの低い環境が求められることも増えています。

そのようなケースにはAWS Lambdaを代表とするサーバレス環境を利用しています。

特に社内ではSpringを利用した開発が多いため、Spring Cloud Functionを利用したサーバーレスアプリケーションの開発も行っています。

spring.io

実際にサーバレスを利用したSpringアプリケーションは開発から本番運用開始するまで1日以内で可能なため、マッチするケースでは強力な選択肢となります。

speakerdeck.com 以前サーバーレスを活用したアプリケーション開発について発表した資料

一方でサーバーレス環境はコールドスタート、従量課金など、限られた環境故の課題もあるので、Spring Boot 2.3のFeatureであるGraalVM対応には非常に期待しています。

開発ツール

弊社でメインで利用しているクラウドであるAWSやアプリケーション実行の基盤となるKubernetes他、開発を行う上では様々なツールを駆使することになります。

特に、基盤を構築しているSREと、それらを利用する開発メンバーの間に知見の差異があることも少なくありません。

これらのギャップを減らし、素早くセットアップや各環境を操作できることを目的としたCLIツールを、SREチームから配布しています。

f:id:disc99:20191211082113p:plain

このツールもJavaで開発しており、GraalVMに対応したpicocliでネイティブなバイナリの生成を行っています。

picocli.info

Java 11対応

Java 11リリース以降、開発を開始したアプリケーションでは最新のLTSであるJava 11を使用して開発しています。 また、AWS Lambdaも最近Java 11をサポートしましたが、即日アップデートするなど、可能な限り最適なランタイムを選択できるようにしています。

その他よくある質問

Javaだと開発が遅くならないか

言語としての向き不向きが無いことは無いですが、多くのサービスがそうであるように、実運用レベルになると言語特性だけでなく、それを取り巻く環境や、ビジネスモデル、人材などの複合的要素により開発速度や品質が決まってくると思います。

実際にアソビューが提供してるサービスでも様々なニーズがあり、それらを満たす上で言語そのものだけで解決できる部分は決して多くないです。

また多くのWeb企業で採用されてることからも分かるように、言語そのものだけで開発速度が決まるわけではないので、用途に合わせ適切に選択できることを重視しています。

他の言語利用状況

今の開発メンバーがJavaの知見が多いので、Javaを選択することが多いですが、用途に合わせ適材適所で使い分けています。 例えば機械学習にはPythonを活用していたりや、新規サービスでGoを試してみたりなど並行してチャレンジを行っています!

まとめ

簡単ではありますが、アソビューのJavaを使っている背景とその状況を紹介させてもらいました!