有名テック企業の技術ブログを、ひとつのフィードで。
フィード
31件
はじめに こんにちは、タイミーでエンジニアをしている徳富(@yannKazu1)です。 タイミーではメインサービスのバックエンドを Rails で開発しています(Go を採用しているプロダクトもありますが、本記事では Rails を前提とします)。 突然ですが、皆さんのチームでは CI の待ち時間、気になっていませんか? 「Push した、コーヒー淹れた、戻ってきた、まだ回ってる……」みたいな経験は、開発者なら一度はあるのではないでしょうか。 本記事では、そんな状況を改善するために GitHub Actions 上のテスト実行パイプラインで取り組んだ 3 つの高速化テク を紹介します。どれも「知っていれば明日から試せる」くらいの温度感なので、気軽に読んでいただければと思います。 1. キャッシュの保存先を GitHub Cache から S3 に移行 課題: actions/cache が安定して速くない 最初にぶつかった壁が actions/cache の速度でした。vendor/bundle(数百 MB〜1 GB 超)の save/restore でやたら時間がかかることがあり、リストアだけで数分待たされる場面がちょくちょくありました。これはセルフホストランナーに限った話ではなく、GitHub ホステッドランナーでも起きます。 実際、公式リポジトリにも Extremely slow cache on self-hosted from time to time という Issue が立っていて、セルフホスト・GitHub ホステッド問わず同様の報告が寄せられています。 さらに私たちの場合、AWS 上のセルフホストランナー を使っているのでなおさらです。actions/cache のバックエンドは Azure Blob Storage のため、セルフホストランナーからだとインターネット経由のアクセスになり、スループットが 約 20 MB/s まで落ちる ケースも報告されています(Actuated Blog)。突発的に遅いうえに経路も遠い——これでは安定した速度は望めません。 容量面でも、リポジトリあたり 10GB の制限があります。また、7 日間アクセスのないキャッシュは自動削除されます。その結果、ブランチが増えるとすぐに上限に達し、必要なキャッシュが消えてしまうのも地味にストレスでした。 解決策: runs-on/cache で S3 をバックエンドに そこで [runs-on/cache](https://github.com/runs-on/cache) を導入し、キャッシュの保存先を 同一リージョン(東京)の S3 バケット に切り替えました。 前述のとおり、セルフホストランナーで actions/cache を使うとスループットが ~20 MB/s まで落ちるケースがあります。一方 runs-on/cache は同一リージョンの S3 を使えるため、200 MiB/s 以上 のスループットが出ます(公式ドキュメント)。単純計算で 10 倍近い改善 です。 actions/cache とインターフェースがそのまま同じなので、uses: を差し替えて環境変数を 1 つ足すだけで移行できました。 # .github/actions/setup-ruby-with-s3-cache/action.yml - name: Restore cache uses: runs-on/cache@v4.2.3-r2 env: RUNS_ON_S3_BUCKET_CACHE: your-gha-cache-bucket with: path: "**/vendor/bundle" key: bundle-v1-${{ runner.os }}-${{ inputs.ruby_version }}-${{ hashFiles('Gemfile.lock') }} restore-keys: | bundle-v1-${{ runner.os }}-${{ inputs.ruby_version }}- bundle-v1-${{ runner.os }}- なぜ runs-on/cache を選んだか S3 をキャッシュバックエンドにする方法は他にもあります(tespkg/actions-cache、whywaita/actions-cache-s3、自前の aws s3 cp スクリプトなど)。その中で runs-on/cache にした決め手はこのあたりです。 環境変数 1 つで切り替え: RUNS_ON_S3_BUCKET_CACHE を設定するだけで S3 バックエンドに切り替わる 自前実装が不要: 圧縮・展開・キャッシュキーのマッチング・フォールバックなど、地味にめんどくさい部分を全部やってくれる 容量無制限: S3 なので 10GB の制限もキャッシュの自動削除もなし キャッシュキーの設計 キャッシュキーは 3 段階のフォールバック構造にしています。 bundle-v1-Linux-3.3.6-<Gemfile.lock のハッシュ> ← 完全一致(最速) bundle-v1-Linux-3.3.6- ← Ruby バージョン一致 bundle-v1-Linux- ← OS のみ一致 完全一致しなくても、部分一致したキャッシュをリストアして bundle install すれば差分の gem だけで済みます。ゼロからインストールするより圧倒的に速いので、新しいブランチでもほぼキャッシュが効く状態を維持できます。 OIDC 認証で安全に S3 にアクセス AWS へのアクセスには OIDC 認証 を使っています。長期的なアクセスキーをシークレットに保存しなくて済むので、セキュリティ面でも安心です。 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::123456789012:role/your-gha-role aws-region: ap-northeast-1 2. マイグレーション結果をまるごとキャッシュ 課題: 毎回のマイグレーションが地味に重い テストジョブは毎回データベースをセットアップします。ここで問題になったのが、マイグレーション数が数百を超えてくると rails db:create db:schema:load だけで 数分かかる ということ。 「schema:load だからすぐ終わるでしょ?」と思いきや、テーブル数が多いとそうでもないんですよね。 解決策: MySQL のデータディレクトリごと S3 にキャッシュ 発想を変えて、マイグレーション済みの MySQL データディレクトリ (/var/lib/mysql) をまるごと S3 にキャッシュ することにしました。要は「マイグレーション済みの DB をそのまま持ってくれば、マイグレーション自体を省略できるよね」という作戦です。 仕組みの全体像 【キャッシュの生成】 【キャッシュの利用】 master ブランチ feature ブランチ db/migrate/** 変更 テストジョブ起動 or 毎日定時 │ │ ▼ ▼ S3 からキャッシュをリストア MySQL 起動 → ./tmp/mysql_data に展開 │ │ ▼ ▼ rails db:create db:migrate MySQL 起動(データマウント済み) │ │ ▼ ▼ ./tmp/mysql_data を S3 に保存 rails db:migrate(差分のみ) │ ▼ テスト実行 キャッシュの生成: master で定期的に焼き直す master ブランチでマイグレーションファイルが変更されたとき、または毎日定時に、専用のワークフローがキャッシュを更新します。 # .github/workflows/update-migration-cache.yml on: push: branches: [master] paths: - 'db/migrate/**' - 'db/schema.rb' - '.github/workflows/update-migration-cache.yml' - '.github/actions/migration-hash/**' schedule: - cron: '0 2 * * *' # 毎日 UTC 2:00(JST 11:00)に実行 workflow_dispatch: # 手動実行も可能 やっていることはシンプルです。 MySQL コンテナを起動(データディレクトリを ./tmp/mysql_data にマウント) rails db:create db:migrate でフルマイグレーション実行 ./tmp/mysql_data をまるごと S3 にアップロード - name: Run database migration run: bundle exec rails db:create db:migrate - name: Save migration cache uses: runs-on/cache/save@v4.2.3-r2 env: RUNS_ON_S3_BUCKET_CACHE: your-gha-cache-bucket with: path: ./tmp/mysql_data key: test-${{ runner.os }}-${{ runner.arch }}-mysql${{ steps.migration-hash.outputs.mysql_version }}-${{ runner.environment }}-db-migration-${{ steps.migration-hash.outputs.hash }} キャッシュキーの設計: 何をキーに含めるかが大事 キャッシュキーには地味に気を使っています。 test-Linux-X64-mysql8.0.28-self-hosted-db-migration-<db/schema.rb のハッシュ> │ │ │ │ │ OS ARCH MySQL Ver ランナー環境 スキーマハッシュ ポイントは db/schema.rb のハッシュを含めていること。マイグレーションの内容が変われば schema.rb も変わるので、自動的に新しいキャッシュが生成されます。MySQL バージョンやアーキテクチャもキーに入れているのは、バイナリ非互換でハマらないための保険です(一度やらかしました……)。 <h3 id="キャッシュの利用-Composite-Actio
はじめに はじめまして、プラットフォームエンジニアリング本部に所属している徳富(@yannKazu1)です。 みなさん、サプライチェーン攻撃って気にしてますか? npm パッケージの乗っ取り(ua-parser-js 事件)、GitHub Actions の改ざん(tj-actions/changed-files 事件)、依存パッケージへのバックドア混入(xz-utils 事件)……。ここ数年、OSS を取り巻くセキュリティの前提がガラッと変わってきています。正直、「いつ・どこから仕掛けられるかわからない」状況です。 しかもサプライチェーン攻撃って、攻撃側のコストが低いわりに被害範囲が広いのが厄介なんですよね。 そんなわけで、ECS Fargate 環境におけるサプライチェーン攻撃対策を整理してみようと思ったのですが、いきなり全部を洗い出そうとしてもカオスになるだけ。何かいいフレームワークはないかな……と探していたところ、Kubernetes の 4C セキュリティモデル(Cloud → Cluster → Container → Code) の考え方がそのまま使えそうだったので、これをベースにチェックシート的に整理してみました。 「うちの環境だとどこが手薄いんだろう?」を考えるときの参考にしてもらえればと思います。 おことわり: これをやれば完璧!というものではないです。あくまで「見通しよく整理するための道具」として 4C モデルを借りているだけなので、実際にどこまでやるかは環境やリスク許容度に応じて取捨選択してください。 整理に使う 2 つの軸 軸 1:4C セキュリティモデル —「どこを守るか」 Kubernetes の公式ドキュメントで紹介されている、クラウドネイティブセキュリティを 4 つの同心円レイヤー で捉えるモデルです。 参考: クラウドネイティブセキュリティの概要 | Kubernetes ┌─────────────────────────────────────────┐ │ Cloud(クラウド基盤) │ │ ┌─────────────────────────────────────┐ │ │ │ Cluster(オーケストレーター) │ │ │ │ ┌─────────────────────────────────┐ │ │ │ │ │ Container(コンテナランタイム) │ │ │ │ │ │ ┌─────────────────────────────┐ │ │ │ │ │ │ │ Code(アプリケーション) │ │ │ │ │ │ │ └─────────────────────────────┘ │ │ │ │ │ └─────────────────────────────────┘ │ │ │ └─────────────────────────────────────┘ │ └─────────────────────────────────────────┘ ポイントは 各レイヤーが外側のレイヤーの上に構築されている ということ。どれだけアプリのコードを堅牢にしても、基盤レイヤーのセキュリティが低い水準では守りきれません。だからこそ、特定のレイヤーだけに頼るのではなく、すべてのレイヤーを固める 多層防御(Defense in Depth) が基本方針になります。 ECS Fargate への読み替えはこんな感じです。 4C レイヤー K8s での意味 ECS Fargate での対応 サプライチェーン攻撃での主な攻撃面 Cloud クラウド基盤 AWS アカウント・IAM・ネットワーク IAM キー漏洩、ECR への不正 push Cluster オーケストレーター ECS クラスター・CI/CD パイプライン CI/CD アクションの改ざん、パイプライン侵入 Container コンテナランタイム Docker イメージ・Fargate タスク ベースイメージの汚染、OS パッケージへのバックドア混入、実行時の不正プロセス Code アプリケーションコード ソースコード・依存パッケージ パッケージ乗っ取り、typosquatting、悪意ある PR 軸 2:対策の目的 —「何のために守るか」 4C モデルは「どこを守るか」を整理するフレームワークですが、それだけだと対策が偏りがちです。そこでもうひとつ、 「何のために守るか」 という軸を加えてみます。今回は、セキュリティ対策を以下の 4 つの目的に分類して整理してみました。 目的 説明 考え方 🛡 予防(Prevention) 攻撃を未然に防ぐ そもそも悪いものを入れさせない 🔍 検知(Detection) 攻撃や脆弱性を発見する 入り込んだ・紛れ込んだことに気づく 🧱 封じ込め(Containment) 侵入後の被害を最小化する やられても被害を広げさせない 🔎 調査(Investigation) 何が起きたかを追跡する 事後に原因と影響範囲を特定する よくある落とし穴は 「予防」ばかりに意識が向いて、他が手薄になる こと。完璧な予防は不可能なので、入り込まれた後にどう気づいて・どう被害を抑えて・どう調べるか、まで含めて考えるのが多層防御の本質です。 この記事の構成 本記事では 目的(予防・検知・封じ込め・調査)を大項目 にして、それぞれの中で 4C のどのレイヤーに対する対策か を整理していきます。 🛡 予防(Prevention)— そもそも入れさせない 攻撃を未然に防ぐための対策です。「入口を塞ぐ」イメージですね。 Cloud:VPC Endpoint 概要: AWS サービスへの通信をインターネットを経由せずに VPC 内で完結させる。 防げる攻撃: 侵害されたタスクからの外部 AWS アカウントへのデータ持ち出し (Endpoint Policy で自社アカウントに限定) マルウェア感染後の C2 通信・情報送信 (Egress 全遮断下でも AWS サービスは利用可能) 漏洩した IAM 認証情報による外部からの不正アクセス (バケットポリシーで aws:SourceVpce を指定) 設定のポイント: S3 Gateway Endpoint(無料)は必須 ECR、SSM、Secrets Manager、CloudWatch Logs 用の Interface Endpoint を検討 Endpoint Policy で aws:PrincipalAccount を制限 リソース側ポリシーで aws:SourceVpce を指定 Cluster:CI/CD パイプラインのハードニング 概要: GitHub Actions など CI/CD で使うサードパーティアクションを、改ざんされない形で固定する。 防げる攻撃: GitHub Actions の改ざん(tj-actions/changed-files 事件のように、人気アクションのリポジトリが侵害されてタグが書き換えられるケース) バージョンタグの上書きによる 意図しないコードの実行 設定のポイント: GitHub Actions は通常 uses: actions/checkout@v4 のようにタグやブランチで指定しますが、これらは 後から書き換え可能 です。tj-actions/changed-files 事件(2025 年 3 月)では、攻撃者がメンテナーの認証情報を侵害し、既存タグを悪意あるコミットに向け直すことで、汚染されたアクションを使う CI でシークレットがビルドログに書き出されるという被害が広範囲に発生しました。一方、commit SHA でピンニングしていたユーザーは影響を受けませんでした(侵害期間中に対象 SHA へ更新していなければ)。 対策として、commit SHA でピンニングする のが推奨されます。 # Before(タグ指定 - 書き換えられる可能性あり) - uses: actions/checkout@v4 # After(commit SHA でピンニング - 改ざんされない) - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 Dependabot や Renovate Bot で SHA の自動更新を設定 permissions: で各ジョブの GITHUB_TOKEN 権限を最小化 OIDC を使った AWS 認証に切り替え、長期クレデンシャルを廃止 パブリックリポジトリでは特に注意:ビルドログが公開されるため、ログ経由のシークレット漏洩のインパクトが大きい サプライチェーン攻撃との関連: これは Container レイヤーの digest ピンニングと同じ思想です。「同じ名前で違うものを掴まされる」攻撃を防ぐには、暗号学的なハッシュで内容を固定するのが基本になります。 Cluster:Secrets の安全な注入 概要: DB パスワードや API キー等の機密情報を、コードへの直書きではなく SSM Parameter Store や Secrets Manager から注入する。 防げる攻撃: ソースコードやコンテナイメージへのシークレット埋め込み を防止 Git リポジトリの漏洩時に クレデンシャルが直接露出するリスク を排除 KMS 暗号化により、AWS アカウントが侵害されても 暗号化キーなしでは復号不可能 設定のポイント: "secrets": [ { "name": "DATABASE_PASSWORD", "valueFrom": "/fargate/myapp/database-password" } ] Cluster:ECS Exec の制御 概要: ECS Exec(コンテナへの対話的アクセス)を必要時以外は無効化する。 防げる攻撃: IAM クレデンシャルが漏洩した場合の コンテナへの直接侵入 本番コンテナへの 不正なコマンド実行 設定のポイント: ECS Exec の制御は、サービス(または RunTask 呼び出し)レベルと IAM レベルの二重で行うのが確実です。enableExecuteCommand はタスク定義のフィールドではなく、サービス(CreateService / UpdateService)または RunTask のパラメータです。 サービス定義(または RunTask 呼び出し)で無効化:enableExecuteCommand = false を明示。そもそもサービス側で受け付けない状態にしておく。AWS CLI でも aws ecs update-service --no-enable-execute-command のように切り替え可能です。 IAM で ecs:ExecuteCommand を Deny:ECS Exec 専用の API なので、これを Deny するのが最も直接的。なお ECS Exec は内部的に SSM Session Manager の通信レイヤー(ssmmessages API)を利用するため、より厳密に制御したい場合は、タスクロールに ssmmessages:* 系の権限が紛れ込んでいないかも確認する 必要時のみ一時的に有効化する運用フロー:踏み台用の専用サービスを別途用意し、調査時のみそちらを起動する運用が安全。 補足: 後述の封じ込めセクションで紹介する readonlyRootFilesystem: true と ECS Exec は 両立しない 点に注意してください。SSM agent がコンテナファイルシステムへの書き込みを必要とするため、ルートファイルシステムを読み取り専用にすると ECS Exec が動きません(AWS 公式ドキュメントでも明記されています)。本番では readonlyRootFilesystem: true + ECS Exec 無効化、調査用の踏み台サービスでは ECS Exec 有効化、と用途で使い分けるのが現実的です。 Container:ベースイメージの digest ピンニング 概要: Dockerfile のベースイメージ指定を、タグだけでなく digest(SHA256 ハッシュ)で固定する。 防げる攻撃: ベースイメージのタグ上書き攻撃(同一タグに悪意あるイメージを push) レジストリが侵害された場合の イメージ改ざんの検知 設定例: # Before(タグのみ) FROM ruby:3.3.0-bookworm # After(digest ピンニング) FROM ruby:3.3.0-bookworm@sha256:2e1e76e5b2... 運用のポイント: Dependabot や Renovate Bot で digest の自動更新を設定する digest がまだ付いていないイメージに digest を自動付与する pinning 自体は、現時点では Renovate のほうが運用面で先行(pinDigests プリセット)。Dependabot 側でも 2026 年 2 月に PR #14071 が dependabot-core 本体にマージされ、docker_pin_digests という experiment flag として実装されました。ただし experiment flag は Dependabot サービス側で段階的に展開されるため、GitHub.com の Dependabot で「タグだけのイメージに digest を新規付与する挙動」がデフォルトで使えるかはタイミング次第です(フラグの有効化状況によっては自分の環境で効かないこともある点に注意)。 現時点で「すでに digest 付き」のイメージに対する digest 更新は Dependabot でも問題なくできます。タグだけで運用しているイメージを一括で digest ピン化したい
はじめに こんにちは。プラットフォームエンジニアリングチームに所属している徳富(@yannKazu1)です。 新規プロダクトを立ち上げるとき、インフラ構築って意外とやることが多いですよね。その中でも地味にめんどくさいのがDBユーザーの作成と権限付与。手動でやると「あ、権限つけ忘れた」「このユーザー名スペルミスってない?」みたいなヒヤリハットが発生しがちです。 今回は、この作業をTerraformでIaC化した話を書いていきます。 背景:ボイラープレートでインフラ構築を爆速にしている 弊社ではTerraformのボイラープレートと、それをもとにインフラを構築するためのDevinへの指示プロンプトをセットで管理しているリポジトリがあります。 新規プロダクトのインフラが必要になったら、このリポを使ってDevinにお願いするだけ。数時間もあれば、AWSアカウントの作成、VPC・ECS・Auroraなどの基盤構築、Terraform実行に必要なIAMロール・バックエンド・CI/CDワークフローの設定に加え、Datadog設定、監査設定、ログ基盤の作成まで、必要なインフラがひととおり立ち上がります。 このボイラープレートを整備するにあたって目指したのは、「Devinにお願いするだけで、新規プロダクトのインフラを簡単に作れる状態」でした。ところが、DBユーザーの作成だけはどうしても手動作業が残ってしまっていました。 せっかくDevinに投げれば数時間でインフラができるのに、DBユーザーだけは人間が踏み台経由でDBに繋いで CREATE USER して GRANT して……とやらないといけない。これだとボイラープレートの意味が薄れてしまいますし、手動オペレーションにはミスのリスクもあります。 ここをTerraformでIaC化できれば、ボイラープレートにサクッと組み込めて、Devinに任せるだけでインフラ構築が完結するようになります。 なぜTerraform? DBユーザーの管理といえばAnsibleを使うパターンも考えました。ただ、弊社のインフラは基本的にTerraformで一元管理しているので、できればTerraformの中で完結させたい。ツールが増えると学習コストも運用コストも増えますし、ボイラープレートにAnsibleのステップを追加するよりも、Terraformモジュールとして組み込む方がシンプルです。 というわけで、Terraformでなんとかする方向で調査を進めました。 Aurora Data APIという選択肢 弊社ではAurora MySQLを使用しており、バージョン3.07以降でRDS Data APIに対応しています。 Data APIは、HTTPSエンドポイント経由でSQLを実行できるAPIです。従来のようにVPC内から直接DBに接続する必要がなく、AWS CLIやSDKからサクッとSQLを叩けます。 これを terraform_data リソースの local-exec プロビジョナーと組み合わせれば、Terraformの中からDBユーザーを作成できるというわけです。 モジュールの実装 実際に作ったTerraformモジュールを紹介します。 DBユーザーの作成・削除 resource "terraform_data" "db_user" { input = { rds_cluster_arn = var.rds_cluster_arn rds_secret_arn = var.rds_secret_arn database_name = var.database_name username = var.username ssm_parameter_name = var.ssm_parameter_name } provisioner "local-exec" { command = <<-EOT PASSWORD=$(aws ssm get-parameter --name "${self.input.ssm_parameter_name}" --with-decryption --query 'Parameter.Value' --output text) aws rds-data execute-statement \ --resource-arn "${self.input.rds_cluster_arn}" \ --secret-arn "${self.input.rds_secret_arn}" \ --database "${self.input.database_name}" \ --sql "CREATE USER IF NOT EXISTS '${self.input.username}'@'%' IDENTIFIED BY '$PASSWORD'" EOT } provisioner "local-exec" { when = destroy command = <<-EOT aws rds-data execute-statement \ --resource-arn "${self.input.rds_cluster_arn}" \ --secret-arn "${self.input.rds_secret_arn}" \ --database "${self.input.database_name}" \ --sql "DROP USER IF EXISTS '${self.input.username}'@'%'" EOT } } ポイントは以下のとおりです。 terraform_data リソースを使って、local-exec プロビジョナーでData API経由のSQLを実行 CREATE USER IF NOT EXISTS で冪等性を担保 when = destroy のプロビジョナーで、terraform destroy 時にユーザーを自動削除 パスワードはSSM Parameter Storeから取得(後述の工夫で安全に管理) 権限の付与・取り消し resource "terraform_data" "db_grant" { for_each = { for idx, grant in var.grants : idx => grant } depends_on = [terraform_data.db_user] input = { rds_cluster_arn = var.rds_cluster_arn rds_secret_arn = var.rds_secret_arn database_name = var.database_name username = var.username privileges = each.value.privileges grant_database = coalesce(each.value.database, var.database_name) grant_table = coalesce(each.value.table, "*") } provisioner "local-exec" { command = <<-EOT aws rds-data execute-statement \ --resource-arn "${self.input.rds_cluster_arn}" \ --secret-arn "${self.input.rds_secret_arn}" \ --database "${self.input.database_name}" \ --sql "GRANT ${self.input.privileges} ON ${self.input.grant_database}.${self.input.grant_table} TO '${self.input.username}'@'%'" EOT } provisioner "local-exec" { when = destroy command = <<-EOT aws rds-data execute-statement \ --resource-arn "${self.input.rds_cluster_arn}" \ --secret-arn "${self.input.rds_secret_arn}" \ --database "${self.input.database_name}" \ --sql "REVOKE ${self.input.privileges} ON ${self.input.grant_database}.${self.input.grant_table} FROM '$<span class="