hcl2パッケージでhclを書き換えてみた

こんにちは、完璧にblogを書くことをサボってました。 今回はgolangのhcl2パッケージを用いてaws security group id, subnet-idを直接記述されている部分をdata sourceへ書き直すツールをgolangで実装してみました。

背景

自分が所属している会社ではterraformを用いてaws infra resourceを管理しています。またマイクロサービスを展開しており、モノレポでterraform projectを管理しています。 そのリポジトリは歴史も古く、ところどころsecurity goup idやsubnet idが直接記述されているとこがありました。 これでは実態に依存している状態で良くないと考えています。 例えば、マルチアカウント戦略を取るときににはそのソースコードは元アカウントのリソースに依存している為、汎用性がないと言えるでしょう。 また自分はgolangを触ったことがなく、自己学習を兼ねてリファクタリングツールを作ってみようと思い書いてみました。(完全にはまだ実装できていない)

HCLについて

hclのデータ構造は以下のように分類ができます。 file,body, block, attribute,

file

hclファイルをparseするとfileというdata型に変換される。
fileの内部にはbodyというdataを保持しており、bodyの中にはblock, attributeを持つ。
イメージとしては下記のような感じになる。
マトリョーシカぽい感じだ...

File
└── body
    ├── attribute
    └── block
        └── body
            ├── attribute
            └── block

body

file, blockの中身に相当する。

reource "aws_vpc" "main" {
  // body
  vpc_id = ""
  tags {
     // body
  }
}

block

resouce {} など hoge {}で表す構造体。内部にbodyを持つ
実際のblockは下記のようになるかと思います。

resource "aws_vpc" "main" {} // block

locals  {} // block

variable "hoge" {} //block

reource "aws_vpc" "main" {
  // body
  tags {} // block
}

module  "hoge" {
  hoge = {} // これはblockではない
}

attribute

attributeは端的に言えば特定の名前が付けられた値である key = value形式のものをいう 右辺に当たる部分をexpressionと呼びます

resource "aws_vpc" "main" {
    vpc_id = "" // attribute
}

// tfvars
hoge = "hoge" //attribute

module "hoge" {
    hoge = {} // これもattribute
}

ここまでざっくりではありますがhclのデータ構造の説明でした。 hclはfileをrootに持ち、body -> block or attributeというふうにマトリョーシカのようなデータ構造になっている。

HCL2パッケージについて

golangではhclを扱うためにHCL2パッケージを提供しています。用途は主に読み込みと、書き込みであり。読み込みであれば, hclsyntax, gohclといったパッケージを使用することになり、書き込みを行う場合はhclwriteというパッケージを用いることになると思います。
今回はhclファイルのparse, 対象のresource idにマッチしたものを書き換え、書き換えたblock path(data.subnet_groups.hogeといったもの)にマッチしたdata source blockを生成するためhclwriteを用いました。

hclのファイル, 各種dataの読み込み

hclwriteでhclファイルをparseするには下記のように記述します。

file, err := os.ReadFile(path)
if err != nil {
    log.Fatal(err)
}

hclFile, diags := hclwrite.ParseConfig(file, path, hcl.Pos{})
if diags != nil {
    log.Fatal(diags)
}

上記でhclファイルをparseできたので、fileの中のblockを取得するには下記のようになります

for _, block := range hclFile.Body().Blocks() {}

さらにblockで定義されたattributeの一覧を取得するには下記のようになります。

for _, block := range hclFile.Body().Blocks() {
       attributes := block.Body().Attributes()
}

次にattibutesのexpressionを取得するには下記のようになります

for _, block := range hclFile.Body().Blocks() {
       attributes := block.Body().Attributes()
       for _, attribute := range attributes {
               attibute..Expr()
       }
}

愚直に各データにアクセスするには上記で対応できるかと思います。
特定のattributeにアクセスする、blockにアクセスするなどはhclwriteパッケージには用意されているのでdocを拝見していただければと思います。

subnet, sg idを書き換える

本題のsubnet-id, sg-idを書き換えなんですが, 一覧の流れとしてはresource idを定義したyamlを準備し、それをparseしaws-sdk用いてidに紐付いたreosurce情報を取得、必要dataをトリミングする。トリミング後そのデータを用いてsg-xxxxdata.aws_security_group.xxx.ids[0]に書き換えを行い、それに相当するhcl構文を生成するという流れになります。

 yaml parse後aws-sdkを用いてresource情報を取得

