AWSの料金を「ざっくり」計算できるサイトを作りました

 
AWSの料金はややこしい。

サービスの選択肢が多く構成が柔軟なおかけで、さまざまな要件をカバーできるのは嬉しいのだけど、そのぶん料金体系がややこしいので、やるせない気持ちになります。

この気持ちはなんだろう、この気持ちはなんだろう、と自問しているうちに春になってしまったので、AWSの料金を「ざっくり」計算できるサイトを作り始めました。

all.gif

ざっくりAWS

公式ツールの存在

Simple Monthly CalculatorというAWSの料金を計算できる公式のツールがあるのですが、悲しいことに名前ほどシンプルではありません。

正確な料金を算出するために入力項目が多いのは仕方がないとは思うのですが、サイトを開いたときの威圧感がすごいので、もう少しさっぱりできないかという気持ちがありました。

なので、公式ツールの敷居が高いと感じる自分のような人向けに、料金を「ざっくり」計算できるサイトを作ります。

目指す方向

AWSの料金を、日本円でざっくり。

正確さよりも、入力項目を少なくする事を優先します。当初は、ドルと円を併記していたのですが、シンプルさが欠けていたので円のみにしました。

また、モバイルファーストなご時世ですが、AWSの料金を計算したくなるのは、PCを使っている時のほうが多い気がするので、PCメインで作ります。公開後にスマホのアクセスが多かったら、スマホの方も頑張ります。

フロントエンド環境

Vue.js
普段はJavaScriptよりもHTMLやCSSを書いている事が多いので、ざっくり書けそうなVue.jsを選びました。

Jest
テストツールも色々あるようですが、簡単に導入できそうなJestを選びました。

Webpack
環境構築にこだわり始めると一日が潰れてしまうので、手短に最小限のWebpack設定を書きました。

https://github.com/noplan1989/aws-rough/blob/master/webpack.common.js

ディレクトリ構成

src/
  ├ ___tests___/ # テスト
  ├ api/         # API
  ├ assets/      # アセット
  │  ├ scss/     # SCSS
  │  └ svg/      # SVG
  ├ components   # コンポーネント
  ├ config       # サービスの設定ファイル
  ├ router       # ルーティング
  ├ store        # 状態とその変更に絡む関数
  └ main.js      # エントリーポイント

ディレクトリ構成は、Vuexの構成を参考にしています。エントリーポイントで、App.vueをマウントして、あとはルーティングやら何やらをよしなにやる、サンプルコードでよく見る構成です。

CSSはコンポーネントに含めず、別ファイルにしています。

アプリケーションの構造 | Vuex

実装の流れ

見た目を作る
コンポーネントを調整しながら見た目を固めていきます。

状態管理を考える
アプリケーションに必要な状態と、その変更方法を考えて実装します。

計算を実装する
サービスごとに料金の計算を実装していきます。

少し豪華にする
見た目と機能をができて実際に使ってみると、構築前には見えていなかった足りないものが出てきたので、追加で実装しました。

見た目を作る

ざっくり組み立てる

まずはサイトの見た目がないと完成形の実感がわかないので、ざっくりとHTMLとCSSのコーディングをしていきます。

かなり適当ですが、HTMLをひとつのコンポーネントにぶち込んで、CSSで見た目を確認しながら整えました。あとでいくらでも調整できるので、サイトとしての体をなすぐらいまで持っていきます。

コンポーネントを分割する(過ち)

つぎに、ひとつのコンポーネントにぶち込んだHTMLを、細かいコンポーネントに分割していったのですが、そもそもこのやり方がおかしいとことに気がつく。

まずはコンポーネントを設計して、それを組み合わせて構築していくのがコンポーネントの在り方だろうと。

しかしやり直したいという気持ちを、面倒くさいという本能が圧倒していたので、気持ちをごまかして先へ進みます。

残念なコンポーネントの紹介

