await をトリガに開始する Promise

fastify の inject 経由で light-my-request を使っていて、ついうっかり次のように書いてしまい、

await fastify
    .inject()
    .get('/')
    .headers({ foo: 'bar' })
    .query({ foo: 'bar' })

おっと .end() が抜けてるじゃん、と思ったのですがこれでも通っており、どういうことかと思って light-my-request のコードを見てみました。

https://github.com/fastify/light-my-request/blob/b7c89c97fd687b53aff3e5a0ddece2e2086ef634/index.js#L169C55-L181

Object.getOwnPropertyNames(Promise.prototype).forEach(method => {
  if (method === 'constructor') return
  Chain.prototype[method] = function (...args) {
    if (!this._promise) {
      if (this._hasInvoked === true) {
        throw new Error(errorMessage)
      }
      this._hasInvoked = true
      this._promise = doInject(this.dispatch, this.option)
    }
    return this._promise[method](...args)
  }
})

なるほど Promise をラップして Promise 関係のメソッドの呼び出しで実行していました。await で .then() が呼ばれるのでその時点で開始されるということですね。

自前で実装するとこんな感じでしょうか、

import { setTimeout } from "timers/promises";

class MyPromise<T> implements PromiseLike<T> {
    private promise: Promise<T> | null = null;

    constructor(
        private readonly executor: ConstructorParameters<typeof Promise<T>>[0],
    ) {}

    then<TResult1 = T, TResult2 = never>(
        onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
        onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
    ): PromiseLike<TResult1 | TResult2> {
        if (!this.promise) {
            this.promise = new Promise(this.executor);
        }
        return this.promise.then(onfulfilled, onrejected);
    }
}

(async () => {
    const p = new MyPromise((resolve)=>{
        console.log("a");
        process.nextTick(() => {
            resolve(null);
        });
    })
    await setTimeout(100);
    console.log("z");
    await p;
})();

MyPromise の中身は await するまで開始しないので z -> a の順番で表示されます。 素の Promise だと new 時点で中身が開始するので a -> z の順番になります。

Terraform のいろいろ

Terraform の雑多なメモ。

有名どころのベストプラクティス

常にこの通りにしているわけではないですけど、参考までに。

モジュールを使う

かつて(v0.11 ごろ?)、モジュールは次のようにシンボリックリンクが作成される前提になっていたことがあって、

$ terraform version | head -1
Terraform v0.11.15

$ ll .terraform/modules/
total 12
drwxr-xr-x 2 user user 4096 2023-08-12 03:10 ./
drwxr-xr-x 3 user user 4096 2023-08-12 03:09 ../
lrwxrwxrwx 1 user user   48 2023-08-12 03:10 8175c15861ae98d01b480045c6f45ff8 -> /path/to/terraform/modules/sub/
-rw-r--r-- 1 user user  273 2023-08-12 03:10 modules.json

当時、以下のように Windows の共有フォルダを cifs でマウントした Linux サーバ上で作業していた身としては非常に相性が悪いものでした。cifs 上ではシンボリックリンクが作成できないためです。いちおう、次のような方法で cifs でマウントした中の一部のディレクトリに Linux 上の別ディレクトリをマウントさせることは出来ますが・・何らかの事情でこの方法はあまり使っていませんでした(理由は忘れた)。

ので、基本的には terraform でモジュールは使わずにフラットに 1 ディレクトリに詰め込んでいました。

v0.12 からはこれが改善されていてシンボリックリンクは使われなくなったようです(今は WSL2 の ext4 上で作業しているので cifs の制限はもう関係ないですが)。

$ terraform version | head -1
Terraform v0.12.31

$ ll .terraform/modules/
total 12
drwxr-xr-x 2 ngyuki ngyuki 4096 2023-08-12 03:15 ./
drwxr-xr-x 3 ngyuki ngyuki 4096 2023-08-12 03:15 ../
-rw-r--r-- 1 ngyuki ngyuki  105 2023-08-12 03:15 modules.json

ので、今は適宜モジュールに分割しています。記述量は増えますが同じようなリソースを複数作るときに DRY にできますし、リソース識別子も名前の衝突をあまり気にせずに「とりあえず main でいいか」と思えます。

ちなみに terraform のオフィシャルの aws 関係のモジュールだと this が良く使われているようです。

terraform-aws-modules/terraform-aws-vpc/main.tf

これをまねて一時的に this を使ってたこともあるのですが、今はやっぱり main で良いかなと思っています。

terraform workspace は使わずディレクトリで分ける