func fetchAwsResource(subnetIds SubnetIds, sgIds SgIds) map[string]string {
    ctx := context.TODO()
    cfg, err := config.LoadDefaultConfig(ctx)
    if err != nil {
        panic(err)
    }

    // clientの生成, aws profile情報を読み込む
    client := ec2.NewFromConfig(cfg)
    // sgの取得
    groupInputs := ec2.DescribeSecurityGroupsInput{GroupIds: sgIds}

    //security groupの取得
    sgOutput, err := client.DescribeSecurityGroups(ctx, &groupInputs)
    if err != nil {
        panic(err)
    }
    sgMaps := createSecurityGroups(sgOutput, client, ctx, sgIds)

    // subnetの取得
    subnetOutput, err := client.DescribeSubnets(ctx, &ec2.DescribeSubnetsInput{
        SubnetIds: subnetIds,
    })
    if err != nil {
        panic(err)
    }
    subnetMaps := createSubnetGroups(subnetOutput, client, ctx, subnetIds)

    resourceMaps := mergeMap(sgMaps, subnetMaps)

    return resourceMaps
}

変更対象を検出

変更対象をserchする。
変更対象が見つかったら、下記の戻り値をもとに、その変更対象を構造体として保持する。

// tokenとreplaceAwsResourcesのkeyを比較して一致したらmapに格納し返却する
func appendMatchResourceId(attribute *hclwrite.Attribute, replaceAwsResources map[string]string) map[string]string {
    awsResourceIds := map[string]string{}
    tokens := attribute.Expr().BuildTokens(nil)
    for _, token := range tokens {
        str := string(token.Bytes)
        for key, resourceId := range replaceAwsResources {
            //aws resource idとattributeのtokenが一致するか
            if !strings.Contains(str, resourceId) {
                continue
            }
            awsResourceIds[key] = resourceId
        }
    }

    return awsResourceIds
}

変更対象を書き換え

上記で変更対象を割り出して、変更対象のattribute.expressionを書き換えていく。

func (r *Rewrite) writeToken(attributes map[string]*hclwrite.Attribute) {
    //bodyのattributeを取得しloopで回す
    for attributeKey, attr := range attributes {
        // addressにかくのされているattribute名と一致するを確認
        if attributeKey == r.Address.GetAttribute() {
            // attributeのtokenを取得しloopで回す
            tokens := attr.Expr().BuildTokens(nil)
            for tokenIndex, token := range tokens {
                // tokenの文字列を取得
                hclToken := string(token.Bytes)

                // address("data.hoge": "sg-hogehoge" )のlistを取得しloopで回す
                for key, awsId := range r.Address.GetAwsIds() {
                    // tokenの文字列にaddressの文字列が含まれているか確認
                    if hclToken == awsId {
                        newLabel := createDataSourceSyntax(hclToken, key, awsId)

                        //対象のaddressと一致するtokenを書き換える
                        frontNodeString := string(tokens[tokenIndex-1].Bytes)
                        backNodeString := string(tokens[tokenIndex+1].Bytes)
                        tokens[tokenIndex].Bytes = []byte(strings.ReplaceAll(hclToken, awsId, newLabel))
                        tokens[tokenIndex-1].Bytes = []byte(strings.ReplaceAll(frontNodeString, frontNodeString, ""))
                        tokens[tokenIndex+1].Bytes = []byte(strings.ReplaceAll(backNodeString, backNodeString, ""))
                    }
                }
            }
        }
    }
}

書き換え後、それに相当するblockを生成する

書き換えて、書き換えた内容に相当したdatasourceブロックを生成する
地味にhclTokens{}で定義される右辺の内容を理解するのに手こずってしまった。
hclはbuildTokenというdata型に変換でき、tokenに変換することで fileの中身をbyte列で取得することができる。
特定のattributeをtokenに変換できるし、expressionも変換可能である。
もちろん、下記のコード以外にhclパッケージにはattribute, expressionを生成してくれる関数も存在するので、詳しくはdocを参照してみてください。