言葉だけでは、HTMLをバラしただけの残念な感じが伝わりにくいので、主要なコンポーネントに説明を添えてお伝えします。そのまま貼るとボリュームがあるので、添付のコードはかなり省略しています。

メニュー

メニューはサービス名がズラッと並びます。 <router-link> はvue-routerで用意されているコンポーネントで、 to プロパティにリンク先のルートを設定します(今回はサービス名)。アクティブなリンクにクラスを付与してくれるので、アクティブなものだけ色を変えるみたいなのが簡単にできます。

router-link | vue-router

components/layout/LayoutMenuPC.vue
<template>
  <div class="menu">
    <div class="menu-inside">
      <h1 class="menu-logo">
        <router-link to="about"><IconLogo /></router-link>
      </h1>
      <nav class="menu-nav">
        <ul class="menu-list">
          <li v-for="service in services" :key="service.key" class="menu-item">
            <router-link :to="service.key">{{ service.name }}</router-link>
          </li>
        </ul>
      </nav>
    </div>
  </div>
</template>

<script>
import serviceConfig from '@/config/service'
import IconLogo from '@/assets/svg/logo.svg'

export default {
  name: 'LayoutMenuPC',
  components: { Logo },
  data() {
    return {
      // こんな感じになってます
      // [
      //   {
      //     key: 'ec2',
      //     name: 'EC2',
      //     ...
      //   },
      //   ...
      // ]
      services: serviceConfig
    }
  }
}
</script>

各サービスの共通部分

一番上のタイトルとテーブルの部分は共通なので、それらを表示するテンプレート的なファイルを作成しました。サービスごとに違う部分は、スロットによるコンテンツ配信という機能で、コンポーネント使用時に差し込むことができます。

このコードの例では、下の方にある <slot /> の部分が置換されます。今回はひとつしかないので、単一スロットを使用していますが、複数箇所に使用したい場合には、名前付きスロットで対応できます。便利。

components/service/template/ServiceTemplate.vue
<template>
  <article class="service">
    <ServiceTemplateTitle :service="service" />
    <div class="service-calc">
      <table class="table">
        <thead>
          <tr>
            <th
              v-for="column in service.table"
              :key="`header-${column.key}`"
              :class="[{[`mod-${column.mod}`]: column.mod}]"
            >{{ column.title }}</th>
            <th class="mod-price">月額</th>
          </tr>
        </thead>
        <tbody>
          <ServiceTemplateRow
            v-for="(row, rowIndex) in table"
            :key="rowIndex"
            :row="row"
            :row-index="rowIndex"
            :labels="tableLabels"
            :service="service"
          />
        </tbody>
      </table>
    </div>

    <slot /> <!-- ここに差分のコンテンツが入る -->
  </article>
</template>

<script>
import ...

export default {
  components: { ServiceTemplateRow, ServiceTemplateTitle },
  props: {
    serviceName: {
      type: String,
      required: true
    }
  },
  computed: {
    service() {
      const service = getService(this.serviceName, serviceConfig)
      // ごにょごにょ整形して返す

      return service
    }
  }
}
</script>

各サービスの差分

<ServiceTemplate> にサービス名を渡すと、設定ファイルをもとにタイトルやテーブルが描画されます。 <ServiceTemplate> の子要素にサービスごとに違う入力項目の補足などを書くと、先ほどの <slot /> の部分に差し込まれます。

components/service/ServiceEC2.vue
<template>
  <ServiceTemplate service-name="ec2">
    <section class="section">
      <h2 class="title">概要と料金</h2>
      <p class="text">スペックや台数が柔軟に変更できる仮想サーバーです。</p>
    </section>
    <!-- 説明が続く -->
  </ServiceTemplate>
</template>

<script>
export default {
  name: 'ServiceEC2',
  components: { ServiceTemplate }
}
</script>

テーブルの行

テーブルの行には、入力エリアや価格の合計が入ります。