Terraform で本番やステージングなどの変数定義

基本的に「案.5 Workspaces を使わずにディレクトリで分ける」一択で良いと思います。↑にも記載の通り workspace は次の2点が微妙だと思ってます。

  • workspace の切り替えに terraform workspace select のひと手間が必要
  • 環境によってあったりなかったりするリソースで count が必要で見通しが悪い

モジュールを使わずに 1 ディレクトリに詰め込んでいたときは環境をディレクトリで分けてしまうと tf ファイルの重複が酷いことになってしまいますが、モジュールを使うならモジュールで共通化できるので重複は最小限にできます。

環境ごとに重複するファイル

workspace は使わずにディレクトリで tfstate を分ける場合、どうしてもルートモジュールに必要な terraform や provider や module などのディレクティブは重複します。次のようなツールを使うという手もありますが、既存の構成からの移行が簡単ではなさそうだったので使っていません。

ので、どうしても重複を除きたいならシンボリックリンクで次のようにしています。

envs/
    common/
        main.tf
        provider.tf
        terraform.tf
    prd/
        main.tf      -> ../common/main.tf
        provider.tf  -> ../common/provider.tf
        terraform.tf -> ../common/terraform.tf
        backend.tf
        locals.tf
    stg/
        main.tf      -> ../common/main.tf
        provider.tf  -> ../common/provider.tf
        terraform.tf -> ../common/terraform.tf
        backend.tf
        locals.tf
    dev/
        main.tf      -> ../common/main.tf
        provider.tf  -> ../common/provider.tf
        terraform.tf -> ../common/terraform.tf
        backend.tf
        locals.tf

ただ、IaC はベタに書いても良いように思うので、今はこれぐらいならベタに書いています。

モジュールをリソースの種類(AWS のサービス)ごとに分けようとしない

これはもともとそんなことをしていたわたしが変だと思うので一般的なことではないと思いますが・・

モジュールを使わずにフラットに 1 ディレクトリに詰め込んでいた時は AWS のサービスごとに tf ファイルを設けていました(iam.tf とか)。それをそのままモジュールに移した結果、モジュールも AWS のサービスごとに分けようとしていました。ただ、それだとモジュール間の依存関係が双方向に入り乱れて非常に判りずらいです。

ので、関連性の強いリソースが1つのモジュールに収まるように意識するべきです。例えば次のようなモジュールは AWS サービスを超えてセットになるのが自然です(時と場合による)。

  • CloudFront 関係
    • Route53 ALIAS レコード(CF用)
    • ACM 証明書
    • Route53 CNAME レコード(ACM検証用)
  • ELB 関係
    • Route53 ALIAS レコード(CF用)
    • ACM 証明書
    • Route53 CNAME レコード(ACM検証用)
  • Lambda 関係
    • Lambda に設定する IAM Role
    • Lambda のトリガとなる SNS サブスクライブ

よく考えれば普通のことですね。。。

また、ある IAM Role にあるリソースへの許可のポリシーを付与したいとき、IAM Role を作成するモジュールに許可したいリソースの arn を与えるのではなく、リソースを作成するモジュールに IAM Role を与える方がスッキリします。

// IAM Role のリソース
resource "aws_iam_role" "main" {
    // ...snip...
}

output "role" {
    value = aws_iam_role.main.id
}
// SQS のリソース
variable "role" {
    // ...snip...
}

resource "aws_sqs_queue" "main" {
    // ...snip...
}

resource "aws_iam_role_policy" "main" {
    role = var.role
    // ...snip...
        "Resource": aws_sqs_queue.main.arn,
    // ...snip...
}

アプリで使用する IAM Role(EC2 インスタンスや ECS Service に付与する IAM Role)を作成するモジュールは色々な AWS リソースへのアクセスを許可するために大量の引数を持ちがちですが、この方法ならそれがありません。アクセスされる側のリソースで「アプリからアクセスされる」と宣言するニュアンスです。

一方で IAM 関係はセキュリティやら何やらでいろいろ言われることもあるので(変更時に申請が必要だったり)、一か所にまとまっていた方が便利なこともあって、これも時と場合によりそうです。

適宜 tfstate を分ける

次のようなリソースは無理やり1つの環境のディレクトリに入れようとはせずに適宜ディレクトリを分けて tfstate を分ける方が良さそうです。

複数環境で共通するリソース

ありがちなのが Route53 ゾーン。また、プロジェクトによっては prd 以外(stg と dev など)で VPC が共通ということもあると思います。そのようなリソースを代表となる1つの環境で作成し、その他の環境では data で参照する、という構成にもできると思います。

