| @WWW

secondlife さんがはてなブログの記事などを引き払って新しいドメインで個人ブログを作ってた。

secondlife さんは今年の春まで世界一周旅行をしていて、ブログで旅行の様子を書いていたが、数ヶ月前に Google Photos が突如過去に Ticker API で取得した写真の URL を無効化して折角の旅行の写真が閲覧不能になるということが起こっていた。

同じ現象は cho45 さんもブログに書いている。

結局お二方とも Google Photos の利用をやめてしまったようだ。

ブログサービスやストレージサービスを使うと楽だが、サービス提供者の胸三寸で機能が利用できなくなってしまうことがある。

secondlife さんの冒頭の記事は「心のざわめき」についてがメインのテーマだ。記事を書く度にはてなブックマークでのリアクションを気にしてしまってよくないので、それらのリアクションから遠い場所に移転する、という趣旨だ。

プラットフォームが提供するサービスは便利だったりコンテンツを生産するモチベーションを与えてくれたりする反面、プラットフォームへの依存度が高まったり、ときにはプラットフォームがもたらすネットワーク効果が負の副作用をもたらしたりする。そういうのに疲れた人は自前でウェブサイトを運用するようになる。

ながしまきょうさんはもっと早くにプラットフォーム依存を脱却しようとしていて、一時は Twitter さえもやめて自サイトで Microblogging していた。

自分はプラットフォームに依存せずにこれまでブログをやってきた。一時期画像のアップロード先に Flickr や Google Photos を使うことを試したけど、最近の記事ではやめていて完全に自前だ。さくらインターネットや AWS といったインフラ部分では IaaS に依存しているが、オペレーションの部分は自分で行っている。ソフトウェアエンジニアとして腕を磨くのが目的だったけど、だんだんとプラットフォームの足かせから自由でありたいという理由が大きくなってきている。

プラットフォームの中でブログを書くと、自分の書いたものがプラットフォームの中の一コンテンツでしかなくなってしまう。自分のブログなのにどこかの知らない人が書いた記事が「関連記事」として表示されてしまう。自分はそういう場所には違和感がある。

インターネットの端っこにいる人たちから「ウェブ縄文時代」に退行していくのではないかと思う。

| @技術/プログラミング

外出自粛かつ自分自身にコロ助疑惑があったのでずっと部屋に閉じこもってサイトのデザインをいじってた。

2020-04-07 デザインアップデート.jpg
2020-04-07 デザインアップデート.jpg

上がこれまでで、左下が今後、右下が画像がワイドな場合のバージョン。これまで横幅 1280px 想定にして画像も横幅 1280px のサイズで表示するようにしていた。

ただしこれだと写真は見やすくても文字が読みにくい。人間の目は 1280px 繰り返し左右に移動させるのには適していないようだった( N=1 )。

そういうわけでまたまた cho45 さんのブログのレイアウトをパクって、文章は横幅短めに、写真はでかいサイズで表示するようにした。

文章部分の幅を 800px にして、画像を読み込んだときに一定の条件にマッチしたら写真の横幅を 1280px で表示するようにした。 margin-left: -250px; してるのがミソ。横幅 800px だと大分文章は読みやすいし、でかい写真は大きく見えて便利。

@media screen and (min-width: 1422px) {
  #content #main article .body > p {
    img[class~="large"] {
      width: $content-max-width;
      max-width: $content-max-width;
      margin-left: -250px;
    }
  }
}

JS はこんな感じのコードを書いた。

const checkImageSize = (target) => {
  if (typeof target === 'undefined') {
    return;
  }
  const width = target.naturalWidth;
  const height = target.naturalHeight;
  const isPhoto = RegExp('(lh3\.googleusercontent\.com|\.jpe?g$)').test(target.src);
  if (width > 1279 && width > height && isPhoto) {
    target.classList.add('large');
  }
}

const lazyImageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const lazyImage = entry.target;
      lazyImage.src = lazyImage.dataset.src;
      lazyImage.addEventListener('load', (event) => checkImageSize(event.target));
      lazyImageObserver.unobserve(lazyImage);
    }
  });
});

const selectors = "#content article .body img, #content article .similar img";
const lazyImages = [].slice.call(document.querySelectorAll(selectors));

if ("IntersectionObserver" in window) {
  for (let image of lazyImages) {
    const src = image.src;
    image.dataset.src = src;
    if (lazyImages.indexOf(image) === 0) {
      const promise = new Promise(resolve => resolve(image));
      promise.then(checkImageSize).catch(setTimeout(checkImageSize, 100))
      continue;
    }
    image.src = "";
  };

  lazyImages.forEach(lazyImage => {
    lazyImageObserver.observe(lazyImage);
  });
}

画像の読み込みが起こったタイミングにフックして画像のサイズチェックを行い、条件にマッチしたら img タグに class を追加し、 CSS のメディアクエリと合わせ技で大きく表示するようにしている。以前やった画像の遅延読み込みのコードを改良した。

その他、 HTML 5 対応が中途半端だったのでマークアップを見直して、適度に <header><secition><article><footer> を使ってマークアップし直した。 このせいだと思うけど Google Adsense の自動広告が差し込まれる位置が変わった。これまでヘッダーの下に入り込んでいたやつが <header><article> の間に入るようになった。

今後も外出禁止が続いたらひたすら自分のブログをいじってしまう予感。こういうエネルギーを仕事とか個人サービス開発とかに当てられるとよいのだろうけどなかなかそういう方向には向かない。

| @ブログ

Rubbish
Rubbish

ブログで使ってる Amazon S3 のバケットの画像のほとんどをミスって空ファイルにしてしまった…。 S3 に上がっている画像、 Cache-Control ヘッダーが付与されていないのですべての画像ファイルに Cache-Control ヘッダーを付与しようとしての事故だった。ファイル一覧を取得して AWS SDK Ruby で Cache-Control だけ付与するつもりだったのにファイルそのものを空で上書きしてしまって無となった。

大した操作じゃないと思って事前にバックアップを取っていなかった& S3 に上げておけば安心だと思って日常のバックアップも行っていなかった。スーパーアホ。写真ならアップロードし直すことは可能だけど、キャプチャとか、アニメーション Gif とか、ブログの内容に合わせて OmniGraffle で描いた画像は基本的には元のファイルが残ってなくて元に戻すことができなかった…。

CDN に残っていたものとローカルでキャッシュされていたものを探したが、 500 ファイル以上が失われてしまった。つらい…。

画像の吹っ飛び、過去にも何回か起こっている。レンタルサーバーの障害で消えたパターンもあったけど、自分のミスで消してしまうパターンが多い。自分が一番信用ならないので画像はプロに管理してもらうのが一番だなと身にしみて思った…。とりあえず S3 バケットのバージョンコントロールを有効にしたいと思います…

追記

AWS SDK Ruby での正しい Cache-Control ヘッダー付与の仕方は以下だった。

object.copy_from(object, cache_control: 'max-age=2592000,s-maxage=31536000', metadata_directive: "REPLACE")

#copy_from の引数に medata_directive: "REPLACE" を渡す必要がある。

🙅🏻‍♀️🙅🏻‍♀️🙅🏻‍♀️以下は NG なので注意されたし ☠️☠️☠️

object.put(cache_control: 'max-age=2592000,s-maxage=31536000')

これやるとファイルの body が空になります 🈳🈳🈳

| @技術/プログラミング

キャッシュ
キャッシュ

CloudFront 転送量削減の試みで色々やっていて、そういやウェブサイトのキャッシュについての説明記事っぽいの読んだことないし、開発現場だと「じゃあキャッシュすればいいじゃん」みたいな発言が行き交うけど、ひとくちにキャッシュと言っても色々あって、ウェブ開発始めたばかりの人にはよく分からないんじゃないかと思ったので書いておきます。

