ALBとECS間がHTTP通信のときのRailsのforce_sslの挙動

SSL終端がALBのとき、Railsのforce_sslがどのように動作するのか疑問に思ったので調べてみました。

こちらの記事を大いに参考にさせていただきました。

ALB + RailsでSSLを有効にする

force_ssl における動作

ALBにhttpsでリクエストするとALBがX-Forwarded-Proto: https ヘッダを追加してRailsにリクエストしてくれます。

HTTP ヘッダーと Application Load Balancer - Elastic Load Balancing

そのヘッダを用いてRackの内部ではSSL判定がなされています。

https://github.com/rack/rack/blob/3-0-stable/lib/rack/request.rb#L410

      def ssl?
        scheme == 'https' || scheme == 'wss'
      end

https://github.com/rack/rack/blob/3-0-stable/lib/rack/request.rb#L249

      def scheme
        if get_header(HTTPS) == 'on'
          'https'
        elsif get_header(HTTP_X_FORWARDED_SSL) == 'on'
          'https'
        elsif forwarded_scheme
          forwarded_scheme
        else
          get_header(RACK_URL_SCHEME)
        end
      end

https://github.com/rack/rack/blob/3-0-stable/lib/rack/request.rb#L713

      def forwarded_scheme
        forwarded_priority.each do |type|
          case type
          when :forwarded
            if (forwarded_proto = get_http_forwarded(:proto)) &&
               (scheme = allowed_scheme(forwarded_proto.last))
              return scheme
            end
          when :x_forwarded
            x_forwarded_proto_priority.each do |x_type|
              if header = FORWARDED_SCHEME_HEADERS[x_type]
                split_header(get_header(header)).reverse_each do |scheme|
                  if allowed_scheme(scheme)
                    return scheme
                  end
                end
              end
            end
          end
        end

        nil
      end

ヘルスチェックでリダイレクトを回避する

ALB + RailsでSSLを有効にする

にもあるように、HTTPで通信かつステータス200で成功判定している場合はforce_sslを有効化するとリダイレクトしてしまうので、 ELB-HealthCheckerをUAに含む場合リダイレクトしない設定をすると良いようです。

request specをどう書くべきか

下の記事のように、ActionDispatch::Integration::RequestHelpersのヘルパーメソッドをオーバーライドするといいみたいです。

Request Specでデフォルトのヘッダーを設定する | Ver.1.0

オプションは他にもあるので、下記のようにpathとheader以外は可変長引数にした方がいいです。

module RequestSpecHelper
  [:get, :post, :patch, :put, :delete, :head].each do |name|
    define_method(name) do |path, headers: {}, **args|
      super(path, headers: headers.merge({ "X-Forwarded-Proto": "https" }), **args)
    end
  end
end

大抵のケースは上記でカバーできる一方で、Specが特定条件下で使用しているCookieJarがSecure Cookieをサポートしていないため、Secure Cookieが絡むテストが一部NGになることがあります。このようなケースを考慮するとテスト環境ではforce_ssl: false にした方が無難そうです。

Signed cookies not available in controller specs - rspec-rails 3.5.0, rails 5.0 · Issue #1658 · rspec/rspec-rails · GitHub

Docker + GoでGitHub Actionsを作ったらWORKDIRが効かなくてハマった

GitHub Actions Advent Calendar 2022 の8日目が空いていたのでお邪魔します。

ECRに指定したタグのDocker imageが存在するかチェックするActionを作りました。

github.com

imageが存在しない場合はビルドしてからデプロイ、する場合はビルドをスキップしてデプロイだけ行う、というケースに対応することを想定しています。

aws-cliで確認しようと思うとレスポンスをjqで処理したり、利用するAPIによってはexit 1したりするのが面倒なので、その辺を考えなくていいのがメリットです。

利用法(2022/12/12時点)

- name: Check if Docker image with tag exists in ECR
  uses: mnmandahalf/check-ecr-image-exists@v0.1.3