# envs/prd/main.tf
module "dns" {
    source    = "../../modules/dns"
    create    = true
    zone_name = local.zone_name
}
# envs/stg/main.tf
module "dns" {
    source    = "../../modules/dns"
    create    = false
    zone_name = local.zone_name
}
# modules/dns/main.tf
resource "aws_route53_zone" "main" {
    count = var.create ? 1 : 0
    name  = var.zone_name
}

data "aws_route53_zone" "main" {
    count = var.create ? 0 : 1
    name  = var.zone_name
}

output "zone_id" {
    value = var.create ? aws_route53_zone.main[0].id : data.aws_route53_zone.main[0].id
}

変に複雑で見通しが悪くなるので素直に別のディレクトリで tfstate を分ける方が良いです。例えば次のように envs と同じ階層に base を設けてその中に共有するリソースのためのディレクトリを設けたりしていました。

base/
    aaa/
    bbb/
envs/
    prd/
    stg/
    dev/

もしくは次のように envs/ に入れても良いかもしれません。

envs/
    base-aaa/
    base-bbb/
    {project}-prd/
    {project}-stg/
    {project}-dev/

更新頻度が異なる

機能的な改修とは無関係に頻繁に更新が必要になるようなリソースは他のリソースとは tfstate を分けておくと良いです。例えば WAF で IP 制限をしている都合でシステムの構成変更とは無関係に変更が頻繁に入ったり、CloudWatch Alarm も運用中に調整が入ることもしばしばなので、次のようにそれらを分けたりです。

envs/
    {project}-prd/
    {project}-prd-waf/
    {project}-prd-alarm/
    {project}-stg/
    {project}-stg-waf/
    {project}-stg-alarm/
    {project}-dev/
    {project}-dev-waf/
    {project}-dev-alarm/

巷ではもっと細かく、例えばストレージ系(RDS とかそういうの)とそれ以外で分けたりもあるのでしょうか。

aws_vpc_security_group_ingress_rule や aws_route を使う

aws_security_group の ingress や egress、および aws_route_table の route は、インラインでルールを記述できて便利ですが、基本的には使用せず、代わりに aws_vpc_security_group_ingress_rule や aws_route を使用します。

aws_security_group や aws_route_table のインラインでルールを記述していると、ルールの変更時に「全削除→全追加」のような plan が表示されます。

実際に呼ばれている API を CloudTrail で確認したところ plan の表示とは異なり差分の分しか変更されていないのですが、本番系の環境でこの plan は心理的な負荷が大きすぎるので、ルール単位のリソースである aws_vpc_security_group_ingress_rule や aws_route を使っておいた方が気が楽です。

moved や import は別ファイルに分ける

リファクタリングのために moved ブロックや import ブロックをつかうことがありますが、moved.tf や import.tf のように別ファイルに分けて記述しています。後で不要になってからファイルごと削除するためです。

default_tags に何か入れておく

provider の default_tags にはとりあえず何か入れておくと良いと思います。

provider "aws" {
  region              = local.region
  allowed_account_ids = local.allowed_account_ids
  default_tags {
    tags = {
      Project    = local.project
      Env        = local.env
      Repository = local.repository
      ManagedBy  = "terraform"
    }
  }
}

また、例えば検証とかでちょっと何か作るときとかも、共用の AWS アカウントならとりあえず名前でも入れておくと良いと思います。「これ消していいやつー?」を誰に聞けばいいかわかりやすいので。

provider "aws" {
  default_tags {
    tags = {
      Author = "ngyuki"
    }
  }
}

さいごに

よく見たら1年以上前に書いた後、しばらく寝かせておこうとして1年以上寝かせてた記事でした。腐ってはいないと思います。

ulimit(rlimit) でコアダンプ抑止できるのは core_pattern でパイプしていないときだけ

とある事情で知ったコアダンプの現状。

よくコアダンプの抑止のために ulimit -c 0 とか、systemd ならユニットファイルで↓などにする例がみられますが、

[Service]
LimitCORE=0

これが有効に働くのは /proc/sys/kernel/core_pattern でコアファイルをプログラムにパイプしていないときだけです。 今日日の systemd な環境であれば /proc/sys/kernel/core_pattern は次のようにパイプされていたりするし、

|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h %e

abrtd でコアダンプの発生をレポートしていると次のようになっていたりするので、

|/usr/libexec/abrt-hook-ccpp %s %c %p %u %g %t e %P %I %h

ulimit(rlimit) でコアダンプを抑止できる、というのは昔の話のようです。

昔の話?

逆に昔は /proc/sys/kernel/core_pattern が core とかになっていて /proc/sys/kernel/core_uses_pid の設定も反映された結果、コアダンプするとカレントディレクトリにコアファイルが pid 付きで吐き出されるため、知らないうちにコアファイルがいろんなディレクトリにまき散らされてディスクフル、などということになりかねないのでコアダンプはよほどのことが無い限り無効化するもの、という話があったようです。ただ、近年は systemd なり abrtd なりでコアファイルの行方がハンドリングされており、圧縮されて所定のディレクトリに保管されるため生の無圧縮コアファイルと比べればそれほどサイズは喰わないし、systemd なら systemd-tmpfiles で3日で消えるし、abrtd はよくわかりませんでしたが、今日日はディスクフルを恐れてコアダンプを無効化するような必要は無いようです。

おまけ

Apache の子プロセスはセグってもコアダンプしないようです。

https://httpd.apache.org/docs/2.4/en/mod/mpm_common.html#CoreDumpDirectory If Apache httpd starts as root and switches to another user, the Linux kernel disables core dumps even if the directory is writable for the process. Apache httpd (2.0.46 and later) reenables core dumps on Linux 2.4 and beyond, but only if you explicitly configure a CoreDumpDirectory.

Apache が、というよりは uid を変更するようなプログラムは素ではコアダンプしないようになっているらしいです。

参考: ユーザIDを変更するプログラムのcoreダンプ - Journal InTime(2014-02-11)

AWS で Windows Server を触るメモ

Windows Server なんて全然わからないけれども急に AWS で触る必要が出てきたので最低限の備忘メモ。

  • Session Manager では cmd.exe かなにかのコマンドラインのみアクセスできる
    • このときユーザーは ssm-user で、素では Administrators に属するのでなんでもできる
    • パスワードは設定されていないので RDP にはこのユーザーは使えない
  • Fleet Manager で Web ブラウザから SSM Agent 経由で RDP できる
    • Linux でいうところの SSH するのと同じ雰囲気?
    • サーバ側にローカルアカウント&パスワードが必要
    • 後述の通り Administrator のパスワードはキーペアで復号できるのでそれでログイン出来る
    • AD に参加していれば AD にアカウントでもいいのかもしれないけれどもその辺はよくわからない
  • インスタンス開始時に Administrator のパスワードはランダムで設定される
  • インスタンスを AMI 化するときに Sysprep しておかないと再インスタンス化時にパスワードがキーペアで復号できなくなる
    • そもそも Sysprep しなければ Administrator のパスワード自体が元のまま
    • Administrator のパスワードを変更せずにマネコンで復号で運用している場合は AMI からのリストア時にログインできなくなって嵌りそう

node-mysql2 の timeout オプションでタイムアウトした次のクエリが前回のタイムアウトしたクエリの終了を待つ

node-mysql2 を弄っていたところ次のようなコードで、1番目のクエリが1ミリ秒でタイムアウトした後、2番目のクエリが1番目のクエリの sleep(10) を待ってから実行される=10秒かかる、という現象がありました。

(async () => {
    const pool = mysql.createPool({ host: "127.0.0.1", port: 3306, user: "user", password: "pass" });

    {
        const conn = await pool.getConnection();
        try {
            // 1ミリ秒でタイムアウト
            const sql = `select sleep(10)`;
            await conn.query({ sql, timeout: 1 });
        } catch (err) {
            console.error(err);
        } finally {
            conn.release();
        }
    }

    {
        const conn = await pool.getConnection();
        try {
            // 10秒待たされる
            const sql = `select 1`;
            const res = await conn.query({ sql });
            console.log(res);
        } catch (err) {
            console.error(err);
        } finally {
            conn.release();
        }
    }

    await pool.end();
})();

これは node-mysql2 のバグ・・というわけでも無いです。この timeout はあくまでもクライアント側で制御しているタイムアウトなので、1番目のクエリがタイムアウトしても MySQL 的にはまだまだクエリ実行中です。そして2番目のクエリのためにプールから取り出された接続が1番目の接続と同じになると、同じ接続上では前回のクエリが終わるまで次のクエリは実行できないので、単なる select 1 が10秒待たされることになります。

クライアント側でタイムアウトしたとき、何かしらの方法でクエリを中断できればいいのですが、MySQL では実行中のクエリを中断する正当な方法は無く、別の接続を使って接続を KILL するぐらいしかありません。

        const conn = await pool.getConnection();
        try {
            const sql = `select sleep(10)`;
            await conn.query({ sql, timeout: 1 });
        } catch (err) {
            console.error(err);
            if (err instanceof Error && "code" in err && err.code === "PROTOCOL_SEQUENCE_TIMEOUT") {
                const conn2 = await pool.getConnection();
                try {
                    // 別の接続を使って KILL する
                    await conn2.query(`KILL ?`, [conn.threadId]);
                } finally {
                    conn2.release();
                    // 切断された接続はプールから削除されるものの、
                    // 非同期なので次のクエリが先に実行される可能性があるため、
                    // release 前に destroy してプールから削除しておく
                    conn.destroy();
                }
            }
        } finally {
            conn.release();
        }

あるいは KILL までしなくても conn.destroy だけで良いかも。これは正しい手続きを行わずに TCP ぶつ切りにするためサーバ側で Aborted connection とかになりますが、プールからも削除されるし接続も切れる・・と思ったのですが、完全に閉じているわけではなくソケットの送信側をシャットダウンしているだけでした、

のでサーバ側で Aborted connection となって切断されるまでは生きたままなので、node のプロセスの終了時に待ち時間が発生してしまうことがあるようです。

さいごに

10年以上前にC言語で MySQL に接続していたときにも同じことに悩まされたような・・MySQL はクエリを多重化できないし(クエリの結果をフェッチしきるまで次のクエリが実行できない)、実行中のクエリを止める手段も無いし(別接続を使って KILL とかかなりの荒業だと思う)、切断しようにもクエリの終了が待たされるし(mysql_close もクエリ実行中は待たされる)、どうしたものか。

長いこと PHP ばかり使っていてあまり気にしていませんでしたが、PDO で mysqlnd.net_read_timeout とかでタイムアウトしたときは TCP ぶつ切りになります。なのでそれ以後に同じ PDO インスタンスを使うとすべてエラーになります。まあそうするしかないですね。

余談ですが node-mysql2 はプールから接続を取り出すときに LIFO で取り出します。つまり最後にプールに戻った接続が最初に取り出されるので、前述のようなケースでプールに複数の接続があったとしても1番目と2番目で同じ接続が使われやすい、という問題もあるかも。

Postfix から SMTP endpoint を使わずに Amazon SES 経由でメールを送る

Postfix から Amazon SES 経由でメールを送る場合、通常であれば SMTP credentials を作成したうえで Postfix から Amazon SES の SMTP endpoint へリレーを設定します。

が、やんごとなき理由によりこの方法が使えなかったときのために Postfix から SES API の SendRawEmail でメールを送信してみました。

master.cfses トランスポートを定義する

master.cf に次のように追記して sqs トランスポートを定義します。

sqs  unix  -       n       n       -       -       pipe
  null_sender=MAILER-DAEMON@example.com
  user=nobody argv=/etc/postfix/ses.sh ${sender} ${recipient}

null_sender=MAILER-DAEMON@example.com は稀に postfix でメールがバウンスしたときに sender が MAILER-DAEMON のようなドメイン無しの名前でスクリプトが呼ばれて「error occurred (InvalidParameterValue) when calling the SendRawEmail operation: Missing final '@domain'」などとエラーになることがあるのでその対策です。ドメイン部分は SES で検証済 ID である必要があります。

mail.cf でデフォルトトランスポートを sqs に変更します。

default_transport = sqs

ses.sh を次の内容で作成します。

#!/bin/bash

sender=$1
shift
jq -Rs '{Data: .}' | aws \
    --region ap-northeast-1 \
    ses send-raw-email \
    --source "$sender" \
    --destinations "$@" \
    --raw-message file:///dev/stdin

簡単化のためにシェルスクリプトから AWS CLI を実行していますが、お好みのプログラミング言語&AWS SDKでも良いと思います。

さいごに

AWS 上の Webアプリケーションでメールを送信するような要件があるなら Postfix のような MTA はかまさずに直接アプリケーションから AWS SDK でメールを送ればよいので通常は Amazon SES の SMTP endpoint は使わないだろうと思います。

ただ、サードパーティのツールやライブラリで、メール送信の方法として「SMTP か sendmail か」ぐらいしか選択肢が無いことは往々にあって、そのような場合は Amazon SES の SMTP endpoint を使わざるを得ません。

SMTP endpoint に必要な SMTP credentials は実のところ IAM User のアクセスキーです(そのままではないけれどもアクセスキーから SMTP credentials が導出できる)。

ので SMTP endpoint を使うためにはそれようの IAM User をアクセスキー付きで作成する必要があるのですが・・メールの送信元サーバが EC2 インスタンスなのであれば IAM User のアクセスキーなんて使わなくても、インスタンスプロファイルに設定した IAM Role のポリシーで ses:SendRawEmail を許可するだけにしたいところです。

この記事の方法で可能です。

CloudFront ~ S3 で CORS を設定する

CloudFront 経由で公開している S3 に Web フォントを置いて、それをクロスオリジンで利用できるようにするために Access-Control-Allow-Origin ヘッダーを返すように設定したときのメモ。

以下の 2 通りの方法が考えられるでしょうか

  • CloudFront のレスポンスヘッダーポリシーで設定
  • S3 で CORS を設定して CloudFront から S3 へ Origin を渡す

CloudFront のレスポンスヘッダーポリシーで設定

CloudFront のビヘイビアで次のようなレスポンスヘッダーポリシーを設定します。

resource "aws_cloudfront_response_headers_policy" "cors" {
  name    = "example-cors"
  comment = "example-cors"
  cors_config {
    access_control_allow_methods {
      items = ["GET"]
    }
    access_control_allow_origins {
      items = ["example.com"]
    }
    access_control_allow_headers {
      items = ["*"]
    }
    access_control_allow_credentials = false
    origin_override                  = true
  }
}

resource "aws_cloudfront_distribution" "main" {
  // ...snip....

    response_headers_policy_id = aws_cloudfront_response_headers_policy.cors.id

  // ...snip....
}

次のように origin ヘッダーを渡してやれば access-control-allow-origin が返ってきます。

curl -I -XGET https://xxx.cloudfront.net/example.woff -H origin:example.com | grep access-control-allow-origin
#=> access-control-allow-origin: example.com

任意の origin を許可して OK なら、カスタムレスポンスヘッダーポリシーを用意しなくても SimpleCORS などのマネージドポリシーでも良いと思います。

data "aws_cloudfront_response_headers_policy" "cors" {
  name = "Managed-SimpleCORS"
}

S3 で CORS を設定して CloudFront から S3 へ Origin を渡す

S3 で次のように CORS を設定します。

resource "aws_s3_bucket_cors_configuration" "main" {
  bucket = aws_s3_bucket.main.id
  cors_rule {
    allowed_methods = ["GET"]
    allowed_origins = ["example.com"]
  }
}

そのうえで CloudFront のキャッシュポリシーで origin をキャッシュキーに含めます

resource "aws_cloudfront_cache_policy" "origin" {
  name    = "example-origin"
  comment = "example-origin"

  min_ttl     = 1
  max_ttl     = 31536000
  default_ttl = 86400

  parameters_in_cache_key_and_forwarded_to_origin {
    headers_config {
      header_behavior = "whitelist"
      headers {
        items = ["origin"]
      }
    }
    cookies_config {
      cookie_behavior = "none"
    }
    query_strings_config {
      query_string_behavior = "none"
    }
  }
}

resource "aws_cloudfront_distribution" "main" {
  // ...snip....

    cache_policy_id = aws_cloudfront_cache_policy.origin.id

  // ...snip....
}

次のように origin ヘッダーを渡してやれば access-control-allow-origin が返ってきます。

curl -I -XGET https://xxx.cloudfront.net/example.woff -H origin:example.com | grep access-control-allow-origin
#=> access-control-allow-origin: example.com

なお、キャッシュポリシーのキャッシュキーに origin を含めなくても、オリジンリクエストポリシーに origin を含めれば origin 付きのリクエストに対して access-control-allow-origin が返るようになります。ただその場合、リクエストの origin が違っていてもキャッシュが共通になるため、origin が無かったり違ったりするリクエストを元に access-control-allow-origin が無いレスポンスがキャッシュされてしまうと、正しい origin を送ったとしてもキャッシュから access-control-allow-origin が無いレスポンスが返されてしまうため、ダメです。

さいごに

CloudFront のレスポンスヘッダーポリシーで設定する方が、CloudFront がリクエストの origin を見て共通のキャッシュから access-control-allow-origin を出しわけてくれるので、こちらの方が良いですね。

S3 で CORS を使うのは CloudFront でレガシーキャッシュ設定しか使えなかった頃の名残で、もう使うことは無さそうです(CloudFront を噛まさない裸の S3 なら別ですけど)。