aws-ecs-pattern の ApplicationBalancedFargetService でのコスト削減法
CDKで @aws-cdk/aws-esc-pattern を使ってサクッとFagateで安くコンテナ建てたかったのですが、
import * as ecr from '@aws-cdk/aws-ecr'; import * as ecsPatterns from '@aws-cdk/aws-ecs-patterns'; import * as ecs from '@aws-cdk/aws-ecs'; const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Fargate', { taskImageOptions: { image: ecs.ContainerImage.fromEcrRepository(ecr.Repository.fromRepositoryName(this, 'Repository', 'my_repository')) }, cpu: 512, memoryLimitMiB: 1024, });
とかやると、プライベートサブネットでコンテナが建ちます。 プライベートサブネットは自動的にNATゲートウェイを伴い、生きている間じゅう時間課金が発生して個人プロジェクトではつらいので、プライベートサブネットを作らない方法を検討していました。
assignPublicIP を true
にすると、
taskSubnets のデフォルトが変わると記載がある
Type: SubnetSelection (optional, default: Public subnets if assignPublicIp is set, otherwise the first available one of Private, Isolated, Public, in that order.)
ので、 true
にしてデプロイしてみたところ、空のプライベートサブネットができてしまいました。
NATゲートウェイを作成しない正解は、VPCを直接生成することのようです。
import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecr from '@aws-cdk/aws-ecr'; import * as ecs from '@aws-cdk/aws-ecs'; import * as ecsPatterns from '@aws-cdk/aws-ecs-patterns'; const vpc = new ec2.Vpc(this, 'VPC', { natGateways: 0, subnetConfiguration: [ { cidrMask: 24, subnetType: ec2.SubnetType.PUBLIC, name: 'ingress' } ] }); const service = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'Fargate', { taskImageOptions: { image: ecs.ContainerImage.fromEcrRepository(ecr.Repository.fromRepositoryName(this, 'Repository', 'asset_tracker')) }, cpu: 512, memoryLimitMiB: 1024, assignPublicIp: true, vpc });
このように vpc
を直接していすると、そのコンストラクタで natGateways
の数を直接指定できます。
この場合、PUBLICなサブネットと、ISOLATEDなサブネット(外との通信不可; プライベートサブネットでは中から外への通信はOK) ができますが、
ISOLATEDなサブネットの方には現状何も置いていないので、 subnetConfiguration
を指定して作成しないようにしました。
期間を指定するActiveRecordのクエリ
開始日と終了日がDateで与えられて「 開始日以降終了日まで」みたいなクエリをActiveRecordで書く時、
Event.where("date BETWEEN ? AND ?", start_date, end_date)
とかやるのだけれど、JOINした時などテーブル名が曖昧になって通らなくなったりするので、 ハッシュで書いてテーブル名の修飾をRailsに任せたい。
こういう場合はRangeが使えて、
Event.where(date: start_date...end_date)
とかできる。で、start_date
やend_date
がnil
の時には不等号で評価してほしいんだけど、
上2つの書き方だとどちらも動かない。
range_handler.rbを見る感じだと、Rangeの端の値がinfinity?
に対してtrue
を返せばよさそう。
ruby - Is there a way to express 'infinite time'? - Stack Overflowによると、DateTime::Infinity.new
で無限大の未来を表せるらしい。このクラスのドキュメントさがせなかったけど。
問題は無限大の開始日が指定されなかった時に代わりに使う無限大の過去をどう表すかで、DateTime::Infinity.new(-1)
はRangeに使うとエラーになってしまう。
[1] pry(main)> t = DateTime::Infinity.new(-1) => #<Date::Infinity:0x00007f852411a9d0 ...> [2] pry(main)> t...Date.today ArgumentError: bad value for range
とりあえずinfinity?
に対応できればよさそうなので、特異メソッドで対応してみた。
MIN_DATE = Date.new(2000, 1, 1) #ダミーなので適当に class << MIN_DATE def infinite? true end end start_date ||= MIN_DATE end_date ||= DateTime::INFINITY.new Event.where(date: start_date...end_date)
infinity?
でなく infinite?
なのは、begin_bind
を作るときに
query_attribute.rb
でラップされるから(?)。
github.com このコミット見る感じ、PostgreSQL以外では通用しないのかな?
Ruby 2.5, Rails 5.2, PostgreSQL 10.5 で 開始日、終了日どちらか or どちらも指定した場合ともに動きました。
CRA + TypeScript での storybook の設定
create-react-app で TypeScriptをサポートしているのいいんだけど、その時のstorybookの設定がよく分からなかった。
バーション
- react-scripts@2.1.0
- typescript@3.1.6
- @storybook/react@4.0.0
Storybookの公式に色々書いてあって、
awesome-typescript-loader
とかを書いてある通りに設定したら動きはしたんだけど、
結局create-react-app本体の設定コード読んだほうがビルドしたものと一緒の設定になっていいかなと思い次のようにしてみました。
const TSDocgenPlugin = require('react-docgen-typescript-webpack-plugin'); module.exports = (baseConfig, env, config) => { config.module.rules.push( { test: /\.mjs$/, include: /node_modules/, type: 'javascript/auto', }, { test: /\.(ts|tsx)$/, use: [ { loader: 'babel-loader', options: { presets: ['react-app'], }, }, ], }, ); config.plugins.push(new TSDocgenPlugin()); config.resolve.extensions.push('.mjs', '.ts', '.tsx'); return config; };
結果として設定も簡単になったと思います。 .mjsの設定はCan't reexport the named export '....' from non EcmaScript module というエラーへの対応です。
こういう風にあからさまに babel-preset-react-app
に依存した時、devDependenciesに足したくなるけどJavaScriptの場合どうするのがいいんだろう?
react-scripts
内部の依存を直接使うのは良くない気がするけど、足しちゃうとreact-scripts
だけバージョンあげてバージョンが合わなくなったりしそう。
apollo-boostではlinkを設定できない
graphql-ruby と react + apollo ではじめてのGraphQLはじめました。
で 、graphql-ruby (Rails) はAPIサーバとして使っていて、Authorizationヘッダで認証するようにしています。
もともとはこんな感じでヘッダを設定していたのだけれど、
import ApolloClient from 'apollo-boost'; import { ApolloProvider } from 'react-apollo'; const client = new ApolloClient({ uri: 'xxxxxxx', request: operation => { operation.setContext({ headers: { 'Authorization': 'xxxxxxxx' }, }); }, }); ReactDOM.render( <Router> <ApolloProvider client={client}> <App /> </ApolloProvider> </Router>, document.getElementById('root'), );
画像ファイルを上げたいということになり、apollo-upload-client でApolloClient
にlinkオプションを渡そうとしたけどうまくいかない。
from 'apollo-boost'
な ApolloClient
は下記のオプションしか取らず、そこにlink
は含まれていないのが原因でした。
なので、ApolloBoost was initialized with unsupported options: link
とか怒られるんだけど、
ずっとNetworkタブをみてなんでだろって思っていたので、Consoleタブを見ていなかった。
どうせならもっと盛大に止まってくれればよかったのに。
import ApolloClient from 'apollo-client';
として解決。
この場合、
import ApolloClient from 'apollo-client'; import { InMemoryCache } from 'apollo-cache-inmemory'; const client = new ApolloClient({ link: xxxxxx, cache: new InMemoryCache(), })
として cache
を指定しないといけないし、apollo-boost
をみて必要そうなlinkをコピってくる必要がある。
turbolinksとGoogle Ad Manager (旧 DoubleClick for Publishers)
Google Ad Managerのレポートを見ていたら、Key-Valueと広告ユニットの組み合わせとしてありえないものが出力されていた。 turbolinksのせいで前の画面のKey-Valueが残っているのだろうと思い、調べてみると、
Turbolinks Compatibility with DoubleClick for Publishers
こういうのが出てきた。すごくよさそうだけど、使っているイベントはturbolinks 5に対応していないので、リンクされているGitHubのissueを参照してみる。
CoffeeScriptなのを書き換えて、レスポンシブ対応とKey-Value対応を入れるとこんな感じ。
export default class Gpt { constructor() { this.slots = {}; window.googletag = window.googletag || {}; window.googletag.cmd = window.googletag.cmd || []; document.addEventListener('turbolinks:before-visit', this.clearAds.bind(this)); document.addEventListener('turbolinks:load', this.evaluate.bind(this)); this.evaluate(); } evaluate() { let targeting = document.querySelector('meta[name=gpt-targeting]'); if (targeting) { targeting = JSON.parse(targeting.content); } window.googletag.cmd.push(() => { for (let target in targeting) { window.googletag.pubads().setTargeting(target, targeting[target]); } }); const ads = document.querySelectorAll('.gpt-ad'); for (var slot of ads) { const cachedSlot = this.slots[slot.id]; if (cachedSlot) { this.refreshSlot(cachedSlot); } else { this.defineSlot(slot); } } } defineSlot(slotEl) { const divId = slotEl.id; const path = slotEl.dataset.gptPath; const dimensions = JSON.parse(slotEl.dataset.gptDimensions); window.googletag.cmd.push(() => { // 指定できるブラウザサイズは実際の端末サイズよりも小さくなります。 // https://support.google.com/admanager/answer/3423562?hl=ja // 実際には15程度小さくなるので、調整する const mapping = window.googletag.sizeMapping() .addSize([992 - 15, 690], [[728, 90], [1080, 128]]) .addSize([0, 0], [[320, 50], [280, 200]]) .build(); const slot = window.googletag.defineSlot(path, dimensions, divId).defineSizeMapping(mapping).addService(window.googletag.pubads()); window.googletag.enableServices(); window.googletag.display(divId); this.slots[divId] = slot; }); } refreshSlot(slot) { window.googletag.cmd.push(() => { window.googletag.pubads().refresh([slot]); }); } clearAds() { window.googletag.cmd.push(() => { window.googletag.pubads().clearTargeting(); window.googletag.pubads().clear(); }); } }
ページのKey-ValueはmetaタグにJSONで書く。 defineSizeMappingはページごとにカスタマイズできずサイトで1つになってしまっているけど、とりあえずはこれで困っていないのでいいかな。
fresh_whenとrender
def index @foo = Foo.find(params[:id]) render "bar" end
こんな感じのやつに fresh_when
を入れて
def index @foo = Foo.find(params[:id]) fresh_when(@foo) render "bar" end
とかやると、キャッシュが効いたときに AbstractController::DoubleRenderError
になってしまう。
ソースみると fresh_when
の中 head :not_modified
してヘッダを送ってしまっているのでこうなるらしい。
def index @foo = Foo.find(params[:id]) fresh_when(@foo) render "bar" unless request.fresh?(response) end
として直したけど、Rails Guide読むと
If you don't have any special response processing and are using the default rendering mechanism (i.e. you're not using respond_to or calling render yourself) then you've got an easy helper in fresh_when:
とか書いてあって、自分で render
するときは
def index @foo = Foo.find(params[:id]) render "bar" if stale?(@foo) end
とやるのがいいみたい。turbolinkを疑ってすごく時間を潰してしまった。