🐳

レンティオの本番環境をAWS Copilot/Fargate構成に切り替えた話

2021/12/13に公開

はじめに

こんにちは。レンティオの開発チームです(今回は長文となるため複数メンバーの合作でお届けします)

https://www.rentio.jp/

レンティオでは、Infrastructure as Code ツール(Architecture as Code[1]と書くべきでしょうか)を Convox から AWS Copilot に移行するプロジェクトが今年6月ごろからスタートし10月に無事本番稼働が実現できました。
この記事では意思決定の経緯や切り替えにあたって工夫したこと、諦めたことなどを少し共有できればと思います。

サービス移行の話が中心ですが、初めて Copilot を導入する方も参考になる部分があると思いますのでぜひご覧いただければと思います!

ありがとう Convox 😉

レンティオではかなり前からコンテナ化されたアプリケーションを Amazon ECS で運用していて、アプリケーションで利用するリソース類(Amazon RDSAmazon ElastiCache など)やパイプラインは AWS CloudFormation で構築していました。
さらにCI/CD は自動化されていて…という普通のIT企業と同じようなアーキテクチャがありました。
(各種テストは CircleCI を使用)

といってもこれらを0から用意したのではなく Convox という Platform as a Service を使うことで実現していました。
おかげで少人数でも効率的なサービス運用ができ、エンジニアはアプリケーション開発に集中できています。

周りで使っているという話をあまり聞いたことありませんが本当によいサービスでした。
https://www.wantedly.com/companies/rentio/post_articles/312021

こんにちは Copilot

しかし、ありがたい事にプロダクトが順調に成長をしエンジニアの数が増えたため Enterpriceプラン への切り替えを考え始めたそのころ

  • Copilot が実運用に十分に耐えるレベルまで進化してきたというのを耳にする
    ECS exec をサポート するようになったのがかなり大きい)
  • そもそも Fargate へ移行する機運が高まる
    (Convox でも Fargate にできるが ECS on EC2 だった)
  • もともと AWS のため移行できれば Convox を丸ごとコストカットできる
    (実際には CodePipeline など新たな支払いも増えるため甘くはなかった)

このようなことが重なり Copilot を中心とした AWS 構成に切り替えることを決断しました。
懸念があるとすれば AWS による実質的なベンダーロックインですが、それを差し引いてもメリットが大きいと判断しました。 (Convox はマルチクラウド
https://github.com/convox/convox

Convox is an open-source PaaS based on Kubernetes available for multiple cloud providers.

移行の話

ドキュメントを読んでいると Copilot を本番投入できる雰囲気はありましたが、移行するにあたって事前確認をしたり作業を進めていく中で見つかった課題がいくつかありました。

  • 未デプロイイメージでコマンド実行(copilot task の話)
  • 今まで通りCI/CD が自動化され好きなタイミングでデプロイできるか(copilot pipeline の話)
  • 今まで通り独立した環境で Cron ライクなジョブを実行できるか(copilot job の話)
  • RDS や S3 などのリソース管理方法(copilot storage の話)
  • 環境変数(copilot secret の話)
  • アプリケーションログ周り
  • ALBリスナールール

また、移行ついでに対応したこともあります。

  • メンテナンスモード
  • Asset Sync

それではこれらを個別に見ていきましょう。

copilot task

Web アプリケーションを継続開発していく上で、何らかの移行スクリプトを実行したい場面があると思います。「機能追加のためにテーブルを作成する」などが典型例です。(Rails ではお馴染みの bin/rails db:migrate ですね)

copilot svc exec コマンドで簡単にコンテナへ接続できることはわかっていましたが、デプロイ前に移行スクリプトを実行するために下記を確認しました。

  1. 未デプロイの Docker イメージをもとに、稼働中のアプリケーションとは独立したコンテナを立ち上げる
  2. そのコンテナで移行スクリプトを実行する

ちなみに Convox ではこうなります。

Terminal
# --release 指定でリリースを先取り
$ convox run web bin/rails db:migrate --release XXXXXXXXXXX

copilot task run

copilot task run を使うと独立した ECS タスクでコマンド実行できます。その際 --image オプションで Amazon ECR リポジトリ上のイメージを指定できるので、未デプロイのイメージを指定します。

さらに環境変数などもオプションで渡す必要があるためコマンドの用意はたいへんです。そこでコマンドのひな型を作ってくれる --generate-cmd オプションが役に立ちます。

Terminal
$ copilot task run --generate-cmd myapp/production/web
copilot task run \
--execution-role arn:aws:iam::000000000000:role/xxxxxxxx \
--task-role arn:aws:iam::000000000000:role/xxxxxxxx \
--image 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/xxxxxxxx \
--entrypoint "" \
--command "" \
--env-vars RAILS_ENV=production,..,.. \
--secrets DB_HOST=/copilot/myapp/production/secrets/db_host,..,.. \
--subnets subnet-00000000000000000,subnet-11111111111111111 \
--security-groups sg-00000000000000000 \
--cluster arn:aws:ecs:ap-northeast-1:000000000000:cluster/xxxxxxxx

これで稼働中の service と同条件で copilot task run できるコマンドが得られるため、あとは --image--command オプションを書き換えるだけです。

✅ ということで、移行スクリプトをデプロイ前に実行できることがわかりました 👍

おまけ
書き換えまでを自動化するシェルスクリプトを作ってみました。

copilot-task-run.sh
copilot-task-run
#!/bin/bash

usage_exit() {
  echo "Usage: $0 -s app/env/svc -c command [-i image]" 1>&2
  exit 1
}

# parse options
while getopts 's:c:i:' opt; do
  case "${opt}" in
    s) service="${OPTARG}" ;;
    c) command="${OPTARG}" ;;
    i) image="${OPTARG}" ;;
    *) usage_exit ;;
  esac
done

# -s and -c must be specified
if [[ -z "${service}" ]]; then
  usage_exit
fi
if [[ -z "${command}" ]]; then
  usage_exit
fi

# generate a command template
template=$(copilot task run --generate-cmd "${service}" 2>&1)

# remove existing --command and --image
template=$(echo "${template}" | grep -v -- --command)
if [[ -n "${image}" ]]; then
  template=$(echo "${template}" | grep -v -- --image)
fi

# append our --command and --image
template="${template} --command '${command}'"
if [[ -n "${image}" ]]; then
  template="${template} --image ${image}"
fi

# stream the logs
template="${template} --follow"

eval "${template}"
Terminal
$ ./copilot-task-run.sh \
  -s myapp/production/web \
  -c 'bin/rails db:migrate' \
  -i 000000000000.dkr.ecr.ap-northeast-1.amazonaws.com/xxxxxxxx

copilot pipeline

Copilot Pipeline が AWS CodePipeline をセットアップしてくれることはドキュメントを読んでわかっていましたが、Convox と同じことができるか確認しました。
確認ポイントは下記の5点です。

  • デプロイタイミングのコントロール
  • ローリングアップデート
  • ロールバック(hotfix か revert PR をマージしてデプロイすればよいのでマストではない)
  • 各種通知
  • Staging, Production 環境で Pipeline を分ける

簡単な図

  • Convoxでのデプロイフローがこちらで
  • Copilot により最終的にできあがったのがこちらです

✅ デプロイタイミングのコントロール ○
CodePipeline の場合、承認待ちで放置していると次のデプロイできません。
Convox CLI でビルド済みイメージを選択していつでも自由にデプロイできたのと比べると手間は増えますが運用上問題ないと判断しました。

CodePipeline でもビルドとデプロイを分離できるのかな 🤔

✅ ローリングアップデート ◎
Convox も CloudFormation で ECS を更新していたのでここは変わらずです。
移行の際に Blue/Green などにする案もありましたが、 Copilot はまだそれらのデプロイ方式に変更できないので今回はローリングアップデートに据え置きました。

✅ ロールバック ✗
たとえば新機能のリリース後に不具合が発覚した場合、リリース前の状態にすばやく戻すといったユースケースはあるかと思います。
mimemagic問題みたいなのがさらに重なると、すでにビルドしているやつで戻せるのは安心感があるので近いうちにどうにかしたいところです。

ちなみに、Convox では簡単にロールバックできます。

Terminal
$ convox releases rollback

✅ 各種通知 ○
承認アクションを挟むようにしているので通知がないと人間ポーリングする必要がでて生産性が落ちるので割と必須の機能です。
Copilot で通知設定を行うことは現時点ではできないので今回は、通知ルールを手動作成 してAWS Chatbot でSlack通知できるようにしました。

本当はビルド完了を ECR へ push したタイミングにしたかったのですが、EventBridge → Chatbot連携 では情報量ゼロの通知だったため承認通知を使うようにしました。

✅ 環境ごとに分ける ○
以前からレンティオでは以下のように Pipeline を分けていました。

  • 本番環境: main ブランチに変更が適用される → ビルド → 手動承認 → デプロイ
  • 検証環境: staging ブランチに変更が適用される → ビルド → 自動デプロイ

そこで移行しても同じような運用をしたかったのですが、Copilot Pipeline は

  • main ブランチに変更が適用される → ビルド → staging にデプロイ → 動作確認 → 承認 → production へデプロイ

という流れで、リリース前のレビュー環境を想定しているようなアーキテクチャでした。
これを解決するために、ざっくり下記の対応をしていますが詳細を書くと長くなるので別の機会に紹介できればなと思います。

  • pipeline.yml を2種類用意して copilot pipeline update を2回実行、 pipeline を2つ作成
  • buildspec.yml を変更してデプロイ先の環境を差し替えられるようにする

copilot job

レンティオでは sidekiq-cron も使っていますが、長時間動く重いバッチ処理は環境を分けて別タスクとして実行しています。
ドキュメントを読む限り、Convox Timer から Copilot Scheduled Job への移行は問題ないようですが、しくみを確認しました。

Copilot Job はAWSリソースの実態としては AWS Step Functions のステートマシーンであり、それが Amazon EventBridge で定期実行されることにより、Fargate Task が起動されます。
やることは、copilot job init コマンドを実行して Job を作成してから manifestファイルの commandschedule を Cronライクに設定するだけです。
Job のデプロイは copilot job deploy コマンドを実行するように見えますが、Pipeline 経由のデプロイも可能なので個別にデプロイする必要はありませんでした。

おまけ
staging 環境では動かす必要のないジョブもあるので、command の前に環境変数を差し込んだり schedule で存在しない日付を指定してジョブが実行されないようにしました。

manifest.yml
command: /bin/sh -c "test $DONT_RUN_JOB = true || bin/rails our:task"

environments:
  production:
    variables:
      DONT_RUN_JOB: false
  staging:
    variables:
      DONT_RUN_JOB: true
    on:
      schedule: "0 0 31 2 ?" # タスクが立ち上がらないようにする

buildspec.yml を書き換えて Job をデプロイしないようにするほうがスマートな気もしますが、ここは今後の課題です 😔

copilot storage

AWS リソース(レンティオの場合は RDS、ElastiCache など)の追加ですが、特に調べたことがないというか Copilot を使ってリソースの追加をしていないので書くことがありません 😅

レンティオでは RDS などほかの AWS サービスに関してはすでに AWS CDK で管理、TypeScript 化まで行っていていまさら CloudFormation テンプレートを YAML で用意するモチベーションが湧かなかったというのが正直なところですが、AWS リソースの追加 にあるように、Copilot でリソースを作成することももちろんできます。
copilot storage は S3 バケットを作成すると自動的にバケット名が環境変数として注入されるようですし便利そうですね(ぜひお試しください)

copilot secrets

Convox ではDB接続情報やAPIキーなどを環境変数で管理していましたが、Copilot Secrets にそのまま移行できるか確認をしました。

ドキュメントを読むと Copilot は SSM パラメータストアを使うという違いがあるのですが、実際に使ってみるとプログラムからは環境変数としてそのまま使えるので何も問題ありませんでした。
ただ copilot secret init に書いてある通り Request-Driven Web Service(App Runnerのこと) では使えない[2]ので注意が必要です。

また、実際に移行するときに使ったTips を書いておきます。

  1. 移行時に大量の Secrets を登録するのが面倒 🤔
    パッと見1つずつ secret init するのかと思いましたが --cli-input-yaml という YAMLファイルから一括登録できるオプションがあります。

    Terminal
    $ copilot secret init --cli-input-yaml /path/to/yaml
    
  2. Environments毎にsecretsを書く
    Secrets は Environments 毎に別々の値をセットできますが、v1.12.0 から manifest.yml で COPILOT_ENVIRONMENT_NAME 使ってスッキリ書けるようになりました![3]

    manifest.yml
    secrets:
      HOGE_API_KEY: /copilot/my_app/${COPILOT_ENVIRONMENT_NAME}/secrets/hoge_api_key
      FUGA_API_KEY: /copilot/my_app/${COPILOT_ENVIRONMENT_NAME}/secrets/fuga_api_key
    

アプリケーションログ周り

レンティオではCloudWatch Logs のログを定期的に S3 へエクスポートしており、Copilot 移行後も問題なく機能するか確認しました。

たとえば appNamerentioenvNameproduction [4]で、 Load Balanced Web Service として web-sampleScheduled Job として job-sample の構成にした場合は以下のように2つのロググループが作成されます。

  • ロググループ(copilot/rentio-production-web-sample)
  • ロググループ(copilot/rentio-production-job-sample)

Convox ではロググループは1つで各サービスがロググループに分かれる設計でした。

  • ロググループ(rentio-production)
    • ログストリーム(web-sample/xxxxxx)
    • ログストリーム(job-sample/xxxxxx)

✅ そこで、prefix (copilot/rentio-production-) にマッチする複数ロググループをエクスポートできるようにしました 💪

おまけ
今回はこのような対応をしましたが、Copilot には logging オプションで FireLens を利用して複数のログ保存先をカスタマイズする機能もサポートされているようですので、将来的にはそちらも試してみたいと思います。
https://aws.github.io/copilot-cli/en/docs/developing/sidecars/#sidecar-patterns

For now, the only supported pattern is FireLens, but we'll add more in the future!

今(2021/12現在)は FireLens のみ対応のようですが、将来的にパターンを増やしていくと明言されているので、今後の機能拡大にも期待ですね。

ALBリスナールール

アプリケーション作成時 copilot app init--domain オプションで既存のドメインを指定できます。
こうすることでドメインが登録され、HTTP, HTTPS リスナーを登録した ALB ができ… となります(裏で何がおきているか を見てもらうとなんとなく想像できると思います)

しかし、レンティオではロードバランサ(ALB)の前に Amazon CloudFront をおく、よくある構成をすでにとっていますから
"裏でおきる何か" の大半は不要で domain オプションを指定せず HTTPS 通信に必要なリソースについては独自に作成することとしました。(domain オプションがないとHTTPS の構成にならない)

  • Route 53
    • Copilot 作成の ALB の CNAMEレコード(copilot_production.{独自ドメイン})
  • ALB
    • HTTPS 用リスナ
      • 証明書はこれまで使っていたものを使用
    • HTTPS 用リスナルール
      • アプリケーション用コンテナをターゲットグループにもつルール
      • そのほか、サービス的に必要なルール

以下は一部書き換えていますが、今回追加した CDK のコードです。これにより HTTPS 通信を実現できました 🏗️

CDK Route 53用Stack
Route 53用Stack
const loadBalancerArn = 'ALBのARN'

const loadBalancerDnsName = elb.ApplicationLoadBalancer.fromLookup(this, 'ALB', {
  loadBalancerArn: loadBalancerArn,
}).loadBalancerDnsName

const hostZoneEc = route53.HostedZone.fromLookup(this, 'HostedZoneEc', {
  domainName: 'ドメイン名',
})

new route53.CnameRecord(this, 'CnameProductionCopilotEc', {
  recordName: 'CNAME',
  zone: hostZoneEc,
  domainName: loadBalancerDnsName,
  ttl: cdk.Duration.seconds(10),
})
CDK ALB用Stack
ALB用Stack
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
  super(scope, id, props)

  const loadBalancer = elb.ApplicationLoadBalancer.fromLookup(this, 'ALB', {
    loadBalancerArn:
      'ALBのARN',
  })

  const targetGroupDefault = elb.ApplicationTargetGroup.fromTargetGroupAttributes(this, 'ALBTargetGroupDefault', {
    targetGroupArn:
      'ターゲットグループARN(Default)',
  })

  const targetGroup = elb.ApplicationTargetGroup.fromTargetGroupAttributes(this, 'ALBTargetGroupEC', {
    targetGroupArn:
      'ターゲットグループARN(通常)',
  })

  const httpsLister = new elb.ApplicationListener(this, 'ALBListenerHttps', {
    loadBalancer: loadBalancer,
    certificateArns: [ 'ACMのARN' ],
    port: 443,
    protocol: elb.ApplicationProtocol.HTTPS,
    defaultAction: elb.ListenerAction.forward([targetGroupDefault]),
  })

  new elb.ApplicationListenerRule(this, 'listerRuleAdmin', {
    listener: httpsLister,
    priority: 1,
    conditions: [elb.ListenerCondition.pathPatterns(['/*'])],
    targetGroups: [targetGroup],
  })

  // 以下2つのリスナルールは通常時は到達しないメンテナンスモード用
  new elb.ApplicationListenerRule(this, 'listerRuleAllowInsiderSourceIpAccess', {
    listener: httpsLister,
    priority: 2,
    conditions: [elb.ListenerCondition.sourceIps(["アクセス許可する内部IP"])],
    targetGroups: [targetGroup],
  })

  // 特定IP以外は固定レスポンスを返す
  new elb.ApplicationListenerRule(this, 'listerRuleMaintenance', {
    listener: httpsLister,
    priority: 3,
    conditions: [elb.ListenerCondition.pathPatterns(['/*'])],
    action: elb.ListenerAction.fixedResponse(503, {
      contentType: 'text/html',
      "レスポンスボディ",
    }),
  })

課題に感じていること

  1. CDKにおけるARNの参照
    上記コードで ARN を指定している箇所は、 Copilot で Service 作成後に ARN を確認して CDK テンプレートを変更 → デプロイという流れになっています。そのため、 Copilot リソースを再度作成した場合には変更が必要です。
  2. HTTP用リスナの存在
    デフォルトの動作によって、 Convox では HTTP 用リスナは HTTPS 用にリダイレクトされていたものが、リスナルールにもターゲットグループが紐づいている状態になっており、HTTP でアクセスができてしまうように思えます。CloudFront において、オプション Viewer Protocol Policy: Redirect HTTP to HTTPS を指定しているため、とりあえずは大丈夫ですが不要なリソースを作成しない&独自の証明書をインポートできるのであれば、domain オプションを使いたいところです。
    証明書のインポートについては課題として上がってるようですが、対応には時間がかかりそうです

メンテナンスモード

システムメンテナンス実施時に社内アクセスのみを通し外部からのアクセスはすべて503を返す、みたいな運用をするケースがあるかと思います。
レンティオではこのアクセス制限をメンテナンスモードと呼んでいて、ALB のリスナールールの priority を変更することで行っています。

この運用については Convox の時から行っていたのですが、今回 Copilot に移行するにあたってメンテナンスモードのコード化をしましたので、どんな感じの運用かと CDK のコードをまとめました。

  • ALB とターゲットグループは Copilot が作成したリソースを指定します。
  • メンテナンスルールは通常時には priority を一番下げて到達できないようにしておき、メンテナンスモードに切り替えるとき priority を一番上にあげるイメージです。
  • 動作確認のため、メンテナンスモード中でも中のひとはアクセスできるようにルールを足しています。
  • /health_check_path のアクセスは許可したいので、そのルールも別途用意しています。
  • (実際の構成はもう少し複雑ですが、簡略化しています🙏)
AWS CDKで書いたサンプルコード
import * as cdk from '@aws-cdk/core'
import * as elb from '@aws-cdk/aws-elasticloadbalancingv2'

export class SampleLoadBalancerListenerStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const loadBalancer = elb.ApplicationLoadBalancer.fromLookup(this, 'sampleALB', {
      loadBalancerArn: 'arn:aws:elasticloadbalancing:xxxxx', // Copilot が作成した ALB
    })

    const targetGroup = elb.ApplicationTargetGroup.fromTargetGroupAttributes(this, 'sampleALBTargetGroup', {
      targetGroupArn: 'arn:aws:elasticloadbalancing:xxxxx', // Copilot が作成したターゲットグループ
    })

    const httpsLister = new elb.ApplicationListener(this, 'sampleALBListenerHttps', {
      loadBalancer: loadBalancer,
      certificateArns: ['arn:aws:acm:xxxxx'],
      port: 443,
      protocol: elb.ApplicationProtocol.HTTPS,
      defaultAction: elb.ListenerAction.forward([targetGroup]),
    })

    // 通常時のリスナールール
    new elb.ApplicationListenerRule(this, 'sampleListerRule', {
      listener: httpsLister,
      priority: 1,
      conditions: [elb.ListenerCondition.pathPatterns(['*'])],
      targetGroups: [targetGroup],
    })

    // 内部アクセスは許可
    new elb.ApplicationListenerRule(this, 'sampleListerRuleAllowInsiderAccess', {
      listener: httpsLister,
      priority: 2,
      // ... 内部アクセスを許可するルール
      targetGroups: [targetGroup],
    })

    // /health_check_path は許可
    new elb.ApplicationListenerRule(this, 'sampleListerRuleAllowPath', {
      listener: httpsLister,
      priority: 3,
      conditions: [elb.ListenerCondition.pathPatterns(['/health_check_path'])],
      targetGroups: [targetGroup],
    })

    // メンテナンス
    new elb.ApplicationListenerRule(this, 'sampleListerRuleMaintenance', {
      listener: httpsLister,
      priority: 4,
      conditions: [elb.ListenerCondition.pathPatterns(['*'])],
      action: elb.ListenerAction.fixedResponse(503, {
        contentType: 'text/html',
        messageBody: 'メンテ中だよ'
      }),
    })
  }
}

おまけ
このままでも AWS マネジメントコンソール上から priority を上下させることで手動切り替えを行うことができますが(参考)、実運用ではもう少し楽をしたいので、コマンド一発で切り替えを行えるようにしました。

Terminal
# 上から内部アクセス許可、ping許可、メンテモード、通常時のリスナールール
aws elbv2 set-rule-priorities --rule-priorities \
  RuleArn=arn:aws:elasticloadbalancing:region:accountId:listener-rule/app/allow-insider-access-listener-rule-arn,Priority=1 \
  RuleArn=arn:aws:elasticloadbalancing:region:accountId:listener-rule/app/allow-ping-listener-rule-arn,Priority=2 \
  RuleArn=arn:aws:elasticloadbalancing:region:accountId:listener-rule/app/maintenance-listener-rule-arn,Priority=3 \
  RuleArn=arn:aws:elasticloadbalancing:region:accountId:listener-rule/app/default-listener-rule-arn,Priority=4

このように AWS CLI を使ってリスナールールの priority を明示して set-rule-priorities すれば指定した priority の通りに切り替えることができます。切り替え時間はほぼ即時でメンテナンスモードに切り替わってくれます。簡単ですね。

実際に Convox → Copilot の移行作業時もこのしくみでメンテナンス画面に切り替えました 🔧

Asset Sync

Copilot で service をデプロイするには以下の選択肢がありますが、挙動としてはどちらもローリングアップデートになります[5]

  • copilot svc deploy を叩いてデプロイ
  • pipeline を使った自動デプロイ

つまり ALB のターゲットグループには一時的に新旧ターゲットが共存し、その後旧ターゲットが外れていきます。
Rails のような HTML を返す (普通の) Web アプリケーションの場合、ローリングアップデート中に新 → 旧 (旧 → 新) の順にリクエストすると CSS,JS, 画像などを取得できずにページの表示が崩れることになります。

  1. ブラウザからページ取得をリクエストする
  2. 新しいターゲットにつながると新しい HTML が返る
  3. 新しい HTML 内の <link><img> が指すアセットにリクエストする
  4. 古いターゲットにつながると新しいアセットは返せない (404)

レンティオではスティッキーセッションとAsset Sync です。

直接アセット配信する場合

Rails でいう public/assets/* のように Web アプリケーションから直接アセット配信する場合は簡単です。ALB のスティッキーセッション を有効にすればターゲットを固定できます。

manifest.yml
http:
  stickiness: true

CDN でアセット配信する場合

アセットは静的なので実際には Fastly 等の CDN を使って配信することの方が多いと思います。この場合、CDN のオリジンを何とするかで対応方法が異なります。

ALB をオリジンとする場合

CDN で未キャッシュであれば ALB にリクエストが来て自分たちのアプリケーションに転送されますが、これは結局新旧どちらのターゲットにつながるか分かりません。

したがって前述のスティッキーセッションを有効にしたいのですが、Cookie なのでドメインが異なると送信されません。つまりサイトのドメインが example.com であった場合、CDN のドメインもそれと同じでなければなりません。(AWSALB には Domain 指定がないため cdn.example.com のようなサブドメインでも駄目です)

サイトの動的なページも CDN 経由 (ただし常にキャッシュしない) とすればドメインをそろえることはできますが、これは大きな制約になるかと思います。

S3 をオリジンとする場合

S3 のようなストレージサービスにアセットを置いてそれをオリジンとする場合です。レンティオもこれに当てはまります。

この場合、アセットがファイル名でバージョニングされているとうまくいきます。
たとえば Rails では bin/rails assets:precompile したアセットにはその内容から算出されたフィンガープリントが付与されます。

Terminal
$ ls ./public/assets/
my-file-88d261a51043637d90a823a4c57b93af6a5266c8d87013b8dabefadff0b66745.css

リクエストのパスにもこのフィンガープリントが付きます。

GET /assets/my-file-88d261a51043637d90a823a4c57b93af6a5266c8d87013b8dabefadff0b66745.css

そこで、新旧両方のバージョンのアセットを S3 に置いておけばどちらの要求にも応えられるというわけです。

S3 へのアップロード

S3 へのアップロードには asset_sync gem が便利です。
デフォルトでは bin/rails assets:precompile したタイミングで自動アップロードしてくれます。
それをオフにして bin/rails assets:sync で任意タイミングでのアップロードもできます。

S3 へのアップロードというのはいわば副作用なわけですが、レンティオでは pipeline での Docker イメージビルド時に行っています。
docker build に AWS のアクセスキーを渡す必要があるので buildspec.yml に一工夫必要ですが、この時点でアップロードしておければ後は rails server を起動するだけなので安心です。
オートスケールした時も副作用なく立ち上がるのはメリットと言えるでしょう。

S3 からの削除

S3 にはバージョン違いのアセットがどんどんたまっていくことになるので、定期的に古いバージョンを削除する必要があります。
asset_sync はそのために bin/rails assets:sync:clean を用意してくれているので定期実行しましょう。

おわりに

以上、Convox から Copilot へ移行した背景とチームとして取り組んだこと、そして移行の中で出てきた課題・解決方法なども含めて一連を盛り込んだ結果、これまでの記事と比べてかなり長文となってしまいました。

Copilot はどんどん便利になるのでリリースから目が離せないですね。
引き続き、開発の中で出てきた Tips など、機会があれば紹介できればと思います。

これを機に Copilot だけでなくレンティオにも興味を持っていただければたいへんうれしいです😊

採用情報

レンティオではエンジニアを絶賛募集中です!
https://www.wantedly.com/companies/rentio

脚注
  1. Architecture as Code? 〜Infrastructure as Codeを超えて〜 https://tech.nri-net.com/entry/2021/07/05/090000 ↩︎

  2. Copilot SecretsがApp Runnerで使えないのをどうにかする ↩︎

  3. Substitute environment variables in manifests https://github.com/aws/copilot-cli/issues/2912 ↩︎

  4. Copilot の Applications, Environments, Services, Jobs の概念についてはドキュメントを御覧ください。 ↩︎

  5. Support Blue/Green Deployment https://github.com/aws/copilot-cli/issues/1758 ↩︎

Discussion