LayerX エンジニアブログ

LayerX の エンジニアブログです。

【9/28-30開催】LayerXから2名のエンジニアがAWS Dev Day Online Japan 2021に登壇します! #AWSDevDay

f:id:shun_tak:20210921170151j:plain

こんにちは!いよいよ来週AWS Dev Dayが開催ですが、この度LayerXから2名のエンジニアが登壇することになりました。

どんな講演になるのか、本記事で簡単に紹介します!

E-2 : LayerXインボイスのAI-OCRを支える非同期処理アーキテクチャ (9/29 15:10〜15:50)

スピーカー:高際 隼 (株式会社LayerX DX事業部 AI-OCRチーム Lead)

LayerX インボイスは、請求書ファイルを読み取りデータ化するAI-OCR機能を備えた請求書処理サービスで、お客様は任意のタイミングで請求書ファイルをいくらでもアップロードすることができます。

AI-OCRは処理が重たいですが、AWS LambdaやAmazon SQS等を活用し、スケーラビリティと可用性を確保しつつコスト削減も実現した手法について解説いたします。

このセッションでお伝えしたいことスライド アジェンダスライド

I-1 : やはりタグ タグは全てを解決する - Tagging Everything (9/30 14:15〜14:55)

スピーカー:鈴木 研吾 (株式会社LayerX シニアセキュリティアーキテクト)

すべての道は資産管理に通ず。17世紀のフランスの詩人はそう謳いました。 時は流れ、21世紀。

資産管理はいまだにITガバナンスにおける基礎となっています。 一方ツールは進化しており巨大なエクセルというテーブルから、資産そのものにタグという属性をつけ、そしてInfrastructure as Codeが実現したマスデプロイによって柔軟な運用を可能としました。

本講演では、LayerXにおけるガードレールを構築するうえで、どのように我々がタグ管理をして、今後発展させていくかをお話しようと思います。

本日のお題スライド

申込方法

ぜひ今すぐ参加登録しましょう!

AWS Dev Day Online Japan 参加登録ページ

ISMSもとったし、エンジニアだけどITガバナンス主導してきた話をする

CTO室 @ken5scal です。座右の銘は「当社はブロックチェーンの会社ではもうありません」です。 主にインフラ構築・運用をしたり、社内の基盤を整えたり、不具合を特定して git blame したら自分のcommitで泣いたりしています。

当社は「すべての経済活動を、デジタル化する」というミッションを掲げており、生産性向上の達成という価値を新しいサービスによって提供しています。

しかしながら、新しい価値にはリスクが伴います。信頼できない価値を提供するサービスを採用する合理的な判断はありませんので、お客様に価値を価値のまま提供しなければlose-loseな関係になってしまいます。

今回は2021/08/27に発表されたISMS認証の基準準拠を含む、「LayerXは信頼できる」ブランドを確立していくための当社のITガバナンス活動について触れようと思います。

prtimes.jp

ガバナンス体制、その遷移

当社は代表の福島が「LayerXはブロックチェーンの会社じゃありません、という話」で述べたとおり、以前はブロックチェーンのコンサル事業を主としていました。

LayerXはブロックチェーンの会社じゃありません、という話|福島良典 | LayerX|note

私が入社した2020年頃は、全員が機動的に配置を変え①Ethereumのシャーディング技術の研究開発 ②金融業界を中心としたブロックチェーンの設計・標準の調査およびPoCの開発に携わるような体制だったと記憶しています。

しかし、こういったコンサル活動を通して学んだことを基に、次のようなプロダクトの模索が始まります。大体、2020/04頃です。

  • Fintech事業:不動産などのアセマネを裏付けとした証券発行 (三井物産デジタル・アセットマネジメント)
  • SaaS事業:LayerX インボイス, LayerX ワークフロー
  • LayerX Labs:Anonifyを使った事業研究

このようにプロダクト領域が定まるとともに、チームも固定化されました。 それとともに、コンサル事業をしていたころのような社内 or Notな情報セキュリティ管理の二元論ではなくなり、 各部は各部の事業領域および成功にコミットする方針が打ち出されました。 言い方を変えると、仮に社内であっても職権や業務形態によって情報セキュリティ管理に強弱が発生したにほかなりません。 情報セキュリティ管理に毀損があればリスクが顕在化することに変わりはありませんが、 そのリスクの性質およびその変化に対する対策も大きく転換を強いられることになります。

最初のガバナンス態勢とゴール

そうしてまず2020/08に立ち上がったのが「ガバナンスをいい感じにし隊」です。

f:id:kengoscal:20210902015120p:plain
初回ガバナンス定例

この時点では、横断組織としてのCTO室がボトルネックにならないよう、各部におけるインフラ的なCommit(もちろんGit的な意味で)をしていたエンジニアを担当者として呼んでいます。 横断組織における中央集権化は早晩にボトルネック化し、結果的に場当たり的かつ非実践的な対応になりがちだからです。

そして私の経験上、横断組織はそもそも採用が難しい(≒候補が少ない)領域です。 もちろん採用を諦めるわけではないのですが、人の頭数によるスケールは悲観的に見ざるをえず、特に各部が独自の技術スタックを採用する当社においては、 事業リスクと対策・実装に詳しい現場が主導的に動いてもらう必要がありました。

ガバナンスをいい感じにし隊では、そういった現場の実装的なAsIsと世間一般で言われるベストプラクティスとのすり合わせをする場としての意識をしていました。 具体的には、以下のようなことを議題として取り上げています。

  • AWSリソースにつけるタグのルール
  • Secretsの管理方法
  • AWS環境へのログイン方法

参加者のリソースの使い方に関する議論もあるなど、この頃はunofficialな場ではありました。