内部ではaws-sdk-goを利用しているのですが、いざMarketplaceに公開した後にアクションを利用すると、entrypointで実行しているスクリプト内でのgo run /app/ecr.go の実行時に同階層かつカレントディレクトリにあるはずのgo.modが見つからず、モジュールがインポートできないという事象が発生しました。

/app/ecr.go:10:2: no required module provides package github.com/aws/aws-sdk-go-v2/aws: go.mod file not found in current directory or any parent directory; see 'go help modules'
/app/ecr.go:11:2: no required module provides package github.com/aws/aws-sdk-go-v2/config: go.mod file not found in current directory or any parent directory; see 'go help modules'
/app/ecr.go:12:2: no required module provides package github.com/aws/aws-sdk-go-v2/credentials: go.mod file not found in current directory or any parent directory; see 'go help modules'
/app/ecr.go:13:2: no required module provides package github.com/aws/aws-sdk-go-v2/service/ecr: go.mod file not found in current directory or any parent directory; see 'go help modules'

結論から言うとWORKDIR命令の結果いるはずの場所にいないのが原因のようでした。

https://docs.github.com/ja/actions/creating-actions/dockerfile-support-for-github-actions#workdir

GitHub では、ワーキング ディレクトリのパスを環境変数 GITHUB_WORKSPACE に設定します。 Dockerfile では、WORKDIR 命令を使用しないことをお勧めします。 アクションが実行される前に、GitHub では、GITHUB_WORKSPACE ディレクトリを、Docker イメージ内にあったその場所になにがあってもその上にマウントし、GITHUB_WORKSPACE をワーキング ディレクトリとして設定します。 詳細については、"Docker ドキュメントの環境変数の使用" に関するページと WORKDIR リファレンスを参照してください。

ワーキングディレクトリが変わってしまっているので、結果的に cd /app && go run ecr.go することで解決しました。

手元でdocker buildしてテストしてもワーキングディレクトリがWORKDIRになっている状態なのでテストにならないのが罠だなと思いました。

皆様もDockerでActionsを作成するときはお気をつけください。

embulk-input-postgresqlでTimestampが9時間巻き戻ってしまったとき

embulk-input-postgresql v0.13.0で、Timestamp without Timezoneなカラムの値が9時間巻き戻るケースを発見した。

PostgreSQLサーバのTZがUTC、実行ホストがJSTの場合に巻き戻る模様。

実験

サーバのTZがUTCの場合

admin=# show timezone;
 TimeZone
----------
 Etc/UTC
(1 row)

mydatabase=# select * from mytable;
 id |  name  |         updated_at
----+--------+----------------------------
  1 | myname | 2022-07-30 17:23:28.698386
(1 row)

liquidファイルは以下のようなものを用意しておく

in:
  type: postgresql
  host: 127.0.0.1
  user: admin
  password: admin 
  database: mydatabase
  table: mytable
結果
$ embulk preview test.yml.liquid

# プレビュー
+---------+-------------+--------------------------------+
| id:long | name:string |           updated_at:timestamp |
+---------+-------------+--------------------------------+
|       1 |      myname | 2022-07-30 08:23:28.698386 UTC |
+---------+-------------+--------------------------------+

サーバのTZがAsia/Tokyoの場合

mydatabase=# show timezone;
  TimeZone
------------
 Asia/Tokyo
(1 row)

mydatabase=# select * from mytable;
 id |  name  |        updated_at
----+--------+---------------------------
  1 | myname | 2022-07-31 01:50:28.73504
(1 row)
結果
$ embulk preview test.yml.liquid

# プレビュー
+---------+-------------+--------------------------------+
| id:long | name:string |           updated_at:timestamp |
+---------+-------------+--------------------------------+
|       1 |      myname | 2022-07-30 16:50:28.735040 UTC |
+---------+-------------+--------------------------------+

