JavaScriptでsmooth scrollを実装する(IE対応)

背景

JavaScriptでsmooth scrollを実装してほしいという依頼を受けました。そのサイトはあるASPを利用していて、なにかをインストールするということはできません。

方法を探す

JavaScript初心者のわたしはとりあえずググることにしました。すると、スムーズスクロールを実装しようとしている人なら誰もが見ていそうなページが見つかりました。

素のJavaScriptで実装したかったので、このページに書かれているwindow.scrollToを使う方法でいこうと思います。

実装

ざっと実装してみました。

1
2
3
4
5
6
7
8
9
10
11
const buffer = 50;
const scrollTarget = document.getElementById('scroll_target');
const scrollTrigger = document.getElementById('scroll_trigger');

scrollTrigger.addEventListener('click', function (e) {
e.preventDefault();
const scrollTargetTop = scrollTarget.getBoundingClientRect().top;
const offsetTop = window.pageYOffset;
const top = scrollTargetTop + offsetTop - buffer;
window.scrollTo({ top: top, behavior: 'smooth' });
});

scroll_triggerというidを付与したaタグにスクロールのイベントリスナーをセットします。そのほか、いくつかのブラウザのAPIを利用しています。

element.getBoundingClientRect

ドキュメント

ドキュメントにあるように、width と height 以外のプロパティは、ビューポートの左上を基準としていますので、ページ内の現在の表示箇所によって返ってくる値は異なります。(ビューポートの左上より下にあればプラスの値、上にあればマイナスの値)

window.pageYOffset

ドキュメント

こちらもドキュメントにあるように、現在のビューポートがページの一番上からどれくらいスクロールしているかを表します。pageYOffsetはscrollYのエイリアスだそうです。

そして、今この文章を書いていて気付いたのですが、

1
const top = scrollTargetTop + offsetTop - buffer;

は常に同じ値を返すので(当たり前ですか?w)、addEventListenerの中で毎回計算する必要はありませんでした…

window.scrollTo

ドキュメント

scrollToはドキュメントの構文にあるように、2種類の引数をとります。

1
2
window.scrollTo(x-coord, y-coord)
window.scrollTo(options)

今回利用しているのは引数にoptionsをとる方です。optionsはこちらで定義されています。今回はtopとbehaviorを指定しています。

IEで動作させたい

現状のままでは、IEで動作しませんでした…IEでも動作させたいので、手を入れていきます。

scrollToの引数を変える

先ほど書いた通りwindow.scrollToの引数は2種類あって、IEはオブジェクトではない方であれば、スクロールするということで、やってみました。

1
window.scrollTo(0, top);

すると、スムーズスクロールではなく、その位置に移動する動作になってしまいました。これでは最低限の役目は果たしているもののイマイチです。

polyfillの導入

今回一番勉強になったのがこちらです。最初に紹介したスムーズスクロールを紹介していたページに記載されていましたが、npm installしていたので見過ごしていました。npm installせずとも利用できました。

polyfillとは?

ドキュメント

MDN Web Docsにはなんでもありますね…ドキュメントにあるように、最近の機能をサポートしていない古いブラウザーで、その機能を使えるようにするためのコードです

polyfillを利用する

https://polyfill.io/v3/url-builder/で利用したい機能のチェックボックスにチェックを入れます。今回はsmoothscrollにチェックを入れます。すると、画面上部にURLが表示されます。copy url to clipboardをクリックしコピーします。

今回のURLは

https://polyfill.io/v3/polyfill.min.js?features=smoothscroll

です。これを<script>で読み込みます。

1
<script src="https://polyfill.io/v3/polyfill.min.js?features=smoothscroll"></script>

この1行を一番最初に書いたJavaScriptの上に追加します。そしてIEで試してみると、スムーズスクロールできていました。

まとめ

JavaScript初心者のわたしがスムーズスクロールを実装するまでを記しました。JavaScript初心者はまずブラウザAPIを正しく理解することが重要だと思いました。MDN Web Docsのドキュメントをちゃんと読むべきですね。

JavaScriptの言語仕様を学ぶのであれば、JavaScript Primerを読みましょう。この本はとてもわかりやすく書かれていて、プログラミングの用語についてもわかりやすく説明してくれています(プリミティブ、リテラルなど)

IEで動作しない場合はpolyfillを最初に調べましょう。1行足すだけでIE対応ができるかもしれません。

参考図書

JavaScript Primer

RailsでAuthorizationヘッダを用いた認証を実装する

背景

仕事でAPIの実装を行うことになりました。ログイン後に発行するtokenでユーザー認証が必要ということで、どういった方法があるかを調べてみました。Authorizationヘッダを用いることが多いのは知っていたので調べてみることにしました。

Authorizationヘッダについて

Authorizationの仕様についてはしっかりと理解していなかったので改めて調べてみました。

Authorization - HTTP | MDN

このサイトに記載されているように、
Authorization: <type> <credentials>
が構文のようです。以前、 Authorization: <credentials>という実装をしていましたが、こちらは仕様に準拠していないことになります。

typeについて

Typeにはいろんな種類があるようです。個人的によく見るのは Bearerです。OAuth認証で利用したりしました。
先ほどのMDNの中にtypeの一覧へのリンクがありました。
Hypertext Transfer Protocol (HTTP) Authentication Scheme Registry

TypeがBasicだった場合、credentialsをBase64でデコードする必要があったりして、少し手間がかかりそうな印象です。

Bearerを試してみる

まずは簡単にできそうなBearerを試してみます。
API側では、authenticate_or_request_with_http_tokenメソッドを用います。

1
2
3
4
5
6
def authenticate
authenticate_or_request_with_http_token do |token, _options|
@user = User.find_by_token token
head status: 401 if @user.blank?
end
end