ISMS取得が決まってからの変化

2021/02に経営レベルでISMSの取得が決定しました。ゴールは変わりませんが、体制とスコープが変わりました。

f:id:kengoscal:20210902021416p:plain
情報セキュリティ委員会

情報セキュリティ委員長に代表取締役である松本( @y_matsuwitter )を設置することで、 体制がかなりオフィシャルなものになりました。

同時にメンバーをエンジニアに限定せず、経営管理や人事広報も含め各部から情報セキュリティ担当者を任命しています。 特に事業部については、広く様々な観点が必要であるため、複数人にコミットをお願いしました。 例えばDX事業部ですと、エンジニアとBiz(営業)を1名ずつ確保した形になっています。

なお、三井物産様と当社が出資して共同設立した「三井物産デジタルアセットマネジメント(MDM)」でアセマネサービスを手掛けるMDM事業部は、 全員MDMに出向し、全て先方の契約・管理するシステム上で開発・運用することになっているため、LayerXのISMS審査対象からは外れています(この辺はまた別ブログにて)。

ちなみに、ISMSの審査自体は全てZoomによるリモート審査でした。 ただISMSの範囲に含まれるオフィスセキュリティなどの物理的な確認はZoomで画面共有をしながら審査してもらいました。 たまたま出社している社員が指名され「今、情報資産の扱いはどうなっているか」といった抜き打ち的審査もありました。

どういうやり方をやっているか

現在は月1度の定例を設け、下記の情報を共有・議論しています。

  • 各部施策の進捗状況確認
  • ヒヤリハットの共有
  • 最新の動向の共有

また、全社を巻き込む議論については、GitHubのIssueでの議論をしています。 例えば次のIssueでは、LayerXに参加した時点で追加されるグループメールを、 事業の変化とともにその役割をおえたということで廃止を提案したものです。

f:id:kengoscal:20210902092932p:plain
全社メーリス廃止に関する議論

ここで重要なのは、背景とその際に上がった意見、そして結論に至った仮定を残し、 タイミング関係なくそういった文脈が参照可能な形で保存されることです。 そういった意味では、だんだんとGitHubを使わないメンバーが増える中、また 異なるやり方を考えねばならないフェーズに来ていると思っています。

これから何をやるか

さて、ITガバナンスが代表取締役配下におかれたということは、PDCAのように執行および振り返りを計画的にやっていく必要があります。2021年度の対応としてエイヤっとやることは、以下の通りです! ちょっとまだブログで公開できるか自信がないので、まずは量が多いことだけお伝えします。

f:id:kengoscal:20210902011346p:plain

従来だとこのようなガバナンス活動は、エンジニア以外が手動でGUIを操作したり、チェックを年数回確認することが一般的だったかと思います。 しかし、不確実性が高いスタートアップでは、ソフトウェア化によるガバナンスのコード化によって、計測可能な形でアジリティの高いセキュアな活動をしていかねばなりません。 従って、人を募集しようにもなかなか「これ!」といったポジション名を特定できません。 そこで私たちは、あまり見ない求人を作成しました。名付けて「屋台骨エンジニア」です。

LayerXらしい「複数事業の」「社内基盤を整え」「 メンバーの生産性をより上げていく」エンジニアをお待ちしています - 株式会社LayerX

新しすぎて「?」になること間違い無いかと思いますが、要は横断的な活動によって事業を支えるポジションです。 本件について詳しく聞きたい方は当社のポジションにアプライいただくか、あるいは気軽にken5scal個人のmeetyでお話・議論させていただけると幸いです

meety.net

meety.net

お待ちしております!

Go: json パッケージ Marshaler/Unmarshaler の実装例

LayerX インボイス を開発しているDX事業部の @yyoshiki41(中川佳希)です。

今回は、json パッケージにある Marshaler, Unmarshaler インターフェイスを満たす構造体を用いたアプリケーション実装の例を紹介します。

Marshaler, Unmarshaler インターフェイス

Go ではインターフェイスを命名する際、実装されたメソッド名や構造体名の末尾に er を付ける慣習があります。 Marshaler, Unmarshaler も下記のようなメソッドを実装するインターフェイスとして定義されています。

type Marshaler interface {
    MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
    UnmarshalJSON([]byte) error
}

pkg.go.dev

UnmarshalJSON は、インプットの json データをハンドルして Go の型へ変換したい時など、
MarshalJSON は、その構造体を json データとしてシリアライズして出力する際などに利用できます。

例. 文字列 "2021-08-31" を time.Time 型へ。json 出力時には "2021-Aug-31" へ。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "time"
)

type T struct {
    Time time.Time `json:"time"`
}

func (t *T) UnmarshalJSON(b []byte) error {
    var s string
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    v, err := time.Parse("2006-01-02", s)
    if err != nil {
        return err
    }
    t.Time = v
    return nil
}

func (t T) MarshalJSON() ([]byte, error) {
    return json.Marshal(t.Time.Format("2006-Jan-02"))
}

func main() {
    b := []byte(`["2021-08-31"]`)
    t := []T{}
    err := json.Unmarshal(b, &t)
    if err != nil {
        log.Fatal(err)
    }
    for _, v := range t {
        fmt.Printf("struct: %q\n", v)
     
        b, err := json.Marshal(v)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("json: %s\n", b)
    }
}
struct: {"2021-08-31 00:00:00 +0000 UTC"}
json: "2021-Aug-31"

アプリケーションでの実装例

店舗側画面での権限を管理するアプリケーションを想定してみましょう。 各ユーザー(店舗のスタッフ)に対して、画面ページ(やデータソース)毎に権限を与えるものです。