components/service/template/ServiceTemplateRow.vue
<template>
  <tr>
    <td
      v-for="(column, columnIndex) in service.table"
      :key="column.key"
    >
      <ServiceFormNumber
        v-if="column.type === 'number'"
        :service-key="service.key"
        :index="rowIndex"
        :column-key="column.key"
        :value="row[column.key]"
        :error="error"
      />

      <ServiceFormSelect
        v-if="column.type === 'select'"
        :service-key="service.key"
        :index="rowIndex"
        :column-key="column.key"
        :value="row[column.key]"
        :options="column.options"
      />
    </td>
    <td>
      <ServicePartsPrice :price="row.total.jpy" />
    </td>
  </tr>
</template>

<script>
export default {
  components: { ServiceFormNumber, ServiceFormSelect, ServicePartsPrice }
}
</script>

数値の入力

数値の入力なので type="number" を使用したのですが、ブラウザによって文字列を受け付けたり、指数表記がいけたり、イベントが発火されなかったりバラバラでつらい。数値入力を完全に制御しようと思ったら、 type="text" にしないと厳しそうなのだけど、スマホでは type="number" の方がいいので、ブラウザ対応に時間がかかりそう。そのうちやる。

明らかに大きすぎる値が入力された場合は、悪意があるとみなして振り出しに戻すようにしています。

<template>
  <div class="form-text">
    <input
      type="number"
      :value="value"
      class="form-text-input"
      @input="e => update({ serviceKey, index, columnKey, value: e.target.value })"
    />
  </div>
</template>

<script>
export default {
  methods: {
    update(params) {
      const limitedValue = params.value < MAX_INPUT ? params.value : ''

      store.update({ ...params, value: limitedValue })
    }
  }
}
</script>

価格の表示

sushi.gif

価格の部分は円をカンマ区切りで表示するだけなのですが、現実的でない料金になった場合は寿司をお見舞いする、というお花畑な実装を思いついたのでPriceコンポーネントに実装しました。

<template>
  <p class="price">
    <span class="price-number">{{ yen }}</span>
    <span class="price-unit"></span>
  </p>
</template>

<script>
export default {
  computed: {
    yen() {
      return formatPrice(this.price)
    }
  }
}
</script>
store/price.js
export const greaterThanMaxPrice = num => num > MAX_PRICE

export const addComma = num => {
  return Math.floor(num)
    .toString()
    .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

export const formatPrice = price => {
  if (greaterThanMaxPrice(price)) {
    return SUSHI
  }

  return addComma(price)
}
store/constants.js
export const SUSHI = '🍣🍣🍣'
export const MAX_PRICE = Math.pow(10, 12)
___tests__/store/price.js
describe('formatPrice', () => {
  test('上限金額を越えたら寿司を返す', () => {
    expect(formatPrice(MAX_PRICE + 1)).toBe(SUSHI)
  })
})

ルーティング

ルートの定義

EC2やRDSなどのサービスごとにページを作成するので、もろもろのルートを定義します。ルートは、パスとコンポーネントを並べるだけなのでとてもシンプルです。ついでに、URLをHistoryモードに設定しました。

スクロール位置の制御にも対応している事に感動しました。

HTML5 History モード | vue-router
スクロールの振る舞い

router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import ServiceEC2 from '@/components/service/ServiceEC2'

Vue.use(Router)

const pagetitle = str => `${str} | ざっくりAWS`

const router = new Router({
  mode: 'history',              // デフォルトはhash
  linkActiveClass: 'is-active', // アクティブなリンクのクラス名
  linkExactActiveClass: 'is-active-exactly',
  scrollBehavior (to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { x: 0, y: 0 }
    }
  },
  routes: [
    {
      path: '/',
      redirect: '/ec2'
    },
    {
      path: '/ec2',
      name: 'ec2',
      component: ServiceEC2,
      meta: {
        title: 'EC2'
      }
    },
    ... // 他のサービスも
  ]
})

router.beforeEach((to, from, next) => {
  if (to.meta && to.meta.title) {
    document.title = pagetitle(to.meta.title)
  }

  next()
})