// datasource blockを作成する
func createBlock(hclBody *hclwrite.Body, newLabel, datasource, resourceTagName, vpcString string) {
    block := hclBody.AppendNewBlock("data", []string{datasource, newLabel})
    body := block.Body()
    vpcFilterBlock := body.AppendNewBlock("filter", []string{})
    vpcFilterBody := vpcFilterBlock.Body()
    vpcFilterBody.SetAttributeValue("name", cty.StringVal("vpc-id"))
    vpcTokens := hclwrite.Tokens{
        {
            Type:  hclsyntax.TokenOBrack,
            Bytes: []byte{'['},
        },
        {
            Type:  hclsyntax.TokenIdent,
            Bytes: []byte(vpcString),
        },
        {
            Type:  hclsyntax.TokenCBrack,
            Bytes: []byte{']'},
        },
    }

    vpcFilterBody.SetAttributeRaw("values", vpcTokens)
    body.AppendNewline()

    filterResourceNameBlock := body.AppendNewBlock("filter", []string{})
    filterResourceNameBody := filterResourceNameBlock.Body()
    var filterResourceName hclwrite.Tokens
    if datasource == aws_subnets {
        filterResourceNameBody.SetAttributeValue("name", cty.StringVal("tag:Name"))
        filterResourceName = hclwrite.Tokens{
            {
                Type:  hclsyntax.TokenOBrack,
                Bytes: []byte{'['},
            },
            {
                Type:  hclsyntax.TokenOQuote,
                Bytes: []byte(`"`),
            },
            {
                Type:  hclsyntax.TokenIdent,
                Bytes: []byte(resourceTagName),
            },
            {
                Type:  hclsyntax.TokenCQuote,
                Bytes: []byte(`"`),
            },
            {
                Type:  hclsyntax.TokenCBrack,
                Bytes: []byte{']'},
            },
        }
    }

    if datasource == aws_security_groups {
        filterResourceNameBody.SetAttributeValue("name", cty.StringVal("group-name"))
        filterResourceName = hclwrite.Tokens{
            {
                Type:  hclsyntax.TokenOBrack,
                Bytes: []byte{'['},
            },
            {
                Type:  hclsyntax.TokenOQuote,
                Bytes: []byte(`"`),
            },
            {
                Type:  hclsyntax.TokenIdent,
                Bytes: []byte(resourceTagName),
            },
            {
                Type:  hclsyntax.TokenCQuote,
                Bytes: []byte(`"`),
            },
            {
                Type:  hclsyntax.TokenCBrack,
                Bytes: []byte{']'},
            },
        }
    }
    filterResourceNameBody.SetAttributeRaw("values", filterResourceName)
}

断片的ではありますが、以上でhclの特定のexpressionの書き換え、それに相当したdatasourceの生成を行いました。
もちろん上記コードは完全なものではありません。
実際は環境を判別し、それに相当したvpcのdatasouceを生成するコードも存在します。repositoryも社内codeに依存する部分があり公開することができないです。
不完全な内容で簡易的ではありますが、hclの操作例を記述してみました。

SRE本を読んでみた2

こんにちは長い間ブログをサボってました。 色々あってなかなか取り組むことができなかったです。 その話はまた別の機会にでもは書いてみます。

長い間SRE本を読むこともサボっていました。というのも、現在はSREとは少し遠いことをやっていたので、そちらの取り組みを主に行なっていました。(サボり)

また所属会社では来Qからproduct teamへSRE Practiceを注入するためのSREチームへ移動することが決まりまた読まなきゃなっていう気持ちで読み進めています。読んだことをまたアウトプット行きたいと思います

第四章

この章は前章のSLIを設定するがその目標値(SLO)を定め管理するのと、その目標値の測定と評価法を述べている。

SLI(サービスレベル指標)

サービスの信頼性の指標。特に重要にしているのがサービスSLIがリクエストのレイテンシである。もう1つ重要な指標としては可用性である。処理に成功したリクエスト数の比率をもとにすると説明があるが、サービスの特性を考えて、userが何を期待いているのかを加味した上でSLIを設定することが大切なのだと感じた。 サービス特性を考慮しメトリックスを収集するのが望ましいし。また、その値を収集していない場合はいったんそれを収集して観察すること。 ここでメトリクス取集として大切なことは平均値ではなく中央値で取集することらしい。 平均値は突発的な変化をマスクしてしまうので可視性の観点で向いていないためである。