UI イメージ

f:id:yyoshiki41-lx:20210830104135p:plain

UIクライアント側では、真偽値としてデータを解釈することが先に決まります。

サーバーサイドAPI、DB側ではどのようなデータの持ち方をするでしょうか? 今後もUIが必要とするAPIやデータリソースは変化するという前提とします。

DB

UI で必要となるデータ構造は上でも述べたように真偽値です。

f:id:yyoshiki41-lx:20210830135024p:plain

クライアントからのリクエストを保存する際は、UI側と同じ json データでDBに保存しています。(UI側のフォームの部品などが変わった場合にはバージョニングなどで対応が必要です。)

{
  "shop": {
    "view": true,
    "update": false
  },
  "product": {
    "view": true,
    "create": false,
    "update": false,
    "delete": false
  }
}

サーバーサイドAPI

アプリケーションコード側ではどのようにこのデータを扱うか? 真偽値によって、アクションを制御できるよう検討してみます。

例えばショップ情報の閲覧権限(view)が、

  • true であれば、 GET /shops という RestAPI へのリクエストが可能
  • false であれば、リクエストに対してステータスコード 403 Forbidden を返す

という具合です。

f:id:yyoshiki41-lx:20210830135932p:plain

今回は、json パッケージのカスタム Marshaler/Unmarshaler で解決する方法を考えてみたいと思います。

許可されているAPIエンドポイントを表す構造体をスライスで持つ PermissionShop 構造体を実装します。 (この構造体は、 Marshaler/Unmarshaler インターフェイスを満たしています。)

type Path string
type Method string

type API struct {
    Path   Path
    Method Method
}

type PermissionShop struct {
    View   []API `json:"view"`
    Update []API `json:"update"`
}

func (p *PermissionShop) UnmarshalJSON(b []byte) error {
    s := map[string]bool{}
    if err := json.Unmarshal(b, &s); err != nil {
        return err
    }
    for k, flag := range s {
        switch k {
        case "view":
            if flag {
                p.View = []API{
                    {"/shops", "GET"},
                }
            }
        case "update":
            if flag {
                p.Update = []API{
                    {"/shops", "GET"},
                    {"/shops", "PUT"},
                }
            }
        }
    }
    return nil
}

func (p PermissionShop) MarshalJSON() ([]byte, error) {
    s := map[string]bool{
        "view":   bool(len(p.View) > 0),
        "update": bool(len(p.Update) > 0),
    }
    return json.Marshal(s)
}

真偽値を用いてアクセスコントロールを行うミドルウェアなどでは、DB から取得してきた json を Unmarshal して、API エンドポイント構造体のスライスにしてハンドル出来るようになります。(真偽値で false の場合には、スライスの要素数が0として表現されます。)

// DB に保存されていた json データ
b := []byte(`{"view": true, "update": false}`)
// Goの構造体へ変換を行い、ミドルウェアなどで使用する
p := PermissionShop{}
err := json.Unmarshal(b, &p)
// PermissionShop が、リクエストされたAPIエンドポイントをスライスに含んでいる場合、リクエストが許可される
allowed := p.Contains(API)

まとめ

Marshaler/Unmarshaler を実装する構造体の実装例を作成していきました。 raw そのままでは扱いにくい json データも、アプリケーションコードで扱いやすい構造体に変換することが出来ます。 もしくは、構造体から json にシリアライズするなどが出来ると必要なコードをスッキリさせたり、ロジックの変化にも対応しやすくなります。

LayerX インボイスでは、 go generate と組み合わせてアクセスコントロールなどに使っています。

  • API エンドポイント一覧は、swagger 定義から生成
  • PermissionResource は、 UI コンポーネントのフォームから生成
  • UnmarshalJSON, MarshalJSON メソッドの実装は、コード生成と相性の良い汎化できる分岐ロジック

開発スピードに耐えれる、変更に強い設計を今後も追求できればと考えています。

興味が湧いたという方は、ぜひ一度話を聞きに来てみてください。

herp.careers

エントリーはちょっとという方、こちらから中の話を聞くこともできます!

meety.net

Amazon ECS と AWS Fargate で動作する「LayerX インボイス」のコスト最適化手法

f:id:shun_tak:20210719111823p:plain

こんにちは!LayerXの高際 (@shun_tak) です!過去にはOCR関連の記事を書きました!

LayerX インボイスのアプリケーション・サーバーはAmazon ECS と AWS Fargateで動作しています。今回の記事ではそのコスト最適化手法について解説したいと思います。よろしくお願いします!

tech.layerx.co.jp

サマリーとしては、LayerX インボイスではオーソドックスに費用の削減タスク数の最適化という2つの方法でコスト最適化を行っています。

  1. Savings PlansとFargate Spotを活用した費用の削減
  2. Auto ScalingとScheduled scalingによるタスク数の最適化

早速次節から解説していきます!

Savings Plansを活用した費用の削減

Fargateは使った分だけ料金が請求されます(vCPU数×メモリサイズ×起動時間)。この料金体系はクラウドの最も優れた特性の一つで、事業がうまくいくかどうか分からなかったり、その負荷の量が読めないときには非常にありがたいです。

しかし、一定の顧客を獲得することに成功し事業が安定してきたならば、1年または3年契約の1時間あたりの利用金額をコミットするSavings Plansを活用することで、さらなる費用の削減を実現できます。

東京リージョンでの削減額は1年全額前払いで22%削減、3年全額前払いで47%削減です(2021年7月18日時点)。また、削減額は減りますが毎月払いも可能です。すごい!参考:Savings Plans 料金