クライアント側はcurlを使います

1
2
$ curl -H 'Authorization: Bearer xxx_credentials' https://api_endpoint
HTTP Token: Access denied.

認証が失敗してしまいました…
ブロック内でbinding.pryを呼び出してもスルーされることから、ブロックに到達する前に認証に失敗しているようです。

いろいろ調べてみると、以下のサイトが見つかりました。

週刊Railsウォッチ(20191118前編)ActiveJob引数のログ抑制、RailsガイドProプランお試し、ファイルアップロードのレジュームgemほか|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

BearerだけではなくTokenでも良いようなので、Tokenを試してみます

1
$ curl -H 'Authorization: Token xxx_credentials' https://api_endpoint

これだとよくわからないので、レスポンスヘッダを表示してみます

1
2
3
4
5
6
7
8
9
10
11
$ curl -D - -H 'Authorization: Token xxx_credentials' https://api_endpoint
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
ETag: W/"d396de9e73b218ac6b8f0de7967e82bc"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 0f90b919-e9f6-42ee-a76a-96930fbfdff2
X-Runtime: 0.064172
Transfer-Encoding: chunked

認証成功しました!

ということはTokenではよくてBearerはダメということですね。原因を調査するためにauthenticate_or_request_with_http_tokenを調べてみます。
該当のバージョンでgithubを探すと該当箇所が見つかりました。

rails/http_authentication.rb at 4-2-stable · rails/rails · GitHub

なんと、Bearerでは引っかからないようになっています…
Tokenだと認証可能ですが、Tokenはの定義にはありません…

Basicを試す

こうなったら一般的なBasicを試してみます。credentialsの部分はユーザー名:パスワードになりますが、今回ユーザー名がないので:パスワードとなり、パスワードの部分にcredentialsを指定します。

API側ではauthenticate_or_request_with_http_basicを利用します

1
2
3
4
5
6
def authenticate
authenticate_or_request_with_http_basic do |_user, password|
@user = User.find_by_token password
head status: 401 if @user.blank?
end
end

クライアント側はBase64.encode64(‘:xxx_credentials’)でBase64エンコードした文字列を準備します。(先頭の:を忘れない)

1
2
$ curl -H 'Authorization: Basic base64_encode_credentials' https://api_endpoint 
HTTP/1.1 200 OK

うまく認証されました!

まとめ

認証はセキュリティ的に重要な箇所になるので、慎重に実装しましょう。
AuthorizationヘッダなどHTTPの仕様をしっかり理解してからどの方法を使うかを検討しましょう。

おまけ

今回クライアント側としてcurlを使いました。有用なオプションがいっぱいありますね。以下にまとめたいと思います。

  • -D (–dump-header)

    HTTPレスポンスヘッダを表示します。標準出力に出力するには-を指定します。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $ curl -D - https://www.google.com
    HTTP/2 200
    date: Wed, 16 Sep 2020 17:16:16 GMT
    expires: -1
    cache-control: private, max-age=0
    content-type: text/html; charset=ISO-8859-1
    p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
    server: gws
    x-xss-protection: 0
    (省略)
  • -o

    レスポンスボディの出力先を指定します。不要な場合に/dev/nullを指定したりします。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    $ curl -D - -o /dev/null https://www.google.com
    % Total % Received % Xferd Average Speed Time Time Time Current
    Dload Upload Total Spent Left Speed
    0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0HTTP/2 200
    date: Wed, 16 Sep 2020 17:19:47 GMT
    expires: -1
    cache-control: private, max-age=0
    content-type: text/html; charset=ISO-8859-1
    p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
    server: gws
    x-xss-protection: 0
    x-frame-options: SAMEORIGIN
    set-cookie: 1P_JAR=2020-09-16-17; expires=Fri, 16-Oct-2020 17:19:47 GMT; path=/; domain=.google.com; Secure
    set-cookie: NID=204=rwcy-xyJUKkXMHIec1I-ETPuB_hzV-mbxnl5ot0DhU2q2B-I-aRIVHKkHFyKvsqESZqjRXQFxn7YjzjmvTkflbrfzePsc87d7F7GFELqNbXvPvATIo2v6EaeaHzIXrsoAL4LRKd20uniGfFR6dN8pq3gB4O4SPDzjYd7EJvWLaA; expires=Thu, 18-Mar-2021 17:19:47 GMT; path=/; domain=.google.com; HttpOnly
    alt-svc: h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
    accept-ranges: none
    vary: Accept-Encoding

    100 13141 0 13141 0 0 62279 0 --:--:-- --:--:-- --:--:-- 62279
  • -s

    進捗を表示しないようにします。

    例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ curl -D - -o /dev/null -s https://www.google.com
    HTTP/2 200
    date: Wed, 16 Sep 2020 17:21:07 GMT
    expires: -1
    cache-control: private, max-age=0
    content-type: text/html; charset=ISO-8859-1
    p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
    server: gws
    x-xss-protection: 0
    x-frame-options: SAMEORIGIN
    set-cookie: 1P_JAR=2020-09-16-17; expires=Fri, 16-Oct-2020 17:21:07 GMT; path=/; domain=.google.com; Secure
    set-cookie: NID=204=Mdp8loY0lxxV_Kf-YoGRq3db-HvCsX81ffRzePmxfFeT6gb2A72Ijy8AoG5PHsjGj962rqODoZ0VXn9ZMen5MPFGnUww7qnmuexWFLC0JGlyR07K1iIO3iAub_CFUzdjZogChUYvm1Qo9yxyzlm2pb0A2i44va50JOIBi-8qrDE; expires=Thu, 18-Mar-2021 17:21:07 GMT; path=/; domain=.google.com; HttpOnly
    alt-svc: h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
    accept-ranges: none
    vary: Accept-Encoding

    ヘッダだけ欲しい場合はこれで良いですね。

  • -X

    HTTPメソッドを指定します。指定しない場合はGETになります。

    例:

    1
    2
    3
    4
    5
    6
    7
    $ curl -X POST https://www.google.com
    <!DOCTYPE html>
    <html lang=en>
    <meta charset=utf-8>
    <meta name=viewport content="initial-scale=1, minimum-scale=1, width=device-width">
    <title>Error 405 (Method Not Allowed)!!1</title>
    (省略)

    405が返ってきてますね…POSTには対応してないようです

  • -F(–form)

    フォームの値を指定します。key=valueという形式で指定します

    例:

    1
    $ curl -X POST -F 'username=user' -F 'password=password' https://somewhere_login_url