キャッシュの目的

キャッシュの目的.png
キャッシュの目的.png

キャッシュの目的は大きく二つあって、コンテンツ表示の高速化とコストの削減がある。コンテンツの表示高速化は、時間のかかる処理の結果を捨てずに再利用して二回目以降の表示を高速化すること、コストの削減は、処理結果の再利用によってコンピューターの利用時間を減らしたり、データの通信量を減らすことをそれぞれ目的としている。表示高速化とコスト削減はどちらか一方だけを達成するものではなく、多くの場合で両方が同時に実現される。

キャッシュに向いているコンテンツ

キャッシュには向いているコンテンツと向いていないコンテンツがある。

キャッシュに向いているコンテンツ.png
キャッシュに向いているコンテンツ.png

ウェブサイトで配信するものに関して何でもキャッシュできるわけではなく、多くの人が共通して閲覧するものか、更新頻度が低いものでないとキャッシュしてはいけない。更新頻度が低くて多くの人が共通して閲覧するもの(画像、 CSS 、 JavaScript など)はキャッシュしやすい。だがショッピングサイトの購入履歴などは人それぞれなので一律にキャッシュしてはいけない。

キャッシュの種類

キャッシュにはブラウザーキャッシュとサーバーサイドのキャッシュがある。

キャッシュの種類.png
キャッシュの種類.png

ブラウザーキャッシュ

ブラウザーキャッシュは、同じ人が繰り返し訪問するサイトで繰り返し利用されるサイト内画像や JavaScript 、 CSS (静的コンテンツ)に対して提供するのに向いている。二回目以降のアクセス時に前回端末にダウンロードした内容を使い回し、転送量を削減してユーザーの閲覧体験を高速化する。

ブラウザーキャッシュ図解.png
ブラウザーキャッシュ図解.png

同じ人が繰り返し訪問しないタイプのサイト( SNS でバズって多くの人が訪れるが、一ページ見ただけで離脱するようなサイト)ではブラウザーキャッシュをフルに効かせても転送量の削減効果はない。逆に一度訪れた人が何度も訪れるような再訪率の高いウェブサイトでは、静的コンテンツに対して Cache-Control ヘッダーを付与することでブラウザーキャッシュを効かせ、転送量の削減とコンテンツの表示速度を高速化させることができる。

ブラウザーキャッシュを聞かせるには、適切な HTTP ヘッダー( Cache-Control ヘッダーなど)を付けてレスポンスを返せばよい。

サーバーサイドキャッシュ

サーバーサイドでキャッシュできるのは多くの人が共通して閲覧する、更新頻度の低いデータだけということに注意が必要。ログインが必要なサイトで、アクセスする人によってコンテンツを出しわけないといけないようなシステムではサーバーサイドのキャッシュはあまり利用できない。

サーバーサイドキャッシュ図解.png
サーバーサイドキャッシュ図解.png

サーバーサイドのキャッシュには転送量の削減効果はない(少なくとも自サイト訪問者との通信では)が、ランキング集計処理や外部 API の呼び出しなど CPU への負荷や時間がかかる処理の結果をキャッシュし、コンテンツの表示を高速化したいときに有効。

例えばこのサイトでは Amazon のアフィリエイトを利用しているが、 Amazon の API にリクエスト回数制限やリクエスト間隔制限があるので適度にキャッシュを行い、リクエスト回数制限に引っかからないようにしている。

サーバーサイドのキャッシュのやり方は色々ある。アプリケーションにキャッシュするためのコードを書いてメモリ上に保持したり、テキストファイルに保存したり、データベースに保存したり、 Redis などを使ったり。ウェブサーバーのキャッシュを使う方法もある。このブログでもいくつかのキャッシュ機構を組み合わせている。

アプリケーションキャッシュ.png
アプリケーションキャッシュ.png

表示に関わる部分の一部をキャッシュする場合や、キャッシュの無効化をアプリケーションでコントロールしたい場合はアプリケーションでキャッシュする必要がある。