実際のSLIの取り決め方はCUJを割り出して、そのエンドポイントに対してサービスの特性を加味した上で、レイテンシ、可用性、スループットなどを取り決める。あくまでサービスの特性を加味するので、必ずしもレイテンシ、可用性、スループットの指標を導入しない。サービスの特性を考えて取捨選択する。 これらの指標を取り決め、その値を取集する。

SLIはテンプレート化しておくこと、また設定メトリクスごとにテンプレート化しておいくこ方が良い

SLO(サービスレベル目標)

SLIで計測されるサービスレベルのターゲット値あるいはターゲット値の範囲。 SLIに対する目標値。下限 <= SLI <= 上限となる。 本書ではシェークスピア検索の検索リクエストに対する平均レイテンシを100ms以下にするというSLOを紹介されている。 設定したSLOは常に満たし続けることを求めるのは現実的ではない。それを追い求める代償として過剰な保守、リリースのペースが落ちて顧客へ価値提供が遅延されるなどが起こりうる。
大切なのはエラーバジェットを設けて、消費しつずけなければリリースを行い顧客へ価値を届けることが大切。
またSLOの未達比率を示すエラーバジェットを日ごと、週ごとなどに追跡をした方がよく、SLOに関しても同様に観察を行なっていくこと。
SLOは完璧なものでなくて良い。それよりもSLOを定期的に見直して育てていく活動の方が大切。

SRE本読み返して1

こんにちは、季節はもう秋ですね。急に気温が下がって体調不良には気をつけたいです。 データベース設計のことをブログとしてまとめていましたが、僕自身SREエンジニアなので、原点に立ち帰って(というか経験はかなり浅い)SRE本を再度少しづつ読んで自分用にまとめていきたいと思います。

第一章

元来企業にはシスアド(Ops)が存在しており、基本的なサーバーの運用業務などはシスアドが行なっていた。 従来型の企業では開発者と運用担当者で線引きが(ガードレールが引かれていた)行われていた。 開発者はビジネスサイクルを回すために、機能を開発をして変更をリリースしたいが、運用担当者はリリースによる不具合のリスクを下げたい(システムの安定化)を行いたい。こいったことからDev vs Opsの構図が出来上がってしまった。 SREとは運用の側面をソフトウェアの力で負荷を軽減することを目指し定量的判断をもとそのサービスの信頼性を向上めざす言わばプラクティス、動機付けである。

第三章

第二章はgoogleシステムアーキテクトな話さのでスキップ。 リスクの受容という項目。サービスの100%の信頼性を提供するのは難しいし、現実的ではない。 どんなに優れたサービスアーキテクトで素晴らしいパフォーマンスでも、それ以前のネットワークレイヤーでパケロスをするかもしれないから100%のサービス稼働(信頼性)を行うことは不可能に近い。 それにシステムに100%の稼働といった信頼性を追い求めると、金銭的コストが増大する。 userにとってはネットワークレイヤーの不具合、提供するサービスでの不具合の原因内容の関心ごとは持っていない。それらはuserにとっては包括的にサービスの信頼性と考える。その為100%の信頼性の提供は現実ではない。 また100%の信頼性を追うことに集中すると新機能開発のなどのチャンスを失う可能性がある。よって信頼性とリスク許容はトレードoffなのだ。 信頼性の指標はどうやって表すのか、それはサービスの特性とuserが中心でなくてはならない。 サービスの特性、userが求めるものを加味した上で可用性やレイテンシに関する指標を作成する。 指標の定義は一旦は割愛する。 忘れてはいけないのはその指標を観察するにはuserやサービス特性を忘れてはいけない。なぜなら、userとしてはシステムのcpu使用率やメモリーの消費率は気にしていないというか関心ごとではないからだ。 第三章には明確には話が出てはないが、サービスの指標をSLI(service level indicator)という。そこから目標値(SLO)を定義し、現実のギャップをエラーバジェットとして管理し、信頼性向上の取り組み(パフォーマンス改善)や継続的リリースを推し進めていく。

終わりに

まだまだ続く。。

SLI /SLOの取り決めかた

こんにちは、季節はすっかり秋になってきましたね。 涼しくなり、非常にお酒が美味しい季節になってきました。のんべーにはたまりません。秋刀魚とビール最高です。