export default router

ルーターの埋め込み

メニューやフッターは全ページ共通なので、切り替わるコンテンツ部分に <router-view> のタグを埋め込みます。

router-view | vue-router

components/App.vue
<template>
  <div class="app">
    <transition name="anime-fade-in">
      <div v-if="state.isLoaded" class="container">
        <LayoutMenu />
        <div class="contents">
          <router-view />
          <LayoutFooter />
        </div>
        <CartIndex :total="state.total.jpy" />
      </div>
    </transition>

    <ErrorModal :error="state.error.fetch" />
  </div>
</template>

<script>
export default {

}
</script>

適当なCSS

assets/scss/layout/_footer.scss
.footer {
  &-list {
    //
  }
  &-item {
    //
  }
}

コンポーネントがうまくできていないので、 scoped は諦めました。

小さいサイトなので、できる限り何も考えずに書きたいのですが、さすがにルールがないとクラス名が重複してしまいます。なので、ざっくりとブロックごとにファイルを作成して、適当にハイフンでつなぐだけにしました。

ファイル名をブロック名にするルールさえあれば、ファイルごとにスコープを切れるので、安心してスタイルを追加できます。

という感じで行けると思ったのですが、デザインがなかったため、ブラウザ上で見た目を調整しながら色々と迷走した結果、CSSが破綻した。

状態管理を考える

Vuexは必要か

いつ、Vuexを使うべきでしょうか?

もし、あなたが大規模なSPAを構築することなく、Vuex を導入した場合、冗長で恐ろしいと感じるかもしれません。そう感じることは全く普通です。あなたのアプリがシンプルであれば、Vuex なしで問題ないでしょう。

Vuexとは何か?

大規模なSPAではないので、必要はなさそうですが、使う場合と使わない場合について考えました。

Vuexを使う場合

やることが増えるけど、構造が明確になる。

Vuexを使わない場合

シンプルだけど、規模が大きくなるとツラい。

使わないことにする

Vuexを使わなくても管理できる規模なので、使わないことにしました。

storeパターン

Vuexを使わないとはいえ、状態管理には最小限の決まりごとが欲しいので、公式のガイドにあったstoreパターンを適用することにしました。パターンとはいってもそんなに大袈裟なものではなく、ひとつのオブジェクトに状態と、その変更を行うメソッドをぶち込むだけです。

store/index.js
export default {
  state: {
    tables: {},
    price: {},
    fx: {},
    isLoaded: false,
    isOpen: false,
    error: {}
  },
  setInitialTables () {},
  fetchAll () {},
  append () {},
  update () {},
  updateTotal() {},
  remove () {}
}

メソッドで this.state.isLoaded = true のよう状態を変更すると、リアクティブに反映されます。

書いているうちに、これVuexで良くない?という気がしてきたのですが、途中から切り替えるのが面倒くさかったので、そのまま走りました。

テーブルの状態

このサイトのメインになるのが、数量を入力するテーブルなので、その状態を中心に考えました。サービスごとに必要な入力項目と現在値、合計をまとめて管理しています。EC2やRDSのインスタンスは複数個ある可能性があるので、配列にポコポコ追加できるようにしました。だいたいこんな感じです。

tables: {
  ec2: [
    { instance: 't2.nano', unit: 1, total: 5 },
    { instance: 't2.micro', unit: 3, total: 30 },
  ],
  rds: [
    { instance: 'db.t2.micro', unit: null, tiotal: 0 }
  ]
}

テーブルの行を追加する

行の追加は、配列にデフォルト値が入ったテーブルを追加します。

store/index.j.s
append ({ serviceKey }, serviceConfig) {
  this.state.tables[serviceKey].push(
    getDefaultTable(serviceKey, serviceConfig)
  )
}

テーブルの値を更新する