1時間あたりの利用金額での契約なので、定常的に利用する分の利用料に合わせて契約するのが良いようです。LayerX インボイスでは多くの企業が休日となる土日など、タスク数が少ない時間の利用料に合わせて契約しています。

どのくらい契約すればいいのか分からない場合、AWSのマネジメントコンソール内のCost Explorerを見るとちょうどいい購入金額を教えてくれるので、そちらも参考に決定しましょう。

f:id:shun_tak:20210719112203p:plain

(画像はAWS Cost Explorerより引用)

Fargate Spotを活用した費用の削減

LayerX インボイスでは後述するAuto Scalingによるタスク数の最適化を行っていますが、Fargate Spotを活用することでさらなる費用の削減を実現できます。

EC2のスポットインスタンスを活用したことある方ならピンと来たと思いますが、Fargate SpotはAWSの余剰キャパシティを活用してタスクを実行することで、最大70%の割引を受けられます。参考:AWS Fargate 料金

Fargate Spotは余剰キャパシティで稼働しているため、AWSにキャパシティが必要になるとデフォルトで30秒、最大で2分前に警告が送信され、実行中のタスクにSIGTERMシグナルが送信され、アプリケーションが終了します。参考:Fargate Spot 終了通知の処理

LayerX インボイスのアプリケーション・サーバーではREST APIに go-swagger を、ジョブワーカーに go machinery (v1) を利用して実装していますが、それぞれSIGTERMシグナル受信時のGraceful Shutdownに対応しているので、Fargate Spotの終了が通知されても安全にアプリケーションを終了できます。

ただし、すべてのタスクが終了されるとさすがに困るので、一部のタスクは通常のFargateで、残りはFargate Spotで稼働するような構成にしています。

このような設定はECSサービスのキャパシティプロバイダ戦略設定によって調整できます。

以下はECSサービス設定の画面ですが、例えばこの画面のような設定をすることで、5タスクは通常のFargateで起動し、残り全てがFargate Spotで起動するような構成を実現できます。

f:id:shun_tak:20210719112556p:plain

ベースやウェイトの詳しい意味はAmazon ECSのドキュメントをご参照ください。

Auto ScalingとScheduled scalingによるタスク数の最適化

Auto Scalingは多くのサービスで利用されていると思うので詳細は省略しますが、サービスの負荷に応じてタスク数を自動的に増減することで負荷分散をコントロールするとともに、コスト最適化を実現するものです。参考:ECSサービスのAuto Scaling

LayerX インボイスでは、より自動的にタスク数が調整されるターゲット追跡スケーリングポリシーによるAuto Scalingを行っています。

加えて、予測可能な負荷の増減に対応するためのScheduled scalingという仕組みも併用しています。LayerX インボイスでは平日の日中にアクセスが増加することが分かっており、Scheduled scalingにより最小タスク数を増加させるようにしています。

最小タスク数を増やすとコストも増えますが、お客様にご不便をおかけしないよう可用性を高めるために実施しています。これまでコスト削減ではなくコスト最適化という言葉を使ってきたのにはこういう理由もあるのです。

まとめ

本記事では、Savings PlansとFargate Spotを活用した費用の削減、Auto ScalingとScheduled scalingによるタスク数の最適化を組み合わせたコスト最適化手法について解説しました。

コストをうまくコントロールすることは、利益率を高め、ひいてはサービス再投資による顧客の満足につながるものです。基本的な取り組みではありますが、こういったことこそ大切にしていかねばなと思います。

基本に忠実に、製品を磨き、顧客に向き合う。そんな仲間を募集しています。

herp.careers

SQLは口ほどにものを言うーRedashによるクエリ共有、監視、データ連携によるチーム効率化ー

f:id:naomasabit:20210704162817p:plain

こんにちは。DX事業部の花村(@naomasabit)です。先日の投稿でユーザーの利用状況確認のためにAWSのQuickSightを利用していると書きましたが、並行して分析ツールのRedashも利用しています。Redashの良い点としてクエリベースでの分析、監視アクション、スプレッドシートとのデータ連携が存在します。

SaaSチームの運営において、これらを活用したユースケースについて伝えていきます。

アドホックな分析クエリの共有によるコミュニケーション効率化

Redashでは、まずクエリベースでアドホックな分析クエリの共有が可能です。アドホックにデータ状況を見たいケースなどで、SQLを書いて分析が可能です。

f:id:naomasabit:20210702143207p:plain

書いたSQL文はURLベースでチーム内に共有できます。LayerXではBizメンバーがSQLを叩くことが日常となっているため、クエリを書いてささっと共有してコミュニケーションをしています。

f:id:naomasabit:20210704162434p:plain
Bizメンバーがクエリを書いてチーム内に共有する図

SQLがなければ、「調査したところX%のユーザーがYのアクションを行い〜」という文章で説明しなければいけませんが、SQLという共通言語を持つことでこのように「ほしい情報出せたろう(残りはクエリとデータセットを見て個々人それぞれコメントしよう)」という言葉だけでコミュニケーションが完了します。

エンジニア、カスタマーサクセス、セールスと多くのロールが混じり合うSaaS組織において、コミュニケーションの効率化に非常に役立ちます。SQLは口ほどにものを言います。

監視アクション設定によりデータ不整合にすぐ気づける体制整備

f:id:naomasabit:20210702110715p:plain

Redashはチェッククエリに基づいてアラートを設定することができ、設定した閾値条件になると指定した場所へ通知します。画像はアラート設定画面で、分析のためのクエリを別途設定し、その結果に基づいてアラートの閾値条件を設定します。通知先は様々設定できますが、LayerX インボイスではSlackを通知先に設定しています。

f:id:naomasabit:20210702135838p:plain