立ち話しはここまでにして、タイトル通りなんですが、自分自身SREエンジニアであるものの、経験が浅くSLI/SLOはどうやって取り決めるの??と考えていました。 サービースの信頼性の指標の為にSLI設定し、その指標をもとにSLOを定める。SLOの実際のギャップを埋める為に、エラーバジェットの概念がありますが、どうやってSLI/SLO決めたらいいのと考えてました。
答えは、SRE nextにありました!!!
動画は下記へ掲載しておきます。

さてさて、動画の内容を拝見してみると、前提にCUJが大切であると理解しました。

誰が信頼性があるサイトを決めるのか

システムのメトリクスがサイトの信頼性を取り決めるのか メトリクスではなく、そのサービスを利用するuserこそが信頼性があるサイトだと決めるのです。 なのでuserからすれば、そのサーバーのメトリクスなんて関心ごととして意識はないです。サクサク動いて使いやすいなーとかを抱くものです。 そいった観点から、userの行動を洗い出す必要があります。

どうやってCUJを洗い出す??

userがよく使う機能にフォーカスして、インフラコンポーネントレベルで洗い出すこと(関連性を考える)
コンポーネントが洗い出せたら、リクエストやapi単位でシーケンス図を書く。 これを行うことで、対処のコンポーネントを洗い出すことができると感じました。

SLIの取り決め方

CUJで割り出したえエンドポイントをもとにSLIを設定していく。
サービスの特性などにもよるが、今回は可用性とレイテンシーに関して考えていく。
SLIは良いイベントをもとに設定していく。

SLI:(良いイベント/有効なイベント) * 100%

//あるイベントのHTTPリクエストレスポンスの全体の件数のうち、全体のリクエスト成功数
SLI = 成功リクエストレスポンス/全体のリクエストレスポンス * 100%

上記をかみしてSLIを考えると下記になる

// /hogeはCUJで割り出したエンドポイント
ロードバランサーで測定したステータスが、
/hogeまたは /hgoe/profileに対する 
HTTP GETリクエストのうち、200番台、3xxまたは4xxを返す割合

// レイテンシー
ロードバランサで測定した、/hogeに対するHTTP GETリクエスト
のうちX msの範囲内にレスポンス全体を送信したものの割合

SLIが決まったらSLOを設定する

  • SLIをもとにどうやってuserの満足度が測れる?
    • 目標には目標値と測定期間が両方ないといけない。
    • 過去28日でリクエストの90% <500ms(レイテンシー)
  • ここで目標値を決めることは理解したが、やってはいけないことはエンジニアだけで取り決めてはだめ。プロダクトマネージャー、ビジネス側と一緒に決めること。
  • 明確に目標値が決めれない時は過去の平均レイテンシーなどを考慮して取り決める

エラーバジェット

  • 開発の舵取りを行える指標。SLOで作成した目標値から100%をもとに差し引きしたもの。例えば全体のリクエスト内、2xx台および3xx台, 4xx台の割合を99.99%に設定した際のエラーバジェットは0.01%になる。この0.01%の許容できる範囲を逸脱しなければ、開発を続けれるし、これを逸脱するばいいは改善作業をしていく取り組みをする。

終わりに

SLI/SLOの具体的な決め方などを知ることができました。SRE本を読んでいてわからなかったことが、この動画で補完できたと思います。

【SRE NEXT 2022】SLO決定のためのArt of SLO / 山口 能迪 www.youtube.com

ディスカッションをする大切さ

こんにちは、季節もすっかり秋になってきました。と思ったら急に暑くなったりと、体調管理が難しい気候だなと感じます。 引き続きデータモデリングの勉強をしているのですが、議論する重要さにいろいろ気づきました。 きっかけは一緒に勉強しているメンバーの子に楽々ERDレッスンという本を読んで、疑問をなげかけた時です。 「商品コードは主キーになりえないとのことなんだけど、どう思う??自分は主キーになり得るかなと考えているだけど、なぜなら商品が同じでも新たな商品とみなして、insertを積み上げていけば良いと思っているだけど??」 という投げかけに対して、「~のユースケースがあるから、自分は〜だと思う」という具合に議論が活発になり、モデルが洗礼されていくのを感じていました。 そして、商品という概念から、他のユースケースを考え、おのずと他のデータモデルの設計ができてきました。 一人で学習することは、確かに大切です。ですが、ディスカッションをすることでより洗礼する感覚があります。

下記は商品から派生して作成したテーブルです。(あくまでディスカッションしながら作成しているので完璧なものではないです。)