追加に比べると少々ややこしいですが、入力された値を上書きして、料金を再計算しています。配列の値を直接変更してもリアクティブに反映されないので、Vueで用意されている splice という変更メソッドを使いました。使い方は Array.prototype.splice() と同じです。 calc[serviceKey] のところで行っている料金の計算については後で書きます。

引数が汚い・・・

store/index.j.s
update ({ serviceKey, index, columnKey, value }) {
  const row = {
    ...this.state.tables[serviceKey][index],
    [columnKey]: value
  }
  const usd = calc[serviceKey](row, this.state.price)
  const jpy = usdToXXX(usd, this.state.fx.usdjpy)

  this.state.tables[serviceKey].splice(index, 1, {
    ...row,
    total: { usd, jpy }
  })

  this.updateTotal()
}

変更メソッド | リストレンダリング

テーブルの行を削除する

削除は splice するだけなのでシンプルです。1行しかないときに削除された場合は、行を削除するのではなく値をリセットするようにしています。

store/index.j.s
remove ({ serviceKey, index }, serviceConfig) {
  if (this.state.tables[serviceKey].length === 1) {
    this.state.tables[serviceKey].splice(index, 1, getDefaultTable(serviceKey, serviceConfig))
  } else {
    this.state.tables[serviceKey].splice(index, 1)
  }

  this.updateTotal()
}

計算を実装する

いよいよ料金の計算をしていきます。インスタンスの計算はただの掛け算なので楽でしたが、転送料などが少しややこしかったので、想定していたよりも時間がかかりました。

価格と為替

AWSの価格はPrice List APIで取得できます。WebサイトへアクセスするたびにAPIを叩くのは無駄が多いので、事前に取得して整形したJSONをS3へ保存してあります。ついでにドル円のレートも取得してます。詳しくは別の記事に書いているので、興味のある方はそちらでご確認ください。

Node.jsでPrice List APIからAWSのサービス利用料を取得して、整形したものをS3に保存する

price.json
{
  "ec2": {
    "instance": [
      {
        "price": 0.0076,
        "attributes": {
          "memory": "0.5 GiB",
          "vcpu": "1",
          "capacitystatus": "Used",
          "instanceType": "t2.nano",
          ...
        }
      },
      ...
    ]
  },
  "transfer": {
    "out": {
      "priceRange": [
        {
          "beginRange": 0,
          "endRange": 1,
          "price": 0
        },
        {
          "beginRange": 1,
          "endRange": 10240,
          "price": 0.14
        },
        {
          "beginRange": 10240,
          "endRange": 51200,
          "price": 0.135
        },
        {
          "beginRange": 51200,
          "endRange": 153600,
          "price": 0.13
        },
        {
          "beginRange": 153600,
          "endRange": null,
          "price": 0.12
        }
      ]
    }
  },
  ...
}
fx.json
{
  "usdjpy": 100
}

EC2の料金計算に必要な項目

EC2の計算に必要な項目は主に、インスタンスの種類とその個数、データ転送量、ストレージの料金になります。

インスタンス

インスタンスを選ぶ際には、リザーブドインスタンスやスポットインスタンスのような選択肢もありますが、「ざっくり」計算するために選択肢を増やしたくないので、 オンデマンドインスタンス に限定しています。

また、インスタンスの種類も用途に応じて「コンピューティング最適化」や「GPU インスタンス」など様々なものが用意されていますが、最もよく使われていそうな t2m4 などの 汎用のインスタンス に絞っています。

OSは Linux です。

データ転送量

データの転送に関しても、インターネットのIN/OUT、リージョン間のIN/OUT、異なるVPCでのIN/OUTなど挙げればキリがないですが、圧倒的に割合が大きいのが インターネットのOUT なので、その一点に絞りました。

ストレージ

ストレージに使用するEBSのボリュームも色々種類がありますが、デフォルトで選択されている 汎用SSD(gp2) を前提にしました。

その他はスルー

その他にも、使用していないElastic IPにも料金がかかったりしますが、影響が少なそうな項目はスルーしました。

