このエントリーをはてなブックマークに追加

初めまして、今年5月にインフラチームへ join した石川と申します。

普段の業務では、インフラコストやセキュリティ対策を最適化するためのプロジェクトを担当しています。

この記事では、 AWS におけるセキュリティについて AWS 認定試験を通して学んだことをご紹介します。

(試験対策や合格テクニックについて扱う記事ではありません。あらかじめご了承ください。)

はじめに

アプリケーション実行環境の構築や運用を行なうインフラエンジニアにとって、サイバーセキュリティに関する知識は特に重要な知識の一つです。

アプリケーションの可用性を担保し、情報資産を含む会社のリソースの機密性や完全性を保つには、ネットワークやサーバーを不正な侵入や悪意のある攻撃からいかに守れるか、どのようにそれを実現できるかといったことを理解している必要があるからです。

カルテットではインフラのほとんどを AWS で構築しているため、セキュリティについて学び続けると共に、 AWS 環境でどのようにセキュリティ要件を満たしていくべきかについての理解は重要だと私は考えています。

AWS おけるセキュリティとは?

オンプレミスで求められるセキュリティとクラウド環境で求められるセキュリティには違いがあります。

クラウドにおけるセキュリティについては、 AWS クラウドセキュリティの中の責任共有モデルでも説明されています。

  

https://aws.amazon.com/jp/compliance/shared-responsibility-model/ より引用

  

クラウドを利用する私たちの責任を果たす方法は数多くありますが、そのうちの一つに AWS Well-Architected フレームワークに基づいてインフラの設計・構築、運用を行なう方法があります。

AWS Well-Architected フレームワーク

AWS Well-Architected フレームワークは、高い安全性や性能、障害耐性や効率性を備えたインフラを構築するための主要な概念、設計原則、またはベストプラクティスのことです。 各種 AWS サービスの活用にあたって、これらの原則に従うことが推奨されています。

AWS Well-Architected フレームワークは、次の 6つの柱によって構成されています。 (2021年12月2日に持続可能性が追加されました。

  • オペレーショナルエクセレンス
  • セキュリティ
  • 信頼性
  • パフォーマンス効率
  • コスト最適化
  • 持続可能性

この AWS Well-Architected フレームワークの柱の 1つであるセキュリティの柱についてさらに見ていくと、クラウドセキュリティの構成要素として、次の 5つの領域が定義されていることが分かります。

  • アイデンティティとアクセスの管理
  • 検出
  • インフラストラクチャの保護
  • データ保護
  • インシデントへの対応

雰囲気でなんとなく想像はできるものの、私自身 Well-Architected フレームワークを読むだけでは、それぞれが何を意味するのか、具体的に何をどうすれば良いのかについて正直よく分かリませんでした。 もともと活字が得意ではない私は、この膨大なドキュメントを読むまでに壁、読んでからも壁を感じました。(つまりすぐ心が折れました…)

心が何回も折れていては次第にお布団の中のやさしい世界しか信じられなくなってしまうので、アウトプットしながら楽しく学ぶために AWS 認定試験を受けることにしました。

AWS 認定

AWS 認定は 2021年12月現在、全部で 11の試験があり(SAP on AWS - Specialty ベータ試験はこの数に含まれていません)、役割と専門領域ごとに異なる試験が用意されています。

  

https://aws.amazon.com/jp/certification/ より引用

  

私が受験したのは、 AWS 認定の中でも専門知識が問われる AWS Certified Security - Specialty(AWS認定 セキュリティ 専門知識)という試験で、前述の 5つの領域が出題範囲となっている試験です。

受験対象者には「 IT セキュリティに関してセキュリティソリューションの設計と実装における5年以上の経験、また AWS ワークロードのセキュリティ保護における2年以上の実務経験が必要」となっていますが、これは受験資格ではないため誰でも受験できます。

この試験で問われる知識や能力は AWS Certified Security - Specialty 試験ガイドにある通りですが、AWS におけるセキュリティについて広範囲かつ網羅的な内容が扱われます。

また、実際の試験では特定の具体的な課題に対する解決策が問われるため、セキュリティの柱を構成する 5つの領域について、試験勉強を通して具体例を交えた体系的な学習をすることができました。

学んだこと

多くのことを学びましたが、今回はその中でも特に私にとって有用だった内容やこれから充実させていきたいと考えていることを一部ご紹介します。

IAM (AWS Identity and Access Management)

厳密な権限管理

IAM は AWS において、もっとも重要なサービスの一つです。 IAM は、あらゆる AWS リソースへの安全なアクセスを管理するサービスだからです。

IAM には多くの役割や機能がありますが、ここでは IAM ロールについてお話します。

IAM ロールは IAM ユーザーと違い、パスワードやアクセスキーなどの認証情報を開発者が用意することなく、特定の AWS リソースに対して他の AWS リソースを操作するための権限を付与できます。 ロールを使用してアクセス許可を委任する方法は、 IAM でのセキュリティのベストプラクティスのうちの一つです。

例えば、特定の EC2 インスタンスで実行されるアプリケーションから S3 へのアクセスを IAM ロールを使って実現したいとします。

この場合必要な権限は、単に「 S3 へアクセスするための権限を EC2 で実行されるアプリケーションに与え」、「 EC2 からのアクセスを許可するよう S3 側の設定をする」だけではありません。 その設定に加えて「 EC2 で実行されるアプリケーションが、 S3 を操作するために必要となる一時的な認証情報をリクエストし、その認証情報を引き受けることができる権限」を与える必要があります。

これは、 S3 へアクセスするための IAM ロールの信頼関係に EC2 を含めることを意味しています。 これによって EC2 インスタンスで実行されるアプリケーションは STS (AWS Security Token Service) に対して一時的なセキュリティ認証情報のリクエストを行なえるようになります。 STS は、 S3 へアクセスするために必要な一時的な認証情報(15〜60分有効)を EC2 に付与し、 EC2 は STS から渡される一時的な権限を使って S3 へアクセスできるようになります。

  

  

S3 へアクセスするための権限を EC2 が引き受けることができるよう、IAM ロールの信頼関係に EC2 を含める

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

  

このように IAM ロールを使用することによって、限定的な役割または権限を、それを必要とする任意のエンティティへ渡すことができます。 また開発者は、アプリケーションに長期的な認証情報を直接保存する必要がないため、認証情報のローテーションや更新といった作業から解放されます。

IAM ロールによる限定的な権限の付与は、 S3 側で行なうアクセス制限をより厳密に行なう上でもメリットをもたらします。

以下に紹介する JSON のように Principal として特定の IAM ロールを指定することで、指定されたロールからのアクセスのみを許可するよう設定できます。

  

EC2 にアタッチされる特定の IAM ロールからのみアクセスを許可するよう S3 側で Principal を指定する

特定のロール以外からの操作を明示的に拒否する

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListBucket",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::{AccountID}:role/{RoleName}"
        ]
      },
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::{BucketName}"
    },
    {
      "Sid": "GetObjectAction",
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::{AccountID}:role/{RoleName}"
        ]
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::{BucketName}/*"
    },
    {
      "Sid": "S3ActionDeny",
      "Effect": "Deny",
      "Principal": "*",
      "Action": [
        "s3:ListBucket",
        "s3:DeleteObject",
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3::::{BucketName}/*",
        "arn:aws:s3::::{BucketName}"
      ],
      "Condition": {
        "StringNotLike": {
          "aws:userid": [
            "{ExampleRoleID}:*"
          ]
        }
      }
    }
  ]
}

"Sid": "ListBucket" 及び "GetObjectAction"Principal は、 "Effect": "Deny", "NotPrincipal": { "AWS": [ "arn:aws:iam::{AccountID}:role/{RoleName}" ]} とすることもできます。

  

実運用では考慮すべき要素が他にもありますが、このように IAM によってリソースへのアクセスや操作権限を細かく制御することができます。

そしてもちろん、このような IAM ロールの作成や S3 バケットポリシーの設定変更を行なうためにも適切な権限が必要です。

IAM の理解は、 AWS におけるセキュリティを実現する上で重要です。

AWS CloudTrail

AWS リソースへのあらゆる操作の検証

AWS CloudTrail は、アカウントに対して行われたすべての操作を詳細に追跡するためのサービスです。「誰が、いつ、どこで、何をしたのか」といった、 AWS リソースに加えられたあらゆる変更と操作に関する疑問を解決するために活用できます。

AWS CloudTrail では、以下のような情報が記録されています。(CloudTrail ログファイルの例

  • リクエストを実行したユーザー
  • 使用したサービス
  • 実行されたアクション
  • アクションのパラメーター
  • AWS サービスによって返されたレスポンスなど

また、ログファイルの暗号化や整合性の検証も可能なため、組織内の厳密な監査を行なうこともできます。

アカウント内のリソースに対してどのようなリクエストが許可され、どのようなアクセスが拒否されたのかを、 AWS CloudTrail によって詳細に調査することができます。

Amazon CloudFront

ネットワークやアプリケーションレイヤーのセキュリティ及びアクセスコントロール

Amazon CloudFront は、ユーザーへのウェブコンテンツの配信の高速化を実現するための CDN サービスです。 一般的に CDN を使用してコンテンツをサーバーから直接配信しないようにすることで、サーバーの負荷を低減し、 DDoS 攻撃などからオリジンサーバーを保護することができます。

CloudFront も同様ですが、 AWS Shield、 AWS Web Application Firewall、 Amazon Route 53 とのシームレスな連携が可能なため、ネットワークやアプリケーションレイヤーにおける複数の種類の攻撃に対する保護を一層強化できます。

また、地域制限機能を使って特定の地域のユーザーによるアクセスを回避したり、 OAI という特別なアイデンティティを使って、 S3 バケットへのアクセスを CloudFront からのみに制限することもできます。

CloudFront によって、高い信頼性と可用性のある安全なインフラを構築できます。

AWS Config

AWS リソースの評価及び自動修復

AWS Config は AWS リソースの状態をモニタリングするサービスです。 AWS Config によって、過去の任意の時点でリソースがどのように設定されていたかといった履歴を確認することができます。

また Config Rule にリソースのあるべき状態を定義することで、リソースの状態変化に対して迅速な対応が可能になります。

AWS Config がルールに逸脱したリソースを検知すると、自動的に Amazon Simple Notification Service や AWS CloudWatch Events がトリガーされるため、どの設定によってルールに違反したかについて通知を受け取ったり、 Lambda を組み合わせることにより、違反したリソースの停止や非準拠 AWS リソースの修復を自動で行なうこともできます。

AWS Config によって、組織のセキュリティリスクやコンプライアンスを継続的に評価できます。

AWS CloudFormation

CloudWatch Alarm と組み合わせたロールバック

AWS CloudFormation は、インフラをコードで管理するための IaC (Infrastructure as Code) ツールです。

CloudFormation によって、信頼性と再現性の高いインフラリソースを維持するとともに、迅速なプロビジョニングや複製、制御が可能となるため、 CloudFormation はチームが DevOps を実現する上で大きな役割を果たします。

CloudFormation にはさまざまな特徴がありますが、ロールバックトリガーを使用してスタックオペレーションをモニタリングおよびロールバックするという機能があります。

CloudFormation はスタックの更新時、すべてのリソースがデプロイされてから指定された間アプリケーションの状態をモニタリングします。 この仕組みに CloudWatch Alarm を組み合わせることで、変更適用後のアプリケーションがあらかじめ指定した閾値を超過した場合、スタックオペレーション全体をロールバックするよう設定できます。

AWS CloudFormation によって、より安全な方法でリソースのライフサイクルを管理できます。

学んだことを活用する

ここまでご紹介した AWS サービスの機能は、すでに AWS を活用しておられる方にとってはごく基本的な内容だったかもしれません。 しかしエンジニア経験の浅い私にとって今回の試験勉強は、 AWS におけるセキュリティについて体系的に学ぶとても良い機会になりました。この記事で扱わなかった「データ保護」についても、暗号化技術や KMS をはじめとする AWS における様々な暗号化のためのサービスを学ぶことができました。

どのような選択肢があるか、どのように実現すべきかについてまず知ることは、 AWS Well-Architected フレームワークにおける 5つの柱に沿ったインフラの設計や構築、運用をしていく上でとても大切な知識だと感じました。

試験勉強や日々の学習の中で学んだことを、現在担当しているセキュリティプロジェクトのために存分に活用していきたいと思います。

まとめ

「セキュリティへの投資はリターンがない」と言われることがあります。

これはセキュリティ対策にコストをかけるべきではない、という意味ではありません。 安全で快適なアプリケーションの実行環境を整備するためには、セキュリティ対策に十分なコストをかける必要がありますが、かといってコストをかければセキュリティを高められるというわけでもありません。

どのような対策をどの程度実施すべきなのか判断するためには、堅牢性と利便性のバランス、つまりトレードオフを考慮する必要があります。

引き続き学び続け、常に最適な判断ができるインフラエンジニアとなれるよう成長していきたいです。

最後に、私の AWS 認定試験の取得履歴が分かるリンクもご用意しました。 「もしかしたら試験に落ちた人のブログを読まされてしまったかもしれないし、夜寝れないじゃん」という方はこちらも覗いてみてください。

https://www.credly.com/users/tatsumi-ishikawa/badges

それではまた、新しい学びがありましたら開発部ブログでお知らせします。


このエントリーをはてなブックマークに追加

Symfony Advent Calendar 2021 最終日25日の記事です。
SymfonyWorld 2021 Winterの最後のkeynote The new Testing Landscape: Panther, Foundry and More! で紹介されていた symfony/panther を使って実用的なe2eテストをPHPで書く方法について、実際に試した内容をまとめました。
試したコードはすべてgithubに公開していますので、実際に手元で動かしてみたい方はご利用ください。 https://github.com/77web/symfony-panther-playground-2021

symfony/panther自体は1年以上前から存在していて、私は1年前のSymfonyWorld 2021で初めて存在に気づいたものの、実際のプロジェクトで使うこともなく今日に至っています。
しかし、最近AngularとSymfonyを統合したテストをなんとか自動テストしたい(手動でポチポチやりたくない)という需要が再燃したため、真面目に試してみようと思った次第です。

symfony/pantherを使えるようにする

まず、composerで依存として追加し、ブラウザドライバーをインストールします。

# Pantherを追加
composer require --dev symfony/panther
# chromedriverをインストール・設定
composer require --dev dbrekelmans/bdi
vendor/bin/bdi detect drivers

次に phpunit.xml.dist でPantherのServerExtensionを有効にします。通常はphpunitをcomposerで追加したときに既にコメントアウトされた形で書き込まれているので、コメントを外して有効化するだけで良いはずです。

-    <!-- Run `composer require symfony/panther` before enabling this extension -->
-    <!--
    <extensions>
        <extension class="Symfony\Component\Panther\ServerExtension" />
    </extensions>
-    -->

これでPantherを使ってテストする準備は整いました。

symfony/pantherを使ってJavaScriptで動きのある画面をテストする

テスト用に簡単なAPIとAPIを利用してDOMを動的に書き換える画面を用意しました。
広告のタイプを選ぶと、対応している媒体が出るだけという簡単なアプリです(PHPer老人会ネタになってしまいますが、昔PEARにHierSelectというコンポーネントがありましたね :grin:

app

この画面を実際にPantherでテストしてみます。
Pantherの前にまず、普通のWebTestCaseでテストを書いてみました。

<?php
// ...
    public function test()
    {
        $client = static::createClient();
        $client->request('GET', '/');

        $this->assertResponseIsSuccessful();
    }

当然ながらDOMの動的な変化はテストできないので、「画面が正常に表示されたこと」までしか調べることができません。
では次にPantherを使ってテストを書いてみます。

<?php

// ...
    public function testPanther()
    {
        $client = static::createPantherClient();
        $client->request('GET', '/');

        // index.jsが "/vendorTypes" APIを呼んでDOMを変化させるのを待ち、変化後のDOMに対してCrawlerを作る
        $crawler = $client->waitForElementToContain('#type', 'search', 10);
        // DOMが変化した内容を確認する
        $this->assertCount(4, $crawler->filter('#type option'), '1+3 options in types');
        $this->assertSelectorIsDisabled('#vendor');

        $crawler->filter('#type')->children()->eq(2)->click();
        $client->takeScreenshot(__DIR__.'/../../../selectDisplayInType.png');
        // index.jsが "/vendors" APIを呼んでDOMを変化させるのを待ち、変化後のDOMに対してCrawlerを作る
        $crawler = $client->waitForElementToContain('#vendor', 'Googleディスプレイ広告', 10);
        // DOMが変化した内容を確認する
        $this->assertCount(3, $crawler->filter('#vendor option'), '3 options in vendors');
        $this->assertSelectorIsEnabled('#vendor');
        $this->assertSelectorAttributeContains('#vendor option', 'value', 'gdn');
    }

PHPのコードだけでなく、jQueryと結合した挙動に対して検証するテストができていることがわかります。

symfony/pantherでデータベースfixtureを利用してテストする

Pantherのテストは APP_ENV=test ではなく APP_ENV=panther で実行されます。
そのまま実行するとDATABASE_URLは .env.test のものでなく .env のものが使われてしまうので .env.panther を作って.env.testと同じものを書き込んでおきましょう。
いろいろ試したのですが、PantherTestCaseの中で APP_ENV=panther のContainerを取得することができなかった(取得できたContainerがどうしても APP_ENV=test でした)ので、 pantherのDBはtestのDBと同じ設定にしておくと良いようです。 testと同じDBを使うことで、LiipTestFixturesBundle(v2系)を使ってyamlで書いたfixtureを利用することもできます。

<?php

// ...

class IndexPantherTest extends PantherTestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        $client = static::createPantherClient();

        $databaseTool = self::getContainer()->get(DatabaseToolCollection::class)->get();
        $databaseTool->loadAliceFixture([__DIR__.'/../../fixtures/user.yaml']);

        static::ensureKernelShutdown();
    }
    // ...
}

symfony/pantherでログイン後の画面をテストする

PantherのClientを使っている場合、 $client->loginUser() は利用できません。
PantherのテストでのログインについてはPantherのレポジトリにいくつかissueがあり、様々なworkaroundが提案されていますが、現時点で一番堅実だったのは実際にログインフォームにアクセスしてログインしてしまうことでした。

<?php
// ...
    public function testMember()
    {
        $client = static::createPantherClient();
        $crawler = $client->request('GET', '/login');
        $form = $crawler->selectButton('Sign in')->form();
        $form['email'] = 'member-email@quartetcom.co.jp';
        $form['password'] = 'password';
        $client->submit($form);

        $client->request('GET', '/private');
        $this->assertSelectorTextContains('body', 'member-email@quartetcom.co.jp');

        // ここでログアウトしておかないと、次のテストメソッドにもログイン状態が引き継がれる
        $client->request('GET', '/logout');
    }

    public function testNonMember()
    {
        $client = static::createPantherClient();
        $crawler = $client->request('GET', '/private');
        $this->assertTrue(str_contains($crawler->getUri(), '/login'), $crawler->getUri()); // $client->getResponse() is not available in PantherClient
    }

また、1個めのテストメソッドでログインしてしまうとクッキーが次のテストメソッド・他のテストケースにも引き継がれてしまうようなので、Pantherでログインを伴うテストをした場合は最後でログアウトしたほうが良いでしょう。

まとめ

フロントエンドとの結合テストは、慣れればcypressより簡単に書けそうな気がします!
一方、ログイン周りはまだちょっと課題かなと思います…。

※ Pantherの読み方について
Pantherのレポジトリトップには豹のロゴがついており、おそらく豹の「パンサー」と読むので合っていると思います。
某戦車アニメにはまったことのある私はついついパンターと読んでしまいます… :sweat_smile:

今年もSymfonyアドベントカレンダー全部埋まりましたね!お付き合いいただいた皆さんありがとうございました!
また来年もよろしくお願いします!!


このエントリーをはてなブックマークに追加

Symfony Advent Calendar 2021 day 20の記事です!
昨日は @polidogさんのSymfony PassportでFirebase Authentication認証を使う でした。

SymfonyWorldにて、symfonycastsで有名なweaverryanさんのテストのセッションがあり、 zenstruck/foundry というテストフィクスチャ登録用のライブラリが紹介されていました。Laravelのfactoryを彷彿とさせる使用感ですが、個人的に長年愛用してきた liip/test-fixtures-bundle の使い方が最近変わって、微妙に使いにくくなったのが気になっていたので、新しいfoundryの使い方を調べてみました。

※ 実際のソースコードは https://github.com/77web/symfony-foundry-playground にて公開しています。

zenstruck/foundryを追加

$ composer req --dev zenstruck/foudndry

bundleっぽくない名前ですが、flexによって config/bundles.php にバンドルとしても追加されていました。

+    Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'test' => true],

ファクトリを作る

ファクトリクラスは --test をつけると tests/ の下に、 --test なしだと src/ の下に作られます。今回はテスト用のfixtureのためのファクトリを作りたいので --test つきで実行しました。

$ bin/console make:factory --test

 Entity class to create a factory for:
  [0] App\Entity\Task
 > 0

 created: tests/Factory/TaskFactory.php

           
  Success! 
           

 Next: Open your new factory and set default values/states.
 Find the documentation at https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories

作られたTaskFactoryクラスはこんな感じです。

<?php

namespace App\Tests\Factory;

// ...

final class TaskFactory extends ModelFactory
{
    public function __construct()
    {
        parent::__construct();

        // TODO inject services if required (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services)
    }

    protected function getDefaults(): array
    {
        return [
            // TODO add your default values here (https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories)
            'title' => self::faker()->text(),
        ];
    }

    protected function initialize(): self
    {
        // see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
        return $this
            // ->afterInstantiate(function(Task $task): void {})
        ;
    }

    protected static function getClass(): string
    {
        return Task::class;
    }
}

機能テストでファクトリを使ってみる

機能テストでファクトリクラスを使うためには、まずテストクラスに Zenstruck\Foundry\Test\Factories をuseさせる必要があります。

ファクトリの使い方は2種類あります。

  • TaskFactory::createOne() …1件のTaskを作る
  • TaskFactory::createMany() …指定件数のTaskを作る

また、ファクトリにフィクスチャを作らせる前に static::createClient() を実行しておきましょう。

<?php

namespace App\Tests\Controller\TaskController;

+ use Zenstruck\Foundry\Test\Factories;

class CompletedTest extends WebTestCase
{
    use Factories;

    public function test()
    {
        $client = static::createClient();
    
+        // 1件未完了タスクを作る
+        TaskFactory::createOne(['completedAt' => null]);
+        // 2件完了済タスクを作る
+        TaskFactory::createMany(2, ['completedAt' => new \DateTimeImmutable('yesterday')]);

        $crawler = $client->request('GET', '/task/completed');

        $this->assertResponseIsSuccessful();
        $this->assertTrue($crawler->filter('title')->text() === '完了したタスク');
+        $this->assertEquals(2, $crawler->filter('li')->count(),'完了済の2件のみ');
    }
}

繰り返しテストを実行するために(テスト用DBのリセット)

テストが通ったと喜んで、再度実行したら落ちました 😅

There were 2 failures:

1) App\Tests\Controller\TaskController\CompletedTest::test
完了済の2件のみ
Failed asserting that 4 matches expected 2.

/Users/hiromi/projects/tryout-foundry/tests/Controller/TaskController/CompletedTest.php:27

2) App\Tests\Controller\TaskController\IndexTest::test
未完了の1件のみ
Failed asserting that 3 matches expected 1.

/Users/hiromi/projects/tryout-foundry/tests/Controller/TaskController/IndexTest.php:27

FAILURES!
Tests: 2, Assertions: 6, Failures: 2.

どうやら2回目に実行すると前回実行したデータが残っているようです。(しかもテストが2つあるので、それぞれ2回ずつtaskが作られたデータが残っています)
テストごとにデータベースをリフレッシュするには Zenstruck\Foundry\Test\ResetDatabase traitをuseする必要があります。
なお、公式ドキュメントでは「全部のテストケースに2つのtraitをuseするのは大変なので、自分用のWebTestCaseを作ると良い」と書かれていました。

<?php

namespace App\Tests\Controller\TaskController;

use Zenstruck\Foundry\Test\Factories;
+ use Zenstruck\Foundry\Test\ResetDatabase;

class CompletedTest extends WebTestCase
{
    use Factories;
+    use Factories, RefreshDatabases;

    public function test()
    {

DAMADoctrineTestBundle https://github.com/dmaicher/doctrine-test-bundle を使うと、テストケース全体をtransactionで囲って高速化もできるようです(試してません)

IDの強制セットができる

以前 Doctrineのエンティティを対象にしたテスト方法あれこれ で検討した、エンティティにidのsetterメソッドを作らなかったときにIDをセットする方法が、foundryではそのまま用意されていました。 forceSet() メソッドを使うことで、setterのないプロパティにも値をセットすることができます。

<?php

// これはエラー
TaskFactory::createOne(['id' => 999]);
// これはOK
$taskProxy = TaskFactory::createOne();
$taskProxy->forceSet('id', 999);

ユニットテストで使う

このファクトリですが、なんとユニットテスト(SymfonyのKernelTestCaseでもWebTestCaseでもない、PHPUnitの普通のTestCaseクラス)でも使えます。 ユニットテストでfoundryのファクトリを利用して作られたエンティティは、DBに保存されず(ここがLaravelのfactoryとは違うところですね 😁)、DB接続のない環境でもテストが通ります。

ただし、 XXXFactory::createOne() の返り値はエンティティそのものでなくfoundryの Proxy のインスタンスになっているため、そのままサービスクラスに渡すことはできないので注意が必要です。 XXXFactory::createOne()->object() でエンティティ自体のインスタンスを取得できます。

<?php

// ...

    use Factories;

    public function test()
    {
        $task = TaskFactory::createOne()->object();

        $this->emP->persist(Argument::that(function (Task $task) {
            return $task->getCompletedAt() !== null;
        }))->shouldBeCalled();
        $this->emP->flush()->shouldBeCalled();

        $this->getSUT()->markComplete($task);
    }

今までの生エンティティを直接使う書き方(下記)より可読性が良い気はします。

<?php

$task = (new Task())
  ->setTitle('foo')
  ->setDescription('bar')
;

参考リンク