監視対象の通知が動くとSlackには画像のようなアラートが飛んできます。修正を施してアラート条件から脱すると、回復の通知も飛んできます。このような仕組みによりデータ不整合の検知と対応を素早くエンジニアができるようにしています。データ不整合は新たなデータ不整合を呼びがちなため、素早い監視が重要になる領域です。

システムエラーの監視にはDataDogを用いていますが、エラーの結果や意図していないオペレーションなどでデータ不整合が起きることが存在します。業務用SaaSであるLayerX インボイスでは、データ不整合が起きてはいけない箇所にチェッククエリを投げ、不整合があった場合はslackに適宜通知を投げるようにしています。データ不整合はそもそも起きないのがベストですが、あったとしても早め早めに気づいて対応し、素早く修正して傷口を最小限に防ぎ、改善していくサイクルを起こすことが肝要です。

複数チームからのデータソース連携によるヘルススコアダッシュボード作成

カスタマーサクセスにおいて、ユーザーがサービスをうまく活用してくれているかを測る「ヘルススコア」も監視しています。LayerX インボイスにおいてヘルススコアのインプットはサービス活用状況を集約したDBに限らず、成約前にセールスが得たユーザー企業の基礎情報なども含みます。

そのため、サービス活用状況のDBとセールスからの情報を合わせてヘルススコアを記録する必要があります。多くのサービスではS3などストレージとの連携には対応していますが、セールスチームが関連情報をCSVにしてアップロードしてS3に上げて連携…というのは非常に手間です。セールスチームにとって扱いやすいデータ形式のインプットを用意してあげる必要があります。

f:id:naomasabit:20210702132746p:plain Supported Databases, APIs and Integrations

redashは多くのIntegrationが用意されており、特にGoogle Spreadsheeetsとの連携は非常に便利です。Redashではスプレッドシートと連携することで、DBと同様のデータソースとして扱うことができます。セールスチームが慣れ親しんだスプレッドシートにデータを入れることで、情報が即時反映されます。

f:id:naomasabit:20210702134144p:plain

セールスチームから、成約した新規ユーザーをカスタマーサクセスチームに連携するとき、ユーザーの基礎情報をスプレッドシートに記入します。カスタマーサクセス業務においては、その基礎情報と分析用DBを突き合わせてヘルススコアを確認し、対策をカスタマーサクセスチームで練ってアクションに落とし込むことができます。

最後に - Redashと他の分析ダッシュボードツールの併用について

Redashを用いたデータの監視、複数のインテグレーションによるモニタリング効率化について書きました。Redashはリアルタイムのデータ監視や、データ連携が得意な一方、ドリルダウン分析などはRedashの得意な分野ではありません。簡単なダッシュボードを並べることは可能ですが、全体傾向から個別ユーザーの状況へ、複数のダッシュボードを組み合わせて深掘りするような分析においては、QuickSightやTableau、Lookerといったツールの方が適しています。いずれも予算帯との相談ではありますが、LayerX インボイスでは素早い分析や監視のためのRedashと、深掘りするための分析ダッシュボードツールも併用する運用にしています。

分析ツールと一言でいっても特性が様々に異なり、インテグレーションのサポートも様々です。そのような特性を見ながら、分析といってもケースを分けて最適なツールをそれぞれ選定していくことが重要なのだと思います。

Terraform import のススメ 〜開発効率化編〜

こんにちは、LayerX で主にインフラを担当している高江です。
今回は、一見地味ではありますが実はとても役に立つ機能である Terraform import についてお話したいと思います。 

Terraform import とは

公式サイトでは次のように説明されています。

Terraform is able to import existing infrastructure. This allows you take resources you've created by some other means and bring it under Terraform management.

要するに、AWS 等のサービスプロバイダー上に既に存在する、Terraform 管理されていないリソースの情報を取得して Terraform 管理下に置く(tfstate ファイルに import する)機能です。
また、同じく公式サイトには

This is a great way to slowly transition infrastructure to Terraform,

ともあります。
このことから、まだ IaC (Infrastructure as Code) を実践していない環境において、既存のリソースを運用しながら徐々に Terraform による IaC を進めていくときなどに有用な機能であることが分かります。

ただ、import の利用シーンはそれだけではなく、開発を効率化する際にも力を発揮します。
ここでは、普段の開発において私がどのように import を活用しているかをご紹介します。

Terraform import の使い方

その前に、まずは import の使い方について簡単に説明します。
コマンドの形式は以下になります。 (公式サイト参照)

% terraform import [options] ADDRESS TO

ADDRESS は Terraform ファイル(.tf)内で定義した、対象リソースの type と name です。 TO は対象とするリソースによって違いがあり、ID だったり ARN だったりします。

また、import を実行するためには、事前に対象のリソースを定義した Terraform ファイルを作成しておく必要があります。 例えば、sample-bucket という名前の S3 bucket を import する場合の手順は以下のとおりです。

1.Terraform ファイルを作成

resource "aws_s3_bucket" "sample_bucket" {
    bucket = "sample-bucket"
}

2.Terraform import を実行

% terraform import aws_s3_bucket.sample_bucket sample-bucket

これによって、import した sample-bucket が tfstate に追加され、Terraform 管理下に置かれることになります。
S3 bucket を import する場合は上記のように bucket 名を指定します。対象リソースによって何を指定するかは変わってくるので、詳細は公式サイトの各リソースのページを参照ください。ページ最下部に import する場合の説明が記載されています。S3 bucket の場合はコチラになります。

Terraform import を使って開発を効率化する

ここからが本題です。Terraform を使ってインフラを構築するにあたり、「この場合どう書けばいいんだっけ?」というケースが出てくると思います。特に、初めて Terraform で構築しようとするリソースであったり、Terraform を使った開発経験自体があまりない場合には、正しいリソース定義の書き方について迷うことも多いのではないでしょうか。