対処法1

サーバのTZがUTCの場合はTZ=UTCでembulk previewを実行する

PostgreSQLと実行ホストのTZを共にUTCで揃えると巻き戻らない。

$ TZ=UTC embulk preview test.liquid.yml

# プレビュー
+---------+-------------+--------------------------------+
| id:long | name:string |           updated_at:timestamp |
+---------+-------------+--------------------------------+
|       1 |      myname | 2022-07-30 17:23:28.698386 UTC |
+---------+-------------+--------------------------------+

対処法2

サーバのTZがJSTの場合はdefault_column_options でフォーマットすることで対応する

in:
  type: postgresql
  host: 127.0.0.1
  user: admin
  password: admin 
  database: mydatabase
  table: mytable
  default_column_options:
    TIMESTAMP: { type: string, timestamp_format: "%Y/%m/%d %H:%M:%S", timezone: "+0900"}

結果

# プレビュー
+---------+-------------+---------------------+
| id:long | name:string |   updated_at:string |
+---------+-------------+---------------------+
|       1 |      myname | 2022/07/31 01:50:28 |
+---------+-------------+---------------------+

参考

こちらのツイートのツリーを参考にさせていただきました。

https://twitter.com/hiroysato/status/1281043865667944448

default_column_options については以下に記載あり。

embulk-input-jdbc/embulk-input-postgresql at master · embulk/embulk-input-jdbc · GitHub

macOSでgradleのCould not find tools.jar. エラー

macOS Montereyでgradle buildを実行するとCould not find tools.jar.というエラーになる。

JDK9からtools.jarが以前のように同包されなくなったことが原因のよう。

過去のバージョンのJDKを利用するか、以下のようにtools.jarをコピーするとひとまず対処することができる。

対処法

①下記からOracle Linux 7.6 x64 Java Development Kit をDLする

jdk.java.net

② DLしたtools.jarをコピーする

sudo cp ~/Downloads/java-se-8u41-ri/lib/tools.jar /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/lib

参考

gradle Could not find tools.jar. エラー対策 - Qiita

MacのBig Surになってビルドエラー | 株式会社オルタ

GitHub ActionsでGCRにDocker image pushするIAMポリシーの設定

参考: 

サービスアカウントキーを用いずにGitHub ActionsからGoogle Cloudと認証する | DevelopersIO

こちらの記事に則ってセットアップし、GCRに docker pushのタイミングで

denied: Token exchange failed for project '${PROJECT_ID}'. Caller does not have permission 'storage.buckets.get'. To configure permissions, follow instructions at: https://cloud.google.com/container-registry/docs/access-control

というエラーが発生した。

サービスアカウントにstorage.buckets.getの権限がないというメッセージだったので、適当そうなロールを探してみた。

IAM を使用したアクセス制御  |  Container Registry のドキュメント  |  Google Cloud

gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member "serviceAccount:${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/storage.legacyBucketWriter"
(gcloud.projects.add-iam-policy-binding) INVALID_ARGUMENT: Role (roles/storage.legacyBucketWriter) does not exist in the resource's hierarchy.

そんなロールは存在しないと言われたので、stack overflowを参考に試しにroles/storage.adminに変更してみた。

kubernetes - Google Cloud Service Account with 'roles/container.admin' - Stack Overflow

gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member "serviceAccount:${SERVICE_ACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role="roles/storage.admin"

ロールの紐付けも成功し、GHAからのpushも成功。

ただ、GoogleのIAM公式ドキュメントstorage.buckets.get権限はroles/storage.adminに含まれないですよと書いてあるので、これは一体...?という状態。

注: イメージを push するには、storage.buckets.get 権限に加え、オブジェクトの読み取り権限と書き込み権限が必要です。Storage オブジェクト管理者ロールには、storage.buckets.get 権限が含まれていませんが、Storage レガシー バケット書き込みロールには含まれています。