他にもいろいろあるようですが、また何かわかったら追記します。

ECSでRailsのassetsを配信する方法

これまでのまとめ

今までGitHub Actionsを用いてECSデプロイする方法を調査してきました。そして、なんとかECSにデプロイできるところまできました。

今回は、デプロイできるようになったものの、アセットが表示されなかったときの対応方法をまとめたいと思います。

状況の確認

ECSは起動タイプにFargateを選択していました。ですのでコンテナの状況はCloudWatchに流れてくるログからのみ知ることができました。

ログにはアセットが見つからないと言う情報だけしかありませんでした。最初はアセットのプリコンパイルが失敗しているのかと思い、コンテナ作成時にプリコンパイルしたり、ECSで起動時にプリコンパイルしたりしましたが状況は変わらずでした。

起動タイプEC2で試す

やはりこういうときはコンテナにアクセスできた方がデバッグが早いです。面倒でしたが起動タイプEC2で起動し、コンテナにアクセスしました。

アセット類はRailsコンテナ内に存在していました。

原因の調査

この時点ではまだ良くわからなかったので、アプリケーションのURLではなく、アセット単体のURLにアクセスしてみました。

するとnginxが404を返してきます。nginxが404を返してくる、そもそもupstreamのRailsにリクエストをフォワードしていないのでは?と思ってnginxの設定を調べてみると、nginxの設定はnginxとRailsが一つのサーバで動作していた時のままでした。

  • nginxのDocumentRootはRAILS_ROOT/public
  • locationの設定もそのまま
  • アセットはnginxが処理し、それ以外はupstreamのRailsにフォワード

これではアセットが表示されないのも当然です。nginxとRailsを別コンテナで動かすという単一責務の法則に従ったことで、以前の設定が使えなくなっていました。

対応

まず、全てのアクセスをupstreamにフォワードするようnginxの設定を変更します。locationの優先順位がちゃんと理解できていなかったことを思い知らされました。nginxについて再度学びたいと思います(後日記載します)。

nginxの設定を変更すると、エラー表示がnginxからRailsに変わりました。これでリクエストがRailsにフォワードされていることがわかります。

Railsのエラーメッセージを確認すると、ActionController::RoutingErrorとなっていて、アセットファイルへのrouteがないと言っています。

原因を確認すると、静的ファイルの配信設定ができていないようでした

1
2
3
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?

config/environments/下のファイルに上記のように記載されていたので、タスク定義ファイルに

1
2
3
"environment": [
{ "name": "RAILS_SERVE_STATIC_FILES", "value": "true" }
],

と設定することで、静的ファイルの配信ができるようになりました。

まとめ

nginxとRailsが同じサーバで動作することに慣れてしまっていたので、原因がわかるまで時間がかかってしまいました。わかってしまえばそんなに大したことではないのですが、わかるまでが大変ですね…

今回のことでRailsやnginxの設定がわかっていないことがよくわかりました。nginxの設定、とくにlocationの設定はちゃんと理解しておくべきだと思うので、これから重点的に学びたいと思います。

ndenvからnodenvへ移行する

今更ながらnodenvに移行

JavaScript Primerという本を買って読んでみると、JavaScriptの今までわからなかったところがわかるようになりとてもJavaScriptが楽しくなってきました。(Web版もあります)。

ローカルの開発環境を新しくしようと思い、anyenv経由でインストールしたndenvをアップデートしました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ anyenv update
$ anyenv update
Updating 'anyenv'...
| From https://github.com/riywo/anyenv
| e963a69..67d402f master -> origin/master
| * [new tag] v1.1.2 -> v1.1.2
Updating 'anyenv/anyenv-update'...
Updating 'ndenv'...
Updating 'ndenv/ndenv-yarn-install'...
Updating 'ndenv/node-build'...
Updating 'rbenv'...
| From https://github.com/rbenv/rbenv
| c879cb0..0843745 master -> origin/master
Updating 'rbenv/ruby-build'...
| From https://github.com/rbenv/ruby-build
| 69ccbf4..0a5e059 master -> origin/master
| * [new tag] v20200819 -> v20200819
| * [new tag] v20200722 -> v20200722
| * [new tag] v20200727 -> v20200727
Updating 'anyenv manifest directory'...
| From https://github.com/anyenv/anyenv-install
| dcbcfe1..d9791df master -> origin/master

ところが、ndenvのgithubを見てみると

[Deprecated] nodenv is better alternative

と書かれていました。以前はnodenvを使っていて、いつからかndenvになったと記憶しています。結局元に戻ったと言う感じでしょうか。

nodenvのインストール