結果的に、入力項目を「インスタンス」「個数」「ストレージ(GB)」「データ転送量(GB)」の4つまで絞ることができました。

計算の実装

EC2の料金

EC2を例に計算の実装を。計算のコードだけでは実際の挙動が伝わりにくいので、テストコードも併記しています。

store/calc/ec2.js
export default (row, priceList) => {
  const unit = toNumber(row.unit)
  const storage = toNumber(row.storage)
  const transfer = toNumber(row.transfer)

  let total = 0

  if (row.instance && unit) {
    const instance = parseInstance(row.instance, priceList.ec2.instance)

    total += instance.price * unit * MONTHLY_HOURS
  }

  if (storage) {
    total += priceList.ebs.gp2.price * storage * (unit ? unit : 1)
  }

  if (transfer) {
    total += reduceRange(transfer, priceList.transfer.out.priceRange)
  }

  return total
}
__tests__/store/calc/ec2.js
describe('ec2', () => {
  test('EC2の料金を計算できる', () => {
    const priceList = {} // ここに価格

    const row = {
      instance: 't2.nano',
      unit: 2,
      storage: 30,
      transfer: 1000
    }

    const instance = 0.01 * 2 * MONTHLY_HOURS
    const storage = 0.1 * 30 * 2
    const transfer = 10 * 10 + 90 * 9 + 900 * 8
    const expected = instance + storage + transfer

    expect(ec2(row, priceList)).toBe(expected)
  })
})

使うほど安くなるもの

データ転送料やS3のストレージなどは、使えば使うほど安くなる料金体系になっています。EC2のデータ転送量は下の表のような感じで安くなっていきます。

転送料 価格
最初の 1 GB/月 $0.000/GB
10 TB まで/月 $0.140/GB
次の 40 TB/月 $0.135/GB
... ...

https://aws.amazon.com/jp/ec2/pricing/on-demand/

泥臭い。

store/price.js
export const reduceRange = (val, ranges) =>
  ranges.reduce((total, range) => {
    // 範囲より小さい
    if (val < range.beginRange) {
      return total
    }

    const diff = (() => {
      // 上限なし
      if (!range.endRange) {
        return val - range.beginRange
      }

      // 範囲より大きい
      if (val > range.endRange) {
        return range.endRange - range.beginRange
      }

      // 範囲内
      return val - range.beginRange
    })()

    return total + diff * range.price
  }, 0)
__tests__/store/price.js
  describe('reduceRange', () => {
    test('料金のレンジを計算できる', () => {
      const ranges = [
        {
          beginRange: 0,
          endRange: 10,
          price: 10
        },
        {
          beginRange: 10,
          endRange: 100,
          price: 9
        },
        {
          beginRange: 100,
          endRange: null,
          price: 8
        }
      ]

      const r0 = 10 * 10
      const r1 = (100 - 10) * 9
      const r2 = (1000 - 100) * 8
      const expected = r0 + r1 + r2

      expect(reduceRange(1000, ranges)).toBe(expected)
    })
  })

少し豪華にする

料金内訳とグラフの表示

合計は常に表示しているのですが、各サービスの合計はそれぞれのページに行かないと見られないので、料金内訳のページを追加しました。一覧を出すだけでは虚しさがあったので、円グラフも追加しています。

vue-chart.js

Chart.jsのVue向けのラッパーですごく使いやすかった。デフォルトのままでもカッコイイのですが、アニメーションやグラフの色を若干調整しました。

vue-chartjs

color

グラフの色は、視覚的にわかりやすいようにサービスのアイコン色にしようと思ったのですが、同じ色が隣り合ったときに境界がわかりにくくになってしまって詰まりました。Sassのlighten()みたいに色を明るくできる関数はないかな〜と思って探したらありました。colorという名前のパッケージが背負う責任は重いなぁ。。

Color.rgb([245, 133, 54]).lighten(0.1).string()

こんな感じで手軽に明るくできるので、色が重複したら明るくなるように力技で実装しました。