このような場合に、公式サイトのドキュメントを見たりググったりしながら手探りで作るのではなく、import を利用することで開発を効率化することができます。
ここでは、AWS CodePipeline と AWS CodeBuild を使ったビルドパイプラインを構築する例をもとに、具体的な開発の流れについて説明します。

パイプラインの概要

構築するパイプラインを図1に示します。

f:id:shnjtk:20210625102609p:plain
図1 ビルドパイプライン アーキテクチャ

  • CodePipeline でパイプライン全体の構成を管理
  • CodeStar connections で GitHub リポジトリと接続
  • 取得したコードを CodeBuild に渡すために S3 bucket をアーティファクトストアとして利用
  • CodeBuild でビルド実行
  • IAM で各種権限管理

開発の流れ

では開発に移っていきましょう。全体の流れとしては以下のようになります。

  1. AWS 管理画面からマニュアルでリソースを構築
  2. terraform import で tfstate を更新
  3. tfstate の内容を見ながら Terraform ファイルを編集
  4. terraform plan で差分を確認しながら Terraform ファイルを編集

1. AWS 管理画面からマニュアルでリソースを構築

AWS 管理画面にログインし、ボタンポチポチでパイプラインを構築していきます。このとき、マニュアルで作るものは仮として、後から Terraform で作り直す場合はそれと分かるようにリソース名の prefix や suffix に temp のようなものを付与することをおすすめします。今回は、リソース名を terraform-import-sample としました。

f:id:shnjtk:20210625112144p:plain
図2 AWS 管理画面からのパイプライン構築の様子

2. terraform import で tfstate を更新

次に、以下の Terraform ファイルを作成して import を実行します。

  • main.tf
terraform {
  required_version = "~> 1.0"

  required_providers {
    aws = {
      version = "~> 3.47"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

resource "aws_codepipeline" "import_sample" {}
  • コマンド
% terraform init

# AWS CodePipeline の import はリソース名を指定する
% terraform import aws_codepipeline.import_sample terraform-import-sample

そうすると、次のような tfstate ファイルが作成されます。これが、Terraform ファイルでリソースを定義していく上でのヒントになります。

{
  "version": 4,
  "terraform_version": "1.0.1",
  "serial": 1,
  "lineage": "00c448d1-bda9-2c60-68e8-0be02331c6d9",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_codepipeline",
      "name": "import_sample",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "arn": "arn:aws:codepipeline:ap-northeast-1:(snip):terraform-import-sample",
            "artifact_store": [
              {
                "encryption_key": [],
                "location": "codepipeline-ap-northeast-1-(snip)",
                "region": "",
                "type": "S3"
              }
            ],
            "id": "terraform-import-sample",
            "name": "terraform-import-sample",
            "role_arn": "arn:aws:iam::(snip):role/service-role/AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp",
            "stage": [
              {
                "action": [
                  {
                    "category": "Source",
                    "configuration": {
                      "BranchName": "main",
                      "ConnectionArn": "arn:aws:codestar-connections:ap-northeast-1:(snip):connection/(snip)",
                      "FullRepositoryId": "(snip)",
                      "OutputArtifactFormat": "CODE_ZIP"
                    },
                    "input_artifacts": [],
                    "name": "Source",
                    "namespace": "SourceVariables",
                    "output_artifacts": [
                      "SourceArtifact"
                    ],
                    "owner": "AWS",
                    "provider": "CodeStarSourceConnection",
                    "region": "ap-northeast-1",
                    "role_arn": "",
                    "run_order": 1,
                    "version": "1"
                  }
                ],
                "name": "Source"
              },
              {
                "action": [
                  {
                    "category": "Build",
                    "configuration": {
                      "ProjectName": "terraform-import-sample"
                    },
                    "input_artifacts": [
                      "SourceArtifact"
                    ],
                    "name": "Build",
                    "namespace": "BuildVariables",
                    "output_artifacts": [
                      "BuildArtifact"
                    ],
                    "owner": "AWS",
                    "provider": "CodeBuild",
                    "region": "ap-northeast-1",
                    "role_arn": "",
                    "run_order": 1,
                    "version": "1"
                  }
                ],
                "name": "Build"
              }
            ],
            "tags": {},
            "tags_all": {}
          },
          "sensitive_attributes": [],
          "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
        }
      ]
    }
  ]
}

3. tfstate の内容を見ながら Terraform ファイルを編集

main.tf を編集していきます。tfstate ファイルや公式サイトのCodePipeline ドキュメントを参照しながら、必要なパラメータを設定します。

  • main.tf (terraformproviderは省略)
resource "aws_codepipeline" "import_sample" {
  name     = "terraform-import-sample"
  role_arn = "arn:aws:iam::(snip):role/service-role/AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp"

  artifact_store {
    type     = "S3"
    location = "codepipeline-ap-northeast-1-(snip)"
  }

  stage {
    name = "Source"
    action {
      category         = "Source"
      name             = "Source"
      namespace        = "SourceVariables"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["SourceArtifact"]
      configuration = {
        ConnectionArn        = "arn:aws:codestar-connections:ap-northeast-1:(snip):connection/(snip)"
        FullRepositoryId     = "(snip)"
        BranchName           = "main"
        OutputArtifactFormat = "CODE_ZIP"
      }
    }
  }

  stage {
    name = "Build"
    action {
      category         = "Build"
      name             = "Build"
      namespace        = "BuildVariables"
      owner            = "AWS"
      provider         = "CodeBuild"
      version          = "1"
      run_order        = 1
      input_artifacts  = ["SourceArtifact"]
      output_artifacts = ["BuildArtifact"]
      configuration = {
        ProjectName : "terraform-import-sample"
      }
    }
  }
}