anyenvを利用してインストールします

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ anyenv install nodenv
/var/folders/mf/_4_k88fj3nqcsgqdz7dzqhy00000gn/T/nodenv.20200828113307.39306 ~
Cloning https://github.com/nodenv/nodenv.git master to nodenv...
Cloning into 'nodenv'...
remote: Enumerating objects: 14, done.
remote: Counting objects: 100% (14/14), done.
remote: Compressing objects: 100% (13/13), done.
remote: Total 4017 (delta 2), reused 4 (delta 1), pack-reused 4003
Receiving objects: 100% (4017/4017), 731.96 KiB | 828.00 KiB/s, done.
Resolving deltas: 100% (2633/2633), done.
~
~/.anyenv/envs/nodenv/plugins ~
Cloning https://github.com/nodenv/node-build.git master to node-build...
Cloning into 'node-build'...
remote: Enumerating objects: 95, done.
remote: Counting objects: 100% (95/95), done.
remote: Compressing objects: 100% (61/61), done.
remote: Total 19746 (delta 39), reused 68 (delta 25), pack-reused 19651
Receiving objects: 100% (19746/19746), 3.51 MiB | 776.00 KiB/s, done.
Resolving deltas: 100% (12653/12653), done.
~
~/.anyenv/envs/nodenv/plugins ~
Cloning https://github.com/nodenv/nodenv-vars.git master to nodenv-vars...
Cloning into 'nodenv-vars'...
remote: Enumerating objects: 211, done.
remote: Total 211 (delta 0), reused 0 (delta 0), pack-reused 211
Receiving objects: 100% (211/211), 31.82 KiB | 201.00 KiB/s, done.
Resolving deltas: 100% (76/76), done.
~

Install nodenv succeeded!
Please reload your profile (exec $SHELL -l) or open a new session.

インストール後はシェルを再起動します

1
$ exec $SHELL -l

ndenvのアンインストール

アンインストールもanyenvを利用して行います。

1
2
$ anyenv uninstall ndenv
anyenv: remove /Users/shibagaki/.anyenv/envs/ndenv?

yesと入力してエンターキーを押すとアンインストールされます。

nodeのインストール

nodenvを使ってインストールします。現時点で最新のLTSである12.18.3をインストールします。

1
2
3
4
5
6
7
8
$ nodenv install v12.18.3
node-build: definition not found: v12.18.3