clipboard.js

料金内訳ページを見る人の中には、料金をコピペしたい人がそれなりにいると思うので、コピーのボタンも追加しました。自前で実装するとブラウザ対応などが面倒だった記憶があるので、ライブラリを使いました。

clipboard.js

ロゴのようなものを作る

今一度サイトを見直してみると、なんだか物足りない気がしたのでロゴのようなものを作ることに。CSSだけで作っても良いかなと思ったのですが、環境によって表示が変わるのは残念なので、SVGで作ることにしました。

ロゴといってもIllustratorは使えないので、Photoshopのテキストと長方形のみを使用しました(他に使えない)。そのままSVGで書き出すとテキスト要素として書き出されてしまうため、テキストをシェイプに変換してpath要素で書き出します。ついでにOG画像も作りました。

logo.png

公開

長くなりましたが完成したので、公開作業に入ります。

S3のバケットにコピー

AWS CLIでバケットにHTMLとバンドルされたJSをコピーします。HTMLはキャッシュして欲しくなくて、JSは長いことキャッシュして欲しいので1年に設定しました。Webpackのhtml-webpack-pluginを使うと、更新したときにJSのハッシュを更新してくれるので、勝手に新しいJSを読みにいってくれます。

webpack.common.js
module.exports = {
  plugins: [
    new HtmlWebpackPlugin({
      hash: true,
      template: 'index.html',
      minify: {
        collapseWhitespace: true,
        html5: true,
        minifyCSS: true,
        removeComments: true,
        removeEmptyAttributes: true
      }
    })
  ]
}

CloudFrontの設定

S3でそのまま公開でもよかったのですが、SSLの証明書を取得できたり、gzip圧縮を手軽に設定できたり、他にもメリットが多かったので、CloudFrontを使いました。

デプロイスクリプトを書く

本来はここでCIというものでテスト→デプロイするのが当たり前のようなのですが、使ったことがなく、学ぶ気力が残っていなかったので、デプロイ用のスクリプトを書きました。普段はシェルスクリプトをまったく書かないので、これはこれで不安。

deploy.sh
#!/bin/sh

export $(cat .env | xargs)

npm run test

if [ $? -gt 0 ] ;then
  echo "**** oops... ****"
  exit 1
fi

npm run build

aws s3 cp ./dist/index.html s3://aws.noplan.cc/index.html --cache-control no-store
aws s3 cp ./dist/bundle.js s3://aws.noplan.cc/bundle.js --cache-control max-age=31536000
aws cloudfront create-invalidation --distribution-id $CF_DIST_ID --paths / /index.html
.env
CF_DIST_ID=HOGEHOGE

終わった

all.gif

ざっくりAWS

※ PC推奨です

振り返る

コンポーネント設計は大事

今回は、勢いでHTMLを組んでそれをコンポーネントに分割するという残念な形にしまったので、今度なにかを作るときは、綺麗にコンポーネントを設計したい。ついでに scoped なCSSも書けたら幸せになれるかもしれない。

テストが気持ちよかった

今回はじめてテストを書いたのですが、ブラウザを見ずにターミナルでテストを繰り返して開発していくのが新鮮で面白かった。ザーッとテストが流れていくのを眺めることが、こんなにも気持ちいいものとは。

最初は書き方を学びながらで時間がかかったのですが、修正が簡単になったので結果的には実装時間を短縮できました。と書きたかったのだけど、実際は単純に倍ぐらいの時間がかかりました。今後の機能追加などの際にテストが効いてくることを祈りたい。

最初はテストケースを英語で書いていたのですが、テストを書くよりも英文を考えるのに時間がかかるという珍妙な状態になっていたので、開き直って日本語にしたら開発速度が上がった。自分しか見ないからいいか、と思いつつもテストケースを迷わず書けるぐらいの英語力は欲しい。

細かいケースは全然テストできていないので、もう少しスキルと体力も欲しい。思考回路がショート寸前だったため、コンポーネントのテストは諦めました。