create table 商品(
  商品Id  interger primay key,
  商品コード varchar, 
  商品名    varchar,
  金額    integer
  ...
)

create table 商品履歴(
  履歴Id
  商品Id  interger primay key,
  商品コード varchar, 
  商品名    varchar,
  ...
)

create table 税率(
  税率Id PK
  税率
)

create table 注文(
  注文Id  PK
  利用者Id FK
  合計金額
)

create table 注文詳細(
  詳細Id
  注文Id FK
  商品Id FK
  税率Id FK
  商品価格
)

create table 利用者 (
  利用者Id  FK
  支払い方法Id FK
  profileId FK
)

create table 支払い方法(
  支払い方法Id
)

create table 現金払い(
  現金払いId PK
  支払い方法Id    FK
)

create table カード払い(
  カード払いId PK
  支払い方法Id     FK
)

create table カード詳細(
  カード詳細Id
  カード払いId FK
  カード番号
  カードブランド
  支払い形式
)

そして、slerの頃にどいうテーブル設計をしてたかというと、依頼主からの仕様をもとにエンジニアだけで作成していまいした。 確かに仕様書どうりに満たせるテーブルになっていたと思います。ですが大切なのは開発者とドメインエキスパートとの対話で生まれるドメインモデルの洗礼。 ここが重要なんじゃないかなと考えます。 モデルを洗礼することで、隠れている概念を抽出する作業をすることで、data storeレベルでの複雑さの軽減につながるのではないかなと考えた今日この頃でした〜 最後は長々となってしまいましたが、それではバイバイ〜

cloudflareにおけるmTLSの調査と検証

お久しぶりです。ながらく忙しくブログを書くことができませんでした。 実務ではEKSを触ったり、codepipeline周りを触りデプロイの仕組みを作成したりと触ったことのない領域を触る機会が出てきました。 本業では関係ないのですが、副業先で新規インフラ構築のタスクに当たっています。要件としてmTLSの要件がありました。 現行のシステムは現在root証明書をproxyで管理しているらしく、proxyでの管理を撤廃しどうにか awsのサービスでまかなえないかを模索していました。 調べたところ、 api gatewayを用いいるとmTLSの管理ができそうで、そちらを検討していました。検討の中、ググってみるとどうやらcloudflareでもmTLSの管理ができるということで検証してみることにしました。 実際に実装してみると簡単にmTLSの実装が出来ました。(ボタンをポチポチするだけ) 証明書の管理もcloudflare側で管理してくれるのでどうにかして、cloudflareでmTLSの管理を行いたいものです。

関心ごとの分離

テーブル設計について現在勉強してます! 今日は学習していて気がついた点を書いていこうと思います。

big table

みなさんは業務にあたっていてこんなテーブルに出会ったことはあるでしょうか? userテーブルで一般userと法人userが同じテーブルに表現されている。法人番号カラムを持っていて、要件として一般userは法人番号を保持しない。 ほんとは、法人番号カラムはnot null制約をかけたいが、一般userは法人番号を持たないという制約でnullableにせざるおえない。 一般userと法人userを識別するフラグカラムまたは、Idに特定の文字列を入れてuserの判別を行う。(法人→AD、一般->GE) これらの例だけではなく、様々なカラムが含まれ肥大化したuserテーブルを僕は扱ったことがあります。 このテーブルを見た時に僕は何が一般userに必要なデータなのか考えました。またそれはソースコードにも影響を与えコードを読む時間も増えました。 ここまでで、適切に正規化ができていなのでは??と考えられますし、その通りだと思います。ですが感じなことはuserという抽象的概念をちゃんと咀嚼して 適切に概念整理ができていますか?というところに立ち返りました。

概念整理と関心ごとの分離

ここまで概念整理が適切にできていないと考えられました。適切に考えると法人userと一般userは概念が違います。 なので適切な粒度としてリソース単位で分離することで、関心ごとの分離も行えます。 また適切な粒度で分離することでnullのデータが減ります。 そしてこれはclassの設計にも言えることですが、1つのclassでいろんなことをやらない、に精通しています。 脱神テーブル!!

終わりに

  • 一つのテーブルでいろいろ表現しようとしない。神テーブルを作らない。
  • 複数の物事が表現されそうになったら、隠れた概念が存在するのかを考える
  • 適切にリソースにテーブルを分けるように設計する