See all available versions with `nodenv install --list'.

If the version you need is missing, try upgrading node-build:

git -C /Users/shibagaki/.anyenv/envs/nodenv/plugins/node-build pull

見つからないと言われてしまったので、一覧表示してみます。

1
2
3
4
5
6
7
8
$ nodenv install --list
...
12.18.1
12.18.2
12.18.3
13.0.0
13.x-dev
...

先頭のvがいらないんですね。

1
2
3
4
5
$ nodenv install 12.18.3
Downloading node-v12.18.3-darwin-x64.tar.gz...
-> https://nodejs.org/dist/v12.18.3/node-v12.18.3-darwin-x64.tar.gz
Installing node-v12.18.3-darwin-x64...
Installed node-v12.18.3-darwin-x64 to /Users/shibagaki/.anyenv/envs/nodenv/versions/12.18.3

インストールされたことを確認します

1
2
3
4
5
6
7
$ nodenv local 12.18.3
$ which node
/Users/user/.anyenv/envs/nodenv/shims/node
$ node -v
v12.18.3
$ npm -v
6.14.6

これで環境を整えることができました。

まとめ

  • ndenvよりはnodenvを利用するようにする
  • nodenvでバージョンを指定する時は先頭のvはいらない
  • バージョン表示時はvがついている

gitのバージョン管理の仕組みを知る その1

きっかけ

今、【夏の翔泳社祭】PDF版電子書籍50%ポイント還元のキャンペーンが行われています。前回のキャンペーンでたまったポイント(前回は書籍購入で50%ポイント還元)があるので、おもしろそうな本があったら買おうと思っていろいろ見てみるとエンジニアのためのGitの教科書[上級編] Git内部の仕組みを理解するを見つけました。

自分のgitのレベルは、gitを使ってプロジェクトを進めることはできますが、なにかイレギュラーなことが起こった時には、ちょっとした緊張が走ってしまうくらいの感じです。ちゃんと理解できていればそうはならないんだと思うので、これを機にgitをより深く学びたいと思います。

本書の構成

構成は、初級、中級、上級の3部構成になっています。全体で80ページ弱のコンパクトな本です。

今回は初級の部分をまとめていきたいと思います。

Gitのバージョン管理の仕組みを知る 〜初級編〜

まとめ

hexoでtwitter cardを記事ごとに変更する

背景

前回、twitter cardの設定を変更してみました。表示されるようになったのですが、すべての記事で同じだとおもしろくありません。

記事ごとの設定はmdファイルのyaml部分に記述しています。titleやdate、updatedなどと同じように設定できそうな気がします。ちょっと調べてみましょう。

updatedの実現方法を確認する

このサイトでは更新日時を表示するようにしています。前回の記事だと、以下のように設定しています。

1
2
3
4
5
6
7
8
9
---
title: hexoでtwitter cardの設定を変更する
date: 2020-08-07 12:30:00
updated: 2020-08-07 12:30:00
categories:
- テクニカルブログ
tags:
- hexo
--

上記の設定をどうやって取得しているのかthemesディレクトリ以下で探してみます。

1
2
3
4
$ grep -R updated themes
themes/landscape/layout/_partial/article.ejs: <%- partial('post/updated', {class_name: 'article-date', date_format: null}) %>
themes/landscape/layout/_partial/post/updated.ejs:<% if (post.updated) { %>
themes/landscape/layout/_partial/post/updated.ejs: Updated: <time datetime="<%= date_xml(post.updated) %>" itemprop="dateUpdated"><%= date(post.updated, date_format) %></time>

post.updatedとすることで取得できているようです。

twitter cardのURLを指定してみる

記事にtwitter_imageというキーを設定して、実際に変わるか試してみます。こちらの記事で試してみます。

記事の設定にtwitter_imageを追加して、URLを記述します

1
twitter_image: https://book-reviews.blog/images/github_actions_rails_template.png

次にthemes/landscape/layout/_partial/head.ejsのtwitter cardの設定の箇所で、twitter_imageを参照するよう変更します。

1
25   <%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id, twitter_card: 'summary_large_image', image: post.twitter_image}) %>

Card validatorで確認すると、そもそも表示されずタイムアウト…
というか、サイト自体も表示できなくなってしまっています。これはエラーが起こっている可能性が高いので、一旦元に戻します。

post.twitter_imageが参照できない原因調査

post.updatedで正しく参照できている場合と、今回はなにが違うのでしょうか。

post.updatedを参照しているのは、themes/landscape/layout/_partial/post/updated.ejsです。そのファイルを読み込んでいるのはthemes/landscape/layout/_partial/article.ejsです。

article.ejsを読み込んでいるファイルを確認します。

1
2
3
4
$ grep -R 'partial/article' themes/
themes/landscape/layout/page.ejs:<%- partial('_partial/article', {post: page, index: false}) %>
themes/landscape/layout/post.ejs:<%- partial('_partial/article', {post: page, index: false}) %>
themes/landscape/source/css/style.styl:@import "_partial/article"

partialメソッドに変数を引き渡していることがわかりました。この記述が必要そうですね。試してみます。

themes/landscape/layout/layout.ejsの1行目を以下のように変更します。

1
<%- partial('_partial/head', {post: page}) %>

再度twitter cardの設定でpost.twitter_imageを参照するように変更し、Card validatorで試してみます。

記事ごとでtwitter cardの画像を変更できた

うまくいきました!

記事にtwitter_imageの設定がない場合はデフォルトでキービジュアルの画像を表示するように修正します。

1
2
3
4
5
6
<% if (post.twitter_image){ %>
<% twitter_image = post.twitter_image %>
<% } else { %>
<% twitter_image = '/css/images/banner.jpg' %>
<% } %>
<%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id, twitter_card: 'summary_large_image', image: twitter_image}) %>

EJSを初めて書きました。概要は知っておいた方が良さそうですね。

まとめ

今回twitter cardを記事ごとに設定することができました。よりよいサイト作りのために、いろいろカスタマイズしていこうと思います。そのためにはhexo, EJSの知識がもうちょっと必要ですね。学んでいきたいと思います。

hexoでtwitter cardの設定を変更する

背景

しばらく使っていなかったtwitterをブログの記事更新の告知に使おうと思って投稿してみました。自分のツイートを見てみると、twitterカードがうまく表示できていませんでした。titleやdescriptionはデフォルトである程度設定してくれているようです。なのでtwitterカードのカスタマイズを行おうと思います。

headタグがあるpartialを探す

OGP関連の記述はheadタグ内にあります。headタグはどのpartialで記述されているのでしょうか。とりあえずthemesディレクトリ配下をgrepしてみました。

1
2
3
4
5
6
7
8
9
10
11
12
$ grep -R twitter themes
themes/landscape/layout/_partial/head.ejs: <%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id}) %>
themes/landscape/source/css/_variables.styl:color-twitter = #00aced
themes/landscape/source/css/_partial/article.styl:.article-share-twitter
themes/landscape/source/css/_partial/article.styl: background: color-twitter
themes/landscape/source/css/_partial/article.styl: text-shadow: 0 1px darken(color-twitter, 20%)
Binary file themes/landscape/source/css/fonts/FontAwesome.otf matches
Binary file themes/landscape/source/css/fonts/fontawesome-webfont.ttf matches
themes/landscape/source/js/script.js: '<a href="https://twitter.com/intent/tweet?url=' + encodedUrl + '" class="article-share-twitter" target="_blank" title="Twitter"></a>',
themes/landscape/README.md:twitter:
themes/landscape/README.md:- **twitter** - Twiiter ID
themes/landscape/_config.yml:twitter:

最初に見つかったtheme/landscape/layout/_partial/head.ejsがそれっぽいですね。中身を確認してみます。

1
2
3
4
5
6
23   <title><% if (title){ %><%= title %> | <% } %><%= config.title %></title>
24 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
25 <%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id}) %>
26 <% if (theme.rss){ %>
27 <link rel="alternate" href="<%= url_for(theme.rss) %>" title="<%= config.title %>" type="application/atom+xml">
28 <% } %>

実際に出力されるHTMLを見ると、25行目の1行でOGP関連のタグを全て出力していることがわかります。カスタマイズするにはopen_graphというヘルパメソッド?がなにをしているのかを理解する必要がありそうです。メソッドがどこで定義されているかを確認します。

open_graphメソッド

open_graphメソッドはどこで定義されているかを確認します。
hexoアプリケーションのTOPでgrepしてみます。

1
2
3
$ grep -R open_graph .
./themes/landscape/layout/_partial/head.ejs: <%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id, twitter_card: 'summary_large_image', image: '/css/images/banner.jpg', twitter_site: '@motoa'}) %>
./node_modules/hexo/lib/plugins/helper/index.js: helper.register('open_graph', require('./open_graph'));

1行目は先ほどのpartialですね。2行目が定義箇所っぽいですね。該当のファイルの中身を見てみます。

1
2
3
4
5
6
43   helper.register('list_tags', require('./list_tags'));
44 helper.register('list_posts', require('./list_posts'));
45
46 helper.register('open_graph', require('./open_graph'));
47
48 helper.register('number_format', require('./number_format'));

正確には理解できてないですが、open_graphというhelperを登録しているような感じでしょうか。helperの処理はrequire('./open_graph')の戻り値なのでしょう。同じディレクトリにあるopen_graph.jsを見てみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
118   result += meta('twitter:card', twitterCard);
119 result += meta('twitter:title', title);
120 if (description) {
121 result += meta('twitter:description', description, false);
122 }
123
124 if (images.length) {
125 result += meta('twitter:image', images[0], false);
126 }
127
128 if (options.twitter_id) {
129 let twitterId = options.twitter_id;
130 if (twitterId[0] !== '@') twitterId = `@${twitterId}`;
131
132 result += meta('twitter:creator', twitterId);
133 }
134
135 if (options.twitter_site) {
136 result += meta('twitter:site', options.twitter_site, false);
137 }

twitter関連の設定をしている箇所はこんな感じでした。twitterカードの種類(summary, summary_large_image)の設定はtwitterCardという変数で設定されているようです。twitterCardという変数はどこで定義されているのか確認します。

1
42   const twitterCard = options.twitter_card || 'summary';

変数ではなく定数でした。options.twitter_cardが指定されていればそれを、指定されていなければsummaryになるということですね。デフォルトはsummaryになるので、なにも設定していない現在はsummaryになっています。

では、optionsを確認します。

1
30 function openGraphHelper(options = {}) {

optionsはこの関数の引数でした。ということは、open_graphメソッドの引数のハッシュにtwitter_cardというキーでtwitterカードの種類を設定すればできそうですね。

試してみる

theme/landscape/layout/_partial/head.ejsのopen_graphの呼び出しを以下のように変更してみます。

1
<%- open_graph({twitter_id: theme.twitter, google_plus: theme.google_plus, fb_admins: theme.fb_admins, fb_app_id: theme.fb_app_id, twitter_card: 'summary_large_image', image: '/css/images/banner.jpg'}) %>

twitter_cardをsummary_large_imageに指定しました。またその際に表示する画像をimageで指定しています。今回は試しにキービジュアルの画像を指定しています。

確認

twitter card validatorを利用します。TOPページのURLを入力し、Preview cardをクリックします。

twitter card validatorの結果

summary_large_imageで表示できました!

まとめ

hexoのOGP関連の設定をカスタマイズしてみました。今回の設定だと全てのtwitter:imageがキービジュアルの画像になってしまうので、次回は記事ごとにtwitter:imageを変更できるようにしてみたいと思います。

Let's Encryptの証明書を更新する

動機

前回Let’s Encryptを使ったSSL証明書の作成を行いました。そのときに、2020年8月4日には有効期限が切れてしまうようですと言っていたにもかかわらず更新をするのを忘れていました…

なので、今回は更新作業を行いたいと思います。

SSL証明書の更新作業

前回も記載していましたが、

1
2
3
Your cert will expire on 2020-08-04. To obtain a new or tweaked
version of this certificate in the future, simply run certbot again
with the "certonly" option.

ということなので、certonlyオプションをつけて実行したいと思います。

1
2
3
4
5
6
7
8
9
10
$ sudo certbot certonly --nginx
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator nginx, Installer nginx

Which names would you like to activate HTTPS for?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: book-reviews.blog
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel):

一つしかドメインがないですが、選択する必要があるので1を入力してエンターキーを押します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1: book-reviews.blog
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel): 1
Cert is due for renewal, auto-renewing...
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for book-reviews.blog
Waiting for verification...
Challenge failed for domain book-reviews.blog
http-01 challenge for book-reviews.blog
Cleaning up challenges
Some challenges have failed.

IMPORTANT NOTES:
- The following errors were reported by the server:

Domain: book-reviews.blog
Type: dns
Detail: During secondary validation: DNS problem: networking error
looking up A for book-reviews.blog

なぜか失敗してしまっているようです。原因も記載されていますね。

1
Detail: During secondary validation: DNS problem: networking error looking up A for book-reviews.blog

ドメインの名前解決中にネットワークエラーが発生したという感じでしょうか…

原因が不明なので再度実行してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
$ sudo certbot certonly --nginx
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator nginx, Installer nginx

Which names would you like to activate HTTPS for?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: book-reviews.blog
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel): 1
Cert is due for renewal, auto-renewing...
Renewing an existing certificate
Performing the following challenges:
http-01 challenge for book-reviews.blog
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
/etc/letsencrypt/live/book-reviews.blog/fullchain.pem
Your key file has been saved at:
/etc/letsencrypt/live/book-reviews.blog/privkey.pem
Your cert will expire on 2020-11-03. To obtain a new or tweaked
version of this certificate in the future, simply run certbot
again. To non-interactively renew *all* of your certificates, run
"certbot renew"
- If you like Certbot, please consider supporting our work by:

Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
Donating to EFF: https://eff.org/donate-le

今回はなぜかうまく行きました。

動作確認

ブラウザでアクセスしてみます。すると、まだ「保護されていない通信」として警告が出ています。証明書を確認すると、有効期限が更新されていませんでした。

証明書を更新したらnginxの再起動が必要ですね。再起動します

1
$ sudo systemctl restart nginx

証明書が更新され、有効期限が2020年11月4日になりました。これで証明書の更新は終わりです、が、なぜかChromeで見たときの「保護されていない通信」という警告はまだ残っています…なぜなのでしょうか…

原因調査

こちらに調査方法が詳しく記載されていました。

デベロッパーツールを立ち上げ、Securityを選択します。
Security overviewを確認すると、証明書やコネクションに関してはグリーンになっていますが、Resourcesがレッドになっています。
メッセージを見ると

1
2
Resources - active content with certificate errors
You have recently allowed content loaded with certificate errors (such as scripts or iframes) to run on this site.

ということでした。証明書を更新する前の、有効期限が切れている状態でサイトへのアクセスを許可したために警告が消えないようです。

ということは、証明書の更新とは関係ありませんでした。試しに別のブラウザで表示したところ問題ありませんでした。

まとめ

  • Let’s Encryptの証明書更新を行うにはcertonlyオプションをつけて実行する
  • 更新した証明書を反映するにはWebサーバの再起動が必要
  • 証明書の期限が切れた状態で、サイトへのアクセスを許可すると、証明書更新後も「保護されていない通信」が解除されない。

次回また2020年11月に行います。次はもっとスムーズにできると思います。

unicornがどのRubyバージョンで動作しているかを調べる

背景

以前、Railsアプリケーションが動作している環境のRubyのバージョンアップを行いました。Rubyのバージョンは正しく上がっているようでしたが、Railsアプリケーションの動作が不安定になってしまいました…その時の調査のログです。

原因

結果的にはunicornが旧Rubyバージョンで動作していることが原因でした。unicornがどのRubyバージョンで動作しているかを確認する方法を記載します。

確認方法

プロセスIDの確認

unicornのmasterプロセスを探します。

1
2
3
4
5
6
7
8
9
$ ps -ef | grep unicorn
user 1797 1 0 Jul29 ? 00:00:06 unicorn master -c /var/www/user-cms/current/config/unicorn/production.rb -E deployment -D
user 1840 1797 8 Jul29 ? 02:42:10 unicorn worker[0] -c /var/www/user-cms/current/config/unicorn/production.rb -E deployment -D
user 1847 1797 8 Jul29 ? 02:40:24 unicorn worker[1] -c /var/www/user-cms/current/config/unicorn/production.rb -E deployment -D
user 1857 1797 8 Jul29 ? 02:40:50 unicorn worker[2] -c /var/www/user-cms/current/config/unicorn/production.rb -E deployment -D
user 1872 1797 8 Jul29 ? 02:38:32 unicorn worker[3] -c /var/www/user-cms/current/config/unicorn/production.rb -E deployment -D
user 1894 1797 8 Jul29 ? 02:39:10 unicorn worker[4] -c /var/www/user-cms/current/config/unicorn/production.rb -E deployment -D
user 1909 1797 7 Jul29 ? 02:37:28 unicorn worker[5] -c /var/www/user-cms/current/config/unicorn/production.rb -E deployment -D
21235 42323 42237 0 00:47 pts/3 00:00:00 grep unicorn

masterプロセスのプロセスIDは1797とわかります。
次にrootになって、/proc/プロセスIDのディレクトリを確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
$ sudo su -
# ls -l /proc/1797/
合計 0
dr-xr-xr-x 2 user user 0 7月 31 00:47 2020 attr
-rw-r--r-- 1 user user 0 7月 31 00:47 2020 autogroup
-r-------- 1 user user 0 7月 31 00:47 2020 auxv
-r--r--r-- 1 user user 0 7月 31 00:47 2020 cgroup
--w------- 1 user user 0 7月 31 00:47 2020 clear_refs
-r--r--r-- 1 user user 0 7月 31 00:46 2020 cmdline
-rw-r--r-- 1 user user 0 7月 31 00:47 2020 comm
-rw-r--r-- 1 user user 0 7月 31 00:47 2020 coredump_filter
-r--r--r-- 1 user user 0 7月 31 00:47 2020 cpuset
lrwxrwxrwx 1 user user 0 7月 31 00:47 2020 cwd -> /var/www/user-cms/releases/20200729065345
-r-------- 1 user user 0 7月 31 00:47 2020 environ
lrwxrwxrwx 1 user user 0 7月 31 00:47 2020 exe -> /usr/local/rbenv/versions/2.4.10/bin/ruby
dr-x------ 2 user user 0 7月 31 00:47 2020 fd
dr-x------ 2 user user 0 7月 31 00:47 2020 fdinfo
-r-------- 1 user user 0 7月 31 00:47 2020 io
-rw------- 1 user user 0 7月 31 00:47 2020 limits
-rw-r--r-- 1 user user 0 7月 31 00:47 2020 loginuid
-r--r--r-- 1 user user 0 7月 31 00:47 2020 maps
-rw------- 1 user user 0 7月 31 00:47 2020 mem
-r--r--r-- 1 user user 0 7月 31 00:47 2020 mountinfo
-r--r--r-- 1 user user 0 7月 31 00:47 2020 mounts
-r-------- 1 user user 0 7月 31 00:47 2020 mountstats
dr-xr-xr-x 6 user user 0 7月 31 00:47 2020 net
dr-x--x--x 2 user user 0 7月 31 00:47 2020 ns
-r--r--r-- 1 user user 0 7月 31 00:47 2020 numa_maps
-rw-r--r-- 1 user user 0 7月 31 00:47 2020 oom_adj
-r--r--r-- 1 user user 0 7月 31 00:47 2020 oom_score
-rw-r--r-- 1 user user 0 7月 31 00:47 2020 oom_score_adj
-r--r--r-- 1 user user 0 7月 31 00:47 2020 pagemap
-r--r--r-- 1 user user 0 7月 31 00:47 2020 personality
lrwxrwxrwx 1 user user 0 7月 31 00:47 2020 root -> /
-rw-r--r-- 1 user user 0 7月 31 00:47 2020 sched
-r--r--r-- 1 user user 0 7月 31 00:47 2020 schedstat
-r--r--r-- 1 user user 0 7月 31 00:47 2020 sessionid
-r--r--r-- 1 user user 0 7月 31 00:47 2020 smaps
-r--r--r-- 1 user user 0 7月 31 00:47 2020 stack
-r--r--r-- 1 user user 0 7月 31 00:46 2020 stat
-r--r--r-- 1 user user 0 7月 31 00:47 2020 statm
-r--r--r-- 1 user user 0 7月 30 22:38 2020 status
-r--r--r-- 1 user user 0 7月 31 00:47 2020 syscall
dr-xr-xr-x 5 user user 0 7月 31 00:47 2020 task
-r--r--r-- 1 user user 0 7月 31 00:47 2020 wchan

exeファイルのリンク先を確認すると、/usr/local/rbenv/versions/2.4.10/bin/rubyとなっており、Rubyの実行ファイルになっています。この場合だとバージョンは2.4.10ですね。今回はパスでバージョンがわかりますが、パスからわからない場合は、exeのリンク先の実行ファイルで-vオプションを実行すればバージョンがわかります。

おまけ

デプロイを行うのにcapistranoを使っているのですが、通常のデプロイシーケンスでは、unicornはworkerが再起動されるだけで、masterプロセスは再起動されませんでした。Rubyのバージョン変更を行った場合はmasterプロセスの停止・起動が必要となります。

自分の環境では以下のコマンドでunicornのmasterプロセスの再起動が行えました。

1
2
3
$ bundle exec cap production deploy:stop
$ bundle exec cap production deploy:start
(productionは環境名)

最初のコマンドでunicornを停止し、次のコマンドで起動しています。Rubyのバージョンを変更した場合は、masterプロセスの再起動が必要ということを覚えておきます。

この辺りを学ぶにはなるほどUNIXプロセスという本がとても役に立ちそうです。読み始めているのですが、とても楽しく読めています。機会があれば本の内容も紹介したいと思います。

ECSでタスクが起動しない時の調査方法

背景

オンプレミス環境からAWS ECSへ移行しようというプロジェクトを行っております。環境はnginx + Railsです。

Docker Desktop for Macでdocker networkを作成しnginxとrailsでコンテナ間通信ができるところまでは確認できたので、そのコンテナイメージをECRにpushし、タスク定義まで行いました。

ECSクラスターの作成を行い、作成したタスク定義を実行したところ起動できなかったのでその時の調査方法を記載します。

ECSインスタンスにログイン

起動しない原因を調査するにあたってまずはログを確認したいのですが、ログの確認方法がわかっていません…なので、本来もっと良い方法があると思うのですがよくわかっていないので、一旦ECSインスタンスにSSHログインできるようにしました。

2020/12/9追記

ログの確認方法

ECSクラスターを選択、下のタブのタスクをクリックします。

タスクの一覧が表示されます。表示されない場合は、コンテナがすでに止まってしまっている可能性が高いです。その場合は、必要なタスクのステータスをStoppedにすると表示されると思います。タスクのIDのリンクをクリックします。するとタスクの詳細ページに遷移します。

画面上部にタブがあり、Logsタブをクリックします。ログを表示するコンテナを選択しますというメッセージの横にセレクトボックスがあるので、ログを表示するコンテナを選択します。CloudWatchに出力する(logDriverがawslog)設定になっているコンテナが選択できます。

選択すると下にログが表示されます。エラーなどコンテナが起動できなかった理由がわかると思います。

ECSインスタンスにログインするための設定

ECSインスタンスにログインするためには以下の設定が必要です。

  • クラスターテンプレートの選択でEC2 Linux + ネットワーキングを選択する
  • クラスターの設定 -> インスタンスの設定でキーペアを指定する
  • クラスターの設定 -> ネットワーキングでパブリックサブネットを指定する
  • クラスターの設定 -> ネットワーキングでAuto assign public IPをEnabledにする
    • サブネットの設定でパブリック IPv4 アドレスの自動がはいになっていればUse subnet settingでも大丈夫です
  • クラスターの設定 -> セキュリティグループでSSHを通しているセキュリティグループを選択する
    • デフォルトのセキュリティグループではNG

以上の設定でクラスターを作成します。

ECSインスタンスの起動確認

先ほど作成したクラスターを選択して、ECS インスタンスタブをクリックすると、作成したクラスターのECSインスタンスが表示されます。ECS インスタンスのリンクをクリックすると、EC2の確認ができます。

ECSインスタンスにSSHする

確認したEC2のパブリックDNS、もしくはパブリックIPにクラスター作成時に指定したキーペアを使ってログインします。

コンテナのログを確認

早速コンテナの状況を確認します。docker ps -aをすると、なにかの原因でステータスがExitedになっていました。

コンテナのログの確認にはdocker logsコマンドを利用します。

1
2
3
4
5
6
7
8
9
$ docker logs コンテナID
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: error: /etc/nginx/conf.d/default.conf is not a file or does not exist
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2020/07/22 07:55:16 [emerg] 1#1: host not found in upstream "rails:3000" in /etc/nginx/conf.d/rails.conf:2
nginx: [emerg] host not found in upstream "rails:3000" in /etc/nginx/conf.d/rails.conf:2

nginxの設定がDocker Desktopで動作確認した時のそのままになってしまっているのが原因のようです。(docker networkを作成して動作確認したときに、Rails用コンテナをrailsという名前で起動していたため)

今回、タスク定義のネットワークモードをawsvpcにしているので、コンテナ間通信はlocalhost指定で行えます。こちらを参考にしました。

ですので、nginxのupstreamの指定をlocalhost:3000として、コンテナをbuild、pushしたら無事タスクが起動しました。

まとめ

なにか起こった時の調査にログは欠かせません。まずはログを確認するようにしたいです。
今回のログの確認方法は本来の方法ではないと思います。Fargate使っていたらできないと思いますし…
本来はCloudWatchなど利用して行うのだと思いますが、まだその辺りを理解できていないのでこれから調べていきたいと思います。

※ Container Insightsを使ってみましたが使用しているリソース確認、マップ表示くらいの機能でした。プロダクション環境では重宝するかもしれません。