この気持ちはなんだろう。

公式のツールが意外と使いやすかった

過去のトラウマで、公式のツールに対する悪い印象が増幅されていただけで、いま使ってみたらふつうに使いやすかった。計算に慣れたからかもしれないけど、なんだかなぁ。とりあえず冒頭で悪く言ってしまったことをAWSの人に謝りたい。本当に感謝してます。

サーバーレスが想像以上に安かった

フロントエンドはCloudFront+S3で、価格を取得するバッチ処理はLambdaでやっているので、全体的にサーバーレスな構成になった。アクセスがない場合は、S3のストレージとLambdaからの書き込みにしかお金がかからないので、1円未満。震える。

「ざっくり」のバランスが難しい

入力項目を減らして手軽に料金を出すことを目的に作ったのですが、その代償として対象外になるものが多くなってしまいました。EC2を例にとっても、

  • オンデマンドインスタンスに限定
  • AMIはAmazon Linux
  • インスタンスタイプは「汎用」のものだけ
  • EBSは汎用SSDに限定
  • リージョン間のデータ転送などは無視
  • Elastic IPの料金は無視

これだけの制約があるので、なんだこのサイト使えねぇじゃねえかと思う人がかなり多そう。制約がたくさんあるのを見ると使う気が失せるだろうし、制約を書かないのは不誠実だと思うので、そこのところを突き詰めていくと、結局公式のツールに行き着く。なんだかなぁ。

この気持ちはなんだろう

このサイトを作ろうと思う前に、AWSの請求を見て、今月は意外と高いなぁと思い内訳を見ているときに、なぜか「この気持ちはなんだろう」というフレーズが頭の中で再生されました。

合唱曲としておなじみの「春に」の歌いだしなのですが、冒頭からこの言葉を繰り返すのが印象的なので、記憶にある人もいるかもしれません。気になったので改めて聞いてみたところ、いくつか印象深いフレーズがありました。

https://www.youtube.com/results?search_query=%E6%98%A5%E3%81%AB+%E5%90%88%E5%94%B1

あしたとあさってが一度にくるといい

まだ会ったことのないすべての人と
会ってみたい話してみたい
あしたとあさってが一度にくるといい
ぼくはもどかしい

最も衝撃を受けたのがこの一節。明日と明後日が一度に来て欲しいと思うほどの好奇心。早く大人になりたいというニュアンスもあるのかもしれないけれど、そんな前のめりな気持ちは失って久しい。というより過去にも持っていなかった。明日が来て欲しくないと思うことは度々あれど、明後日を願ったことはなかったので、その気持ちを味わってみたい。

そのくせこの草の上でじっとしていたい

地平線のかなたへと歩きつづけたい
そのくせこの草の上でじっとしていたい
大声でだれかを呼びたい
そのくせひとりで黙っていたい

「動」と「静」の対比として、そのくせこの草の上でじっとしていたいという言葉を使っていると思うのですが、それはそれでエクストリームな気がして頭の中のイメージがブレてしまったのだけど、印象に残るフレーズですごくいい。

この気持ちはなんだったのか

結局のところ、この気持ちは〇〇だとは言い切れない感情を歌っているので、一言で表そうとするのは無粋でした。ひとつだけ確かなのは、「この気持ちはなんだろう」という言葉を中高生が全力で合唱するから響くのであって、いい年してこんな所に書いても残念に映るだけだということです。この気持ちはなんだろう。

今後やること

ある程度サイトにアクセスがあったら、もう少し対応するサービスを増やしたい。あと、実装がめんどくさかったためにRDSのデータベースがMySQL固定になっていたり、ELBをClassicに限定していたりするので、早めにどうにかしたい。

ただ需要がない気もするので、1年ぐらい放置してアクセスがなかったら、人知れず404になっていると思います。

ざっくりAWS

コード

https://github.com/noplan1989/aws-rough

参考

詩「春に」の音読授業をデザインする