4. terraform plan で差分を確認しながら Terraform ファイルを編集

terraform plan を実行してみましょう。

% terraform plan
aws_codepipeline.import_sample: Refreshing state... [id=terraform-import-sample]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

変更なしと表示されましたね。これで、マニュアルで作ったものと main.tf ファイルで定義したものが同一になることが確認できました。

CodePipeline 以外のリソースについても同様の作業を行います。例えば、今の main.tf では IAM role が ARN ハードコードになっているので、これを Terraform のリソースとして定義します。 そのため、次はこの IAM role を import します。

  • main.tf (IAM role 定義部分のみ抜粋)
resource "aws_iam_role" "import_sample_codepipeline" {}
  • コマンド
# IAM role の import はリソース名を指定する
% terraform import aws_iam_role.import_sample_codepipeline AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp

そうすると、tfstate に IAM role が追加されます。(該当箇所のみ抜粋)

    {
      "mode": "managed",
      "type": "aws_iam_role",
      "name": "import_sample_codepipeline",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "arn": "arn:aws:iam::(snip):role/service-role/AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp",
            "assume_role_policy": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"codepipeline.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}",
            "create_date": "2021-06-25T01:22:35Z",
            "description": "",
            "force_detach_policies": false,
            "id": "AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp",
            "inline_policy": [
              {
                "name": "",
                "policy": ""
              }
            ],
            "managed_policy_arns": [
              "arn:aws:iam::(snip):policy/service-role/AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-sample"
            ],
            "max_session_duration": 3600,
            "name": "AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp",
            "name_prefix": null,
            "path": "/service-role/",
            "permissions_boundary": null,
            "tags": {},
            "tags_all": {},
            "unique_id": "AROAZYMBT63SJTFIQE7EF"
          },
          "sensitive_attributes": [],
          "private": "eyJzY2hlbWFfdmVyc2lvbiI6IjAifQ=="
        }
      ]
    }

これを参考に、CodePipeline の時と同じようにリソースを定義します。(該当箇所のみ抜粋)

resource "aws_codepipeline" "import_sample" {
    role_arn = aws_iam_role.import_sample_codepipeline.arn
}

resource "aws_iam_role" "import_sample_codepipeline" {
  name = "AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp"
  path = "/service-role/"

  assume_role_policy = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"codepipeline.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"
}

terraform plan を実行してみましょう。

% terraform plan
aws_iam_role.import_sample_codepipeline: Refreshing state... [id=AWSCodePipelineServiceRole-ap-northeast-1-terraform-import-samp]
aws_codepipeline.import_sample: Refreshing state... [id=terraform-import-sample]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

今回も変更なしと表示されましたね。上手くいきました。

このように、import した tfstate ファイルを見ながら必要なパラメータを設定していくことで Terraform によるインフラ開発を効率化することができます。一通りリソース定義が終わったら、マニュアルで作ったリソースや import した tfstate はそのままにして運用するもよし、一旦削除して各種リソース名等をプロジェクトの命名規則等にあわせて作り直すもよしです。

まとめ

以上、Terraform import を利用したインフラ開発の効率化についてご紹介しました。いかがでしたでしょうか?
Terraform import は既存リソースを Terraform 管理下に置く場合に使うものという意識があるかもしれませんが、新規にリソースを構築する際にもとても有用なので、ぜひご活用いただければと思います。 余談ですが、答えを見ながら必要な項目を埋めていくので若干カンニングしてる感があります。

おまけ

ちなみに、上記の IAM role リソース定義で、 assume_role_policy は string でベタ書きされていますが、明らかにメンテナンス性が低いので、Data Source を使って定義した方がよいです。

こんな感じです。(関連箇所のみ抜粋)

data "aws_iam_policy_document" "import_sample_codepipeline_assume_role_policy" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["codepipeline.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "import_sample_codepipeline" {
  assume_role_policy = data.aws_iam_policy_document.import_sample_codepipeline_assume_role_policy.json
}

あるいは、Terraform の IAM role の 公式ドキュメントにもあるように、 jsonencode を使ってもいいですね。
一つのリソースでしか使わないのであれば jsonencode でインラインに、複数のリソースで使うのであれば Data Source として再利用できるように、それぞれ使い分ければいいかなと思います。

それでは。

MySQL Generated Columns を活用したユニークキー制約

DX事業部の @yyoshiki41(中川佳希)です。 現在は、LayerX インボイス という経理業務を行う方を対象ユーザーにした SaaS をメインで開発しています。

今回は、MySQL での Generated Column の活用についての紹介です。

Generated Column とは?

カラム定義時にロジックを組んでおくことで、演算や条件分岐ロジックの結果を値として、仮想的に参照可能にするもしくは記憶領域に格納することが出来る機能です。

dev.mysql.com

ドキュメントには以下のような直角三角形の斜辺 sidec を格納するスキーマの例が紹介されています。

CREATE TABLE triangle (
  sidea DOUBLE,
  sideb DOUBLE,
  sidec DOUBLE AS (SQRT(sidea * sidea + sideb * sideb))
);
INSERT INTO triangle (sidea, sideb) VALUES(1,1),(3,4),(6,8);

AS 以降にカラムの値計算を行う式が定義されています。

mysql> SELECT * FROM triangle;
+-------+-------+--------------------+
| sidea | sideb | sidec              |
+-------+-------+--------------------+
|     1 |     1 | 1.4142135623730951 |
|     3 |     4 |                  5 |
|     6 |     8 |                 10 |
+-------+-------+--------------------+

ユースケース

よく紹介されている例としては、以下のようなものがあります。

  1. 複雑な条件結果を先にカラムに定義しておき、クエリ条件を簡略化させる
  2. STORED を指定して記憶領域に書き込みを行っておき、クエリ参照時には計算コストをかけない
  3. JSON 型のようなインデックスキーをつけれないカラムに対して、インデックスキー用のカラムを生成する

JSON との組み合わせの例としては、以下のようなものです。

CREATE TABLE `tests` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `json` json DEFAULT NULL,
  `version` int GENERATED ALWAYS AS (json_extract(`json`,'$.version')) VIRTUAL,
  PRIMARY KEY (`id`),
  KEY `version` (`version`)
);
INSERT INTO tests(`json`) VALUES ('{"version": "1", "definitions": "foo"}');

json のキーとして version を持つデータに対して、インデックスキーカラムを設定できました。

mysql> SELECT * FROM tests;
+----+--------------------------------------+---------+
| id | json                                 | version |
+----+--------------------------------------+---------+
|  1 | {"version": 1, "definitions": "foo"} |       1 |
+----+--------------------------------------+---------+

アプリケーションでのデータ整合性

アプリケーションが読み書きするデータの整合性は、DB側でも外部キー、ユニークキーやCHECK制約などを用いて担保したいものです。

今回紹介したいのは、ユニークキーとして Generated Columns を活用する例です。

例1. 状態やステータスで有効なユニークキーを表現する

レコードの物理削除を行いたくない場合に、deleted_at カラムを用いることがあるかと思います。 名称 name カラムに対してユニークキーを設定したいとします。

まずは、namedeleted_at(nullableなカラム)の複合ユニークキーを設定した良くない例は下記です。

CREATE TABLE `bad_examples` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_1` (`name`,`deleted_at`)
);

NULLとなっているカラム値に対しユニークキーは有効でないため、重複名が登録できてしまいます

mysql> INSERT INTO `bad_examples`(name) VALUES("foo"), ("foo");
mysql> SELECT * FROM `bad_examples`;
+----+------+------------+
| id | name | deleted_at |
+----+------+------------+
|  1 | foo  | NULL       |
|  2 | foo  | NULL       |
+----+------+------------+
2 rows in set (0.00 sec)

このようなケースでは、deleted_at が NULL であるかをフラグとして持つ Generated Columns を定義し、ユニークキーとして使うと有効です。

CREATE TABLE `good_examples` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  `is_active` tinyint(1) AS ((case when isnull(`deleted_at`) then 1 else NULL end)) VIRTUAL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_1` (`name`,`is_active`)
);
mysql> INSERT INTO `good_examples`(`name`,`deleted_at`) \
    -> VALUES("foo", NULL), ("foo", NOW() - INTERVAL 1 SECOND), ("foo", NOW());
mysql> SELECT * FROM `good_examples`;
+----+------+---------------------+-----------+
| id | name | deleted_at          | is_active |
+----+------+---------------------+-----------+
|  1 | foo  | NULL                |         1 |
|  2 | foo  | 2021-06-20 23:49:03 |      NULL |
|  3 | foo  | 2021-06-20 23:49:04 |      NULL |
+----+------+---------------------+-----------+

例2. ユニークキーの条件がレコードにより異なる

更にこみいったビジネスロジックにおけるデータ整合性を担保する場合を考えてみます。

条件

  1. 各店舗の商品を持つテーブル
  2. 全店舗で商品名 name は必ず持つが、商品コード code は店舗により未設定の場合がある
  3. 商品コード code をもつ場合、code がユニークキーとなる
  4. 商品コード code をもたない場合、商品名 name がユニークキーとなる

これをスキーマとして持つテーブルは以下のように定義できます。

CREATE TABLE `shop_goods` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `shop_id` int unsigned NOT NULL,
  `name` varchar(255) NOT NULL,
  `code` varchar(255) NULL DEFAULT NULL,
  `is_null_code` tinyint(1) GENERATED ALWAYS AS ((case when isnull(`code`) then 1 else NULL end)) VIRTUAL,
    PRIMARY KEY (`id`),
  UNIQUE KEY `uq_1` (`shop_id`, `name`, `is_null_code`),
  UNIQUE KEY `uq_2` (`shop_id`, `code`)
  );

商品コードを設定していない店舗1で、重複した商品名は以下のようにユニークキー制約でエラーになります。

mysql> INSERT INTO `shop_goods`(`shop_id`, `name`, `code`) \
    -> VALUES(1, "商品A", NULL), (1, "商品A", NULL);
ERROR 1062 (23000): Duplicate entry '1-商品A-1' for key 'uq_1'

商品コードを設定している店舗2では、重複した商品名も登録可能になります!

mysql> INSERT INTO `shop_goods`(`shop_id`,`name`, `code`) \
    -> VALUES(1, "商品A", "コードA-1"), (1, "商品A", "コードA-2");
mysql> SELECT * from `shop_goods`;
+----+---------+---------+--------------+--------------+
| id | shop_id | name    | code         | is_null_code |
+----+---------+---------+--------------+--------------+
|  1 |       1 | 商品A    | コードA-1      |         NULL |
|  2 |       1 | 商品A    | コードA-2      |         NULL |
+----+---------+---------+--------------+--------------+

おわりに

Generated Columns の活用例をいくつか紹介してみました。 INSERT/UPDATE 時にはカラムにセットする値をアプリケーション側から無視する必要があったり(もしくは DEFAULT を使う)、考えることはあります。
しかし、有効なケースも多々あると思いますので参考になりましたら幸いです!

herp.careers