ウェブサーバーキャッシュ
ウェブサーバーキャッシュ

レスポンス全体をキャッシュしてよい場合にはウェブサーバーのキャッシュを使うとよい。その方がアプリケーション層までリクエストが届かず、コンピューターリソースを節約できる。

様々なキャッシュの組み合わせ

更新頻度が低く、多くの人が共通して閲覧するコンテンツはサーバーサイドでもキャッシュできるしブラウザーキャッシュを効かせることができる。画像、 CSS 、 JavaScript などがそれで、これらのファイルはよく CDN ( Content Delivery Network )から配信される。 AWS だと CloudFront というのがある。この手のサービスはサーバーサイドのキャッシュとブラウザーキャッシュを効かせることに特化したもので、コンテンツの転送量を抑えつつウェブサイトの表示速度を高速化してくれる。

CDN もそうだが、サーバーサイドキャッシュとブラウザーキャッシュを併用すると、コンテンツを更新したいときに問題が出てくる。画像ファイルを新しいものに差し替えたが CSS が更新されず古いキャッシュが参照され、画像が非表示になってしまったり、ということが発生する。そういうことが起こらないように、 HTTP にはいくつか仕組みがある。

CDN だと ETag も自動付与してくれて、コンテンツの衝突を抑えてくれる。 CDN はよくできているのでよく分からない人はまずは CloudFront を使って勉強してみるとよいだろう。

特に注意が必要なのがアプリケーションキャッシュとウェブサーバーキャッシュの組み合わせだ。異なるレイヤーでキャッシュを効かせてしまうと、キャッシュを無効化したいときに狙い通りに無効化されず、意図しない障害になってしまったりする。コンテンツの特性に応じて、キャッシュは一つのレイヤーで行うようにするとよいだろう。画像や CSS はウェブサーバーでキャッシュし、更新頻度が低いが動的に生成される JSON はアプリケーションレイヤーでキャッシュするなど。

まとめ

  • ウェブサイトのコンテンツは何でもキャッシュすれば良いわけでない
  • サーバーサイドのキャッシュとブラウザーキャッシュの違いを理解する
  • 多段キャッシュに注意(特にアプリケーションキャッシュとウェブサーバーキャッシュの併用)

| @技術/プログラミング

DSC_4022.jpeg
DSC_4022.jpeg

Amazon Product Advertising API ( PA API )が 5.0 になるらしい。 4.0 は 3 月で廃止になるそうだ(当初は 2 月 11 日と言われていたが、 3 月まで伸びたみたい)。

最近 4.0 に対応させたのにな、と思って調べてみたら何と 4.0 対応したのは 10 年以上前だった。

PA API 4.0 までは AWS アカウントで利用する感じ1だったが、 PA API 5.0 では AWS から独立して Product Advertising API 専用のアカウントを登録しなければならないようだ。

AWS は US Amazon でアカウントを作るのに PA API のレポート画面へのログインでは Amazon Japan のアカウントを使うのは変だなと思っていたけど、 PA API 専用アカウントを設けることでその辺のねじれも解消されるだろう。

クライアントライブラリにはこれまで ecs という gem を使ってきたが、 PA API 5.0 対応はされてなくて、別の人が作った vacuum という gem に乗り換えた。

ecs という gem の名前に違和感をもつ人がいるかもしれない。いまでは Amazon で ECS といえば AWS の ECS ( Elastic Container Service ) のことを指すが、昔は Amazon 自身が PA API という名前ではなく Amazon ECS という名前でアフィリエイト用のシステムを提供していた。 Amazon は命名が色々紛らわしい。

PA API 5.0 は RESTful API ではなく GraphQL のような感じで、欲しいフィールド名を指定して API リクエストする感じになっている。レスポンスのサイズが小さくなって便利になった。

ちなみに移行ガイドは以下にあります。

DSC_4119.jpeg
DSC_4119.jpeg


  1. 10 年前にペパボの面接を受けに行ったときに「 AWS 使えますか?」と聞かれて「はい、使えます。アフィリエイトで小銭を稼いでいます」と答えてしまった。 

| @技術/プログラミング

関連記事に画像を表示するようにして喜んでいたが、先月の AWS の請求額を見てビックリ。普段の 15 倍くらいの金額になっていた。デイリーの利用料金を見ると関連記事に画像を表示するようになった日から高くなっている。

CloudFront 転送量
CloudFront 転送量

このブログの画像は S3 に置いてあって CloudFront から配信している。これまでたくさん写真を掲載しても特にコストは高くなかった( Route 53 の費用など含めても $3 くらい、転送量だけだと $1.5 くらいだった)のが、転送量だけで $30 オーバーになっていた。ブログのサーバー代は Adsense 広告と Amazon アフィリエイトでまかなうつもりでやっているので、これでは完全に赤字になってしまう。

なぜ高くなったのかというと関連記事にサムネイル画像を表示することで、 imageproxy から CloudFront へのアクセスが発生するようになったからのようだった。こんな感じ。

image-data-transfer-infrastructure-1.png
image-data-transfer-infrastructure-1.png

imageproxy にもキャッシュの仕組みはあるが、 CloudFront が返す Cache Control ヘッダーの内容を理解せず決め打ちの時間でキャッシュを Expire させるので効率が悪い。

恐らく以下のように画像関連のインフラは AWS に寄せるのが一番効率的だと思う。Amazon の優秀なエンジニアが作ってる CDN が一番前段に出てブラウザーからのリクエストに答えるのがもっとも効率的に画像を配信できると思う。

image-data-transfer-infrastructure-2.png
image-data-transfer-infrastructure-2.png

ただ個人のブログレベルでここまでやるのは割に合わない感じがしたのでとりあえずは以下のような構成にした。

image-data-transfer-infrastructure-3.png
image-data-transfer-infrastructure-3.png

Nginx の proxy cache を使う。

キャッシュ時間は長めにとって 30d にしておいた。

あわせてキャッシュの HIT 率を計測するようにした。ログに $upstream_cache_status を書き出すようにして、 awk で定期的に集計するようにした。こんな感じ。

cat log/access.log \
  | grep 'cache_hit:' | grep -v 'cache_hit:-' | cut -f16 | sort | uniq -c \
  | awk '{
      if ($2 ~ /HIT/) {
        hit = $1
      };
      if ($2 ~ /EXPIRED/) {
        expire = $1
      };
      if ($2 ~ /MISS/) {
        miss = $1
      };
      sum += $1
    } END {
      hit_rate = hit/sum*100;
      expired_rate = expire/sum*100;
      miss_rate = miss/sum*100;
      print "HIT\t"hit_rate"%\nEXPIRE\t"expired_rate"%\nMISS\t"miss_rate"%"
    }'

こいつを Lokka の Dashboard に表示させる。

キャッシュヒット率
キャッシュヒット率

加えて、 Google の以下の記事を参考に、画像の遅延読み込みを行うようにした。

とりあえずはこれで様子を見たい。いまのところ、ちょびっとずつ転送量は下がってきているような感じがする。もうちょい下げたいところ。

しかし、画像の配信で毎月 $30 もかかるようであれば自前で画像をホストするのは諦めて Flickr に金払って PRO プランを継続した方が安いなと思い始めてしまった…。 Google Photos でも良いが、 Exif がわからなくなるのと埋め込み用の画像を取得する作業(公開用のアルバムを作ってそこに埋め込みたい写真を入れていく必要がある)が面倒くさいので移行に踏み切れない。

| @技術/プログラミング

Hot Chocolate @ Tana Cafe & Coffee Roaster

この記事は CircleCI Advent Calendar 2018 19 日目の記事ですが間に合わず一日遅れて書いております。すんません 🙇🏻

CircleCI を使った Rails アプリのデプロイフローみたいな話を書こうかなと思ったのですが、すでに他の方が書いてる内容とかぶりそうだし、自分自身ブログに過去何回も書いた話なんで今回はエモ方面の話を書くことにします。技術的な情報はないのでそっち方面を期待している方はすんません。


いまの職場で働き始めて 1 年半なんですが、当初は CI はなく、テストコードもありませんでした。いまはそこで当たり前のように CI が回り、テストのカバレッジもまぁまぁ高く、デプロイは CircleCI 経由でじゃんじゃん行われるような状況となっております。新しく会社に入った人も GitHub の Organization に入ってもらえたらその瞬間から deploy 実行できます。具体的な話は昔書いてますのでよかったらご覧下さい。

8 年くらい前の自分はどうやったら CI だとか自動デプロイだとかできるようになるのか皆目見当が付きませんでした。いま 8 年前の自分と同じような状況にいる人(回りにテストを書く習慣を持つ人がいない人、 CI 動かすためにどうすればよいかわからない人)に何か言いたいと思い筆をとりました。

まずは何はなくとも頑張って一つテストケースを書いてみましょう。最初からカバレッジ 100% とか目指さなくてもよいです。どれか一つ、テストが書きやすそうなコードを見つけてテストを書き、ローカルで実行してテストがパスするのを確認しましょう。テストファーストとかも最初から目指さなくてよいです。

手元でテストが通ることを確認したら、 CI 環境でもテストを実行できるようにしましょう。

昔は Jenkins しか選択肢がなく、 Jenkins が動く環境をセットアップする(サーバーを調達する、 VPS を借りてもらう、などなど)に社内調整が必要でしたが、 CircleCI ならプライベートリポジトリでも 1 プロセスなら無料で使えますので社内調整が非常に楽です(外部にコード出してはダメな職場だと厳しいですね…)。

最初にプロジェクトを追加して言語を選ぶと設定ファイルが自動生成されるので、それをコピペして .circleci/config.yml として保存し、リポジトリにコミットするだけでとりあえずビルドが実行されるようになります。

昔は難しかった CI 環境構築のうち、お金の問題、設定の難しさの問題を CircleCI は解決してくれます。あとはあなたが頑張るだけです。

CircleCI ならビルド終了ごとに結果を Slack などチャットシステムに通知させることができます。まずはテストケースが一つでもよいのでリポジトリへの push をトリガーにビルドが実行されたら結果を Slack に通知してみましょう。

CircleCI Slack Notification
CircleCI Slack Notification

CircleCI Slack Notification
CircleCI Slack Notification

リポジトリに GitHub を使っているなら Pull Request にビルド結果が表示されるようになるはずです。

CircleCI GitHub Build status
CircleCI GitHub Build status

これらで「なんかようわからんけどやっとる感」を出していきましょう。

そして過去のコードのことは一旦無視して、あなたが新しく追加する部分に関してはテストコードをセットで書くようにしていきましょう。あなたがコードレビューを依頼するときには必ずテストがグリーンな状態で依頼するようにするのです。

そうこうしているうちに他の人が出した Pull Request でテストが失敗するケースが発生します。 Slack の #circleci チャンネルに赤色の Failure 通知が届き社内が騒然とするかもしれません。しかしこれはチャンスです。

「よかった、これでバグが未然に防げましたね」

あなたのこの一言でテストや CI がもたらす開発効率の向上がチームの皆さんに伝わるはずです。こうなったらもう一押しです。あなたがテストと CI の伝道師になりましょう。テストを書くことが当たり前になってきたら、 CircleCI からの deploy や定型処理を CircleCI でやらせるような使い方にチャレンジしていきましょう。どんどん周囲を巻き込んで、 CI 文化を定着させていって下さい。

何はともあれ、最初は一つのテストコードを書くことから始まります。変更に強いコードを書いてじゃんじゃん deploy し、じゃんじゃん Money making していきましょう🤑