このページはPR・広告を含みます

AWS公式のローカルエミュレータ「AWS Emulator」でCIの自動テストを強化しようとした話

2026-11-14 | Shinichi Noguchi | AWS

【技術相談】本件の内容に関して30分間の無料相談承ります →

AWS Emulator CIの統合テストはどこまで任せられるか

この記事は、2026年11月11日に公開されたAWS Emulatorを、筆者がTerraform+ECS Fargate+RDSの構成で試した記録です。仕様と数値は2026年11月14日時点の公式情報と、末尾に挙げた検証記事で確かめました。他の人の検証から引いた数字は、出典をその場で書いています。

CIの統合テストを、実AWSなしで回したかった

結論から書くと、AWS Emulatorに「システム全体の自動統合テスト」を任せるのは、まだ早いと感じました。ただ、触れば触るほど夢のあるツールだとも思っています。

2026年11月11日、AWSが公式のローカルエミュレータ「AWS Emulator」をオープンソースで公開しました。LocalStackのCommunity版が2026年3月にアーカイブされてから、無料で使える定番のローカルAWSを待っていた人は多いはずです。私もその一人でした。

狙いはシンプルです。普段組んでいるTerraform+ECS Fargate+RDSの構成を、CIの中で毎回エミュレータに丸ごとデプロイして、統合テストを自動で回したい。実AWSの検証アカウントを使わずに済めば、待ち時間もコストも、消し忘れや誤操作の心配も減ります。

この記事は、その目論見で触り始め、だんだん「できないこと」が見えてきて、それでも最後に「これは化けるかもしれない」と思うまでの記録です。インストールからTerraform、pytest、GitHub Actionsまで、コードを多めに載せています。

AWS Emulatorとは

AWS Emulatorは、AWSのAPIをローカルのHTTPSサーバー1つで再現する、AWS公式のツールです。AWS CLIやSDK、Terraformの接続先を環境変数で切り替えるだけで、実アカウントなしに「AWSのように応答する相手」に向けて開発できます。

AWS Emulatorの概要(2026年11月14日時点)
項目内容
公開元AWS公式(aws/aws-emulator)
公開日2026年11月11日(公開と同時に一般提供)
ライセンスCC0 1.0(vendor/ のみApache 2.0)
実装Rust製。Linux向けのRPM・Debianパッケージで配布
対象438のAWS APIモデル
既定のエンドポイントhttps://127.0.0.1:9297(自己署名証明書)
状態の保存メモリ上。停止すると作ったリソースは消える
料金無料。AWSアカウントも不要
最新版v1.0.345(2026年11月13日。1日に何度もリリースされる)
接続先を環境変数で切り替える仕組みAWS CLI、SDK、Terraformは、環境変数AWS_ENDPOINT_URLなどでローカルのAWS Emulator(127.0.0.1:9297、HTTPS、メモリ上の状態)に向く。実AWSにはリクエストが届かない。起動時に資格情報と証明書がホームディレクトリの.aws/emulator/9297に生成される。接続先を環境変数で切り替える仕組みAWS CLISDK(boto3など)pytest・アプリTerraform環境変数AWS_ENDPOINT_URLAWS_CA_BUNDLE資格情報・configAWS Emulatorhttps://127.0.0.1:9297AWSのAPIを1プロセスで再現状態はメモリ上(停止で消える)アカウントIDは 000000000000~/.aws/emulator/9297/ に鍵と証明書を生成実AWSリクエストは届かない(資格情報も不要)×コードは書き換えず、シェルの環境変数だけで向け先が変わる

スマートフォンなど画面幅が狭い場合は、図と表を左右にスクロールできます。

私が一番驚いたのは、作り方です。公式によると、コードはすべて自律型のAIエージェントが書き、保守しています。エージェントが実AWSで通るテストを書き、同じテストがエミュレータでも通るまで直す、というループを回し続けているそうです。リポジトリはPRを受け付けておらず、不具合はIssueで報告する運用です。

もう一つの特徴が「フィデリティスコア」です。実AWSと同じ結果になるかを確かめるテストの合格数を、サービスごとにリリースのたびに公開しています。v1.0.322のスコアファイルを集計したDevOps Dailyの検証記事によると、全体の合格率は56.5%(42,937件中24,243件)でした。よく使うサービスの数字は次のとおりです。

主なサービスのフィデリティ(v1.0.322、DevOps Dailyの集計から抜粋)
サービス合格率サービス合格率
EventBridge71.4%SQS67.8%
KMS71.3%S362.7%
SNS69.3%IAM58.0%
DynamoDB68.1%Lambda51.9%
Secrets Manager68.0%STS47.8%

この数字の意味は、後半で身にしみることになります。

インストールと起動

配布されているのは、Linux向けのRPMとDebianパッケージです。ビルド済みのDockerイメージは公開されていません。MacやWindowsでは、公式ガイドにあるDockerfileで自分でイメージを作ります。

Linux(パッケージ)

# Amazon Linux / RHEL系(x86_64)
sudo dnf install -y https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator-x86_64.rpm

# Debian / Ubuntu(amd64)
curl -fsSLo aws-emulator.deb https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator_amd64.deb
sudo apt-get install -y ./aws-emulator.deb

aws-emulator -v                 # バージョンとコミットを表示
aws-emulator --telemetry off    # 起動(https://127.0.0.1:9297)

テレメトリは既定でオンです。OSやSDKの種類、どのAIコーディングアシスタントから使っているかまで送られるので、私は --telemetry off を常に付けています。環境変数 EMULATOR_TELEMETRY=off でも止められます。

Mac / Windows(Docker)

FROM public.ecr.aws/amazonlinux/amazonlinux:2023-minimal
# Apple SiliconなどArm環境では aws-emulator-aarch64.rpm に置き換える
ADD https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator-x86_64.rpm /tmp/aws-emulator.rpm
RUN rpm -i /tmp/aws-emulator.rpm && rm /tmp/aws-emulator.rpm \
    && mkdir -p /home/emulator && chown 1000 /home/emulator
USER 1000
ENV HOME=/home/emulator
EXPOSE 9297
ENTRYPOINT ["aws-emulator", "-b", "0.0.0.0"]
docker build -t aws-emulator .
mkdir -p ~/.aws/emulator
PORT=9297
docker run --rm --memory=2g --memory-swap=2g \
  -p 127.0.0.1:$PORT:$PORT \
  -e HOME="$HOME" -v "$HOME/.aws/emulator:$HOME/.aws/emulator" \
  aws-emulator -p $PORT --telemetry off

-p 9297:9297 と書くと、同じネットワークの他の端末からも見えてしまいます。必ず 127.0.0.1: を付けてください。起動は速く、DevOps Dailyの計測では中央値383ミリ秒でした。公式ガイドでは、アイドル時のメモリは約160MiBとされています。

接続先を切り替える

起動するたびに、資格情報と証明書が ~/.aws/emulator/9297/ に作られます。あとは環境変数でそれを指すだけです。私はこれを emu-env.sh として保存し、source して使っています。

# emu-env.sh
d=$HOME/.aws/emulator/9297   # ~ ではなく $HOME で書く
export AWS_ENDPOINT_URL=https://127.0.0.1:9297   # localhost ではなく 127.0.0.1
export AWS_SHARED_CREDENTIALS_FILE=$d/emulator_credentials
export AWS_CONFIG_FILE=$d/config
export AWS_CA_BUNDLE=$d/ca.pem
export NODE_EXTRA_CA_CERTS=$d/ca.pem   # Node.jsを使う場合
export AWS_EC2_METADATA_DISABLED=true
export AWS_DISABLE_HOST_PREFIX_INJECTION=true
export AWS_REGION=us-east-1 AWS_DEFAULT_REGION=us-east-1
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN AWS_PROFILE

最後の unset を忘れないでください。実AWS用のアクセスキーが環境変数に残っていると、そちらが優先されてしまいます。設定したら、ヘルスチェックとアカウントIDで向け先を確かめます。

source emu-env.sh
curl --cacert $AWS_CA_BUNDLE https://127.0.0.1:9297/__admin/health   # => ok
aws sts get-caller-identity   # Account が 000000000000 ならエミュレータ
emu-env.shで設定する環境変数
環境変数役割
AWS_ENDPOINT_URL全サービスの接続先をエミュレータにする
AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILEエミュレータが発行した資格情報と設定を読む
AWS_CA_BUNDLE / NODE_EXTRA_CA_CERTS自己署名証明書を信頼させる
AWS_EC2_METADATA_DISABLEDEC2のメタデータから資格情報を探しに行かせない
AWS_DISABLE_HOST_PREFIX_INJECTIONSDKがホスト名の先頭に文字列を足すのを止める
AWS_REGION / AWS_DEFAULT_REGIONリージョンを固定する

AWS CLIで触ってみる

最初の印象は「普通にAWSだ」でした。いつものコマンドがそのまま通り、応答は数ミリ秒で返ってきます。

# S3
aws s3 mb s3://local-getting-started
echo "hello from the emulator" > hello.txt
aws s3 cp hello.txt s3://local-getting-started/
aws s3 ls s3://local-getting-started/

# DynamoDB
aws dynamodb create-table --table-name demo \
  --attribute-definitions AttributeName=id,AttributeType=S \
  --key-schema AttributeName=id,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST
aws dynamodb put-item --table-name demo \
  --item '{"id": {"S": "item-1"}, "msg": {"S": "hello"}}'
aws dynamodb get-item --table-name demo --key '{"id": {"S": "item-1"}}'

# SQS + SNS(SNSに投げたメッセージがSQSに届く)
Q=$(aws sqs create-queue --queue-name demo-q --query QueueUrl --output text)
QARN=$(aws sqs get-queue-attributes --queue-url $Q \
  --attribute-names QueueArn --query Attributes.QueueArn --output text)
T=$(aws sns create-topic --name demo-t --query TopicArn --output text)
aws sns subscribe --topic-arn $T --protocol sqs --notification-endpoint $QARN
aws sns publish --topic-arn $T --message "hello sns"
aws sqs receive-message --queue-url $Q

エラーも本物と同じ形で返ってきます。存在しないキーには NoSuchKey、条件付き書き込みの失敗には ConditionalCheckFailedException。未実装のAPIは黙って成功したふりをせず、HTTP 501の NotImplemented を返します。この時点では「これでCIが変わる」とかなり期待していました。

Terraformでリソースを作る

Terraformは、emu-env.sh を読み込んだシェルで実行すれば、providerブロックを空にしたまま動きます。まずはVPC、S3、DynamoDB、IAMロール+Lambdaという素直な構成から試しました。

# main.tf
provider "aws" {}   # 接続先は環境変数で決まる

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_s3_bucket" "uploads" {
  bucket = "demo-uploads"
}

resource "aws_dynamodb_table" "orders" {
  name         = "orders"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "id"
  attribute {
    name = "id"
    type = "S"
  }
  # エミュレータでは on_demand_throughput が -1 で返り、毎回差分が出る既知問題への回避
  lifecycle {
    ignore_changes = [on_demand_throughput]
  }
}

resource "aws_iam_role" "lambda" {
  name = "demo-lambda-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "lambda.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

data "archive_file" "hello" {
  type        = "zip"
  source_file = "${path.module}/hello.py"
  output_path = "${path.module}/hello.zip"
}

resource "aws_lambda_function" "hello" {
  function_name    = "hello"
  role             = aws_iam_role.lambda.arn
  runtime          = "python3.13"
  handler          = "hello.handler"
  filename         = data.archive_file.hello.output_path
  source_code_hash = data.archive_file.hello.output_base64sha256
}
# hello.py
def handler(event, context):
    return {"statusCode": 200, "body": "hello from emulator"}
source emu-env.sh
terraform init
terraform apply -auto-approve

aws lambda invoke --function-name hello out.json && cat out.json
# {"statusCode": 200, "body": "hello from emulator"}

terraform plan -detailed-exitcode   # 冪等性チェック(0なら差分なし)

あっさりapplyが通り、Lambdaも実際に実行されました。DynamoDBの lifecycle は、2回目の plan で on_demand_throughput の差分(-1 → null)が毎回出る問題への回避です。同じ現象は他の検証記事でも報告されています。なお、DevOps Dailyの検証では、Terraform AWS provider 6.73.0で aws_s3_bucket の作成が失敗し、5.100.0では通ったそうです。providerのバージョンによって結果が変わるので、CIではバージョンを固定しておくのが無難です。

続けて、本命のECS Fargate+RDSを足します(抜粋)。

resource "aws_ecs_cluster" "app" {
  name = "app"
}

resource "aws_ecs_task_definition" "web" {
  family                   = "web"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = 256
  memory                   = 512
  container_definitions = jsonencode([{
    name         = "web"
    image        = "nginx:latest"
    portMappings = [{ containerPort = 80 }]
  }])
}

resource "aws_ecs_service" "web" {
  name            = "web"
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.web.arn
  desired_count   = 1
  launch_type     = "FARGATE"
  network_configuration {
    subnets = [aws_subnet.private_a.id]
  }
}

resource "aws_db_instance" "main" {
  identifier          = "app-db"
  engine              = "postgres"
  instance_class      = "db.t4g.micro"
  allocated_storage   = 20
  username            = "app"
  password            = "change-me-please"
  skip_final_snapshot = true
}

これも apply 自体はそれらしく進みます。ただ、ここから雲行きが怪しくなっていきました。

pytestで統合テストを書く

次はテストコードです。最初に入れたのは、「本当にエミュレータを向いているか」を確かめるガードです。環境変数の設定を忘れたシェルで実行すると、実AWSにリソースを作ってしまうからです。

# conftest.py
import os
import boto3
import pytest

@pytest.fixture(scope="session", autouse=True)
def guard_emulator():
    assert os.environ.get("AWS_ENDPOINT_URL", "").startswith("https://127.0.0.1:")
    acct = boto3.client("sts").get_caller_identity()["Account"]
    assert acct == "000000000000", f"実AWSに向いています: {acct}"

アプリのデータの流れを確かめるテストは、拍子抜けするほど素直に通りました。

# test_orders.py
import boto3
import pytest
from botocore.exceptions import ClientError

ddb = boto3.client("dynamodb")
sqs = boto3.client("sqs")

def test_conditional_write_rejects_duplicate():
    ddb.put_item(TableName="orders", Item={"id": {"S": "o-1"}})
    with pytest.raises(ClientError) as e:
        ddb.put_item(
            TableName="orders",
            Item={"id": {"S": "o-1"}},
            ConditionExpression="attribute_not_exists(id)",
        )
    assert e.value.response["Error"]["Code"] == "ConditionalCheckFailedException"

def test_message_redelivered_when_not_deleted():
    q = sqs.create_queue(
        QueueName="jobs", Attributes={"VisibilityTimeout": "1"}
    )["QueueUrl"]
    sqs.send_message(QueueUrl=q, MessageBody="charge")
    first = sqs.receive_message(QueueUrl=q, WaitTimeSeconds=1)["Messages"]
    # delete_message を忘れたバグを再現:モックでは気づけない
    again = sqs.receive_message(QueueUrl=q, WaitTimeSeconds=3)["Messages"]
    assert again[0]["Body"] == first[0]["Body"]

SQSの可視性タイムアウトや、DynamoDBの条件付き書き込みは、自前のモックでは再現しにくい挙動です。ここが実AWSと同じルールで動くのは、素直にうれしいところでした。

Node.jsを使う場合は一点だけ注意が要ります。SDK v3のSQSクライアントは、返ってきたQueueUrlのホスト名をそのまま辿るため、エミュレータではなく実AWSのSQSに送ろうとします(DevOps Dailyでも同じ報告があります)。

import { SQSClient } from "@aws-sdk/client-sqs";
// これを付けないとエミュレータではなく実AWSのSQSに飛ぶ
const sqs = new SQSClient({ useQueueUrlAsEndpoint: false });

問題は、本来書きたかったテストの方でした。「ALB経由でWebアプリが200を返す」「アプリがRDSに書き込める」というテストです。

# test_web.py(書いてみたが、通らなかったテスト)
import boto3
import requests

def test_ecs_service_is_running():
    ecs = boto3.client("ecs")
    svc = ecs.describe_services(cluster="app", services=["web"])["services"]
    assert svc and svc[0]["runningCount"] == 1

def test_web_via_alb():
    elb = boto3.client("elbv2")
    dns = elb.describe_load_balancers(Names=["app-alb"])["LoadBalancers"][0]["DNSName"]
    assert requests.get(f"http://{dns}/health", timeout=5).status_code == 200

このあたりから、エミュレータの正体が少しずつ見えてきます。

GitHub Actionsに組み込む

その前に、CIへの組み込み自体はどうだったか。これも拍子抜けするほど簡単でした。LinuxのランナーならDockerも要らず、.debを入れてバックグラウンドで起動するだけです。

# .github/workflows/emulator-test.yml
name: emulator-test
on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    permissions:
      contents: read          # AWSの資格情報は一切渡さない
    env:
      EMULATOR_TELEMETRY: "off"
    steps:
      - uses: actions/checkout@v4

      - name: Install aws-emulator
        run: |
          curl -fsSLo aws-emulator.deb \
            https://downloads.aws.com/aws-emulator/packages/aws-emulator/LTS/latest/aws-emulator_amd64.deb
          sudo apt-get install -y ./aws-emulator.deb

      - name: Start aws-emulator
        run: |
          nohup aws-emulator > emulator.log 2>&1 &
          d="$HOME/.aws/emulator/9297"
          for _ in $(seq 1 60); do
            if [ -f "$d/ca.pem" ] && curl -sf --cacert "$d/ca.pem" https://127.0.0.1:9297/__admin/health; then break; fi
            sleep 1
          done
          {
            echo "AWS_ENDPOINT_URL=https://127.0.0.1:9297"
            echo "AWS_SHARED_CREDENTIALS_FILE=$d/emulator_credentials"
            echo "AWS_CONFIG_FILE=$d/config"
            echo "AWS_CA_BUNDLE=$d/ca.pem"
            echo "AWS_EC2_METADATA_DISABLED=true"
            echo "AWS_DISABLE_HOST_PREFIX_INJECTION=true"
            echo "AWS_REGION=us-east-1"
            echo "AWS_DEFAULT_REGION=us-east-1"
          } >> "$GITHUB_ENV"

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform apply
        working-directory: infra
        run: |
          terraform init
          terraform apply -auto-approve
          terraform plan -detailed-exitcode   # 冪等性チェック

      - name: Integration tests
        run: |
          pip install boto3 pytest requests
          pytest -m "not aws_only"
ワークフローの各ステップでやっていること
ステップ内容つまずきやすい点
Install.debを取得して入れるlatest は日々変わる
Startバックグラウンド起動し、/__admin/health が返るまで待つca.pem ができる前にcurlすると失敗する
GITHUB_ENV以降のステップに接続先の環境変数を渡すexport はステップをまたがない
Terraformapply後に plan -detailed-exitcode差分ありは終了コード2で失敗扱い
pytestaws_only 以外を実行外したテストの扱い(後述)

本番のCIで使うなら、latest のままにせず、使うバージョンを決めておきたいところです。AWS Emulatorは1日に何度もリリースされ、不具合の修正で挙動が変わることがあります。リリースの一覧はGitHubのReleasesで追えます。バージョンを固定したパッケージの取得方法は、私はまだ確かめていないので、公式ガイドで確認してから使ってください。

AWSの資格情報をリポジトリに一切置かず、読み取り権限だけでTerraformのapplyまで回せる。それだけでも大きな変化です。ただ、pytest -m "not aws_only" という一行が示すとおり、エミュレータでは回せないテストを外す必要が出てきました。その「外すテスト」こそ、当初いちばん自動化したかったものだったのです。

違和感:実体のない架空のリソースたち

わかったのは、AWS Emulatorが再現しているのは「AWSのAPIの振る舞い」であって、「インフラそのもの」ではない、ということです。私の構成で試した範囲では、サービスによって「本当に動くもの」と「記録だけのもの」にはっきり分かれました。

エミュレータの中で「動くもの」と「記録だけのもの」左側の実体があるサービスは、S3とDynamoDBがデータを保存し、SQS、SNS、EventBridgeがメッセージを配送し、LambdaとStep Functionsがコードを実行する。右側の記録だけのサービスは、EC2、VPC、ECS、ALB、RDSで、APIとしては作成できるがVMやコンテナは起動せず通信も通らない。境目はテスト対象がAWSのAPIを呼ぶか、OSやネットワークの上で動くか。エミュレータの中で「動くもの」と「記録だけのもの」筆者のTerraform+ECS+RDS構成で試した範囲実体がある(APIの先で動く)S3・DynamoDBデータを保存し、読み戻せるSQS・SNS・EventBridgeメッセージが実際に配送されるLambda・Step Functionsコードが実際に実行される記録だけ(作ったことになる)EC2 VMは起動しないVPC ネットワークとしては機能しないECS・ALB コンテナも通信もないRDS 本物のPostgreSQLではない境目:テスト対象が「AWSのAPIを呼ぶ」か、「OS・コンテナ・ネットワークの上で動く」か
エミュレータ内での各サービスの実態(筆者が試した範囲)
サービスエミュレータ内での実態
S3、DynamoDBデータを実際に保存し、読み戻せる
SQS、SNS、EventBridgeメッセージが実際に配送される
Lambda、Step Functionsコードが実際に実行される(内蔵のサンドボックス)
EC2記録だけ。VMは起動しない
VPC作成できるが、ネットワークとしては機能しない
ECS(Fargate含む)記録だけ。コンテナは起動しない
ELB / ALBもっともらしいDNS名は返るが、通信は通らない
RDS / Aurora独自のSQLエンドポイント。本物のPostgreSQLやMySQLではない

つまり、前の章で作ったECSサービスもALBもRDSも、「作ったことになっている」だけでした。nginxはどこでも動いていないので、ALB経由のHTTPテストは通りようがありません。RDSについては、一般的なPostgreSQLの機能が通らないというIssueも上がっています。

CloudFormation経由で作る場合は、さらに注意が要ります。CloudFormationで作ったCloudWatchアラームが CREATE_COMPLETE になるのに、describe-alarms では空のリストが返る、という報告がありました(同じアラームを put-metric-alarm で直接作れば見えるそうです)。スタックのステータスだけを見るテストは「偽の緑」になりかねません。必ず各サービスのAPIで実体を読み戻して確かめます。

# スタックのステータスを信じず、各サービスのAPIで読み戻す
aws cloudformation describe-stacks --stack-name app \
  --query 'Stacks[0].StackStatus'                  # CREATE_COMPLETE と出ても
aws cloudwatch describe-alarms --alarm-names app-5xx \
  --query 'length(MetricAlarms)'                   # 0 なら実体がない
aws ecs describe-services --cluster app --services web \
  --query 'services[0].runningCount'               # ECSも実数を見る

他にも気になった点を挙げておきます。

  • 全体のフィデリティは56.5%:検証シナリオの4割以上で、実AWSと何かが違います。よく使うS3でも62.7%です。
  • IAMの評価は実AWSと同じではない:公式ブログも、IAMポリシーをAWSと同じようには評価しないと書いています。READMEでも、セキュリティのテストには使わないよう求めています。
  • 既知の不具合が7,000件近い:リポジトリの issues/things-that-do-not-work/ に、動かない挙動の報告が並んでいます。数え方と時点で違いますが、11月中旬の時点で6,500〜6,900件ほどでした。

Terraform+ECS Fargate+RDSの構成を丸ごと自動テストする、という当初の目論見は、ここで一度崩れました。

それでも使える場面

見方を変えると、使いどころははっきりしています。判断の基準は「テスト対象のコードが、AWS SDKやCLIを呼んでいるか」です。呼んでいる部分はエミュレータに任せられます。OS、コンテナ、ネットワーク、DBエンジンの上で起きることは任せられません。

  • サーバーレスやイベント駆動の統合テスト:S3、SQS、DynamoDB、Lambda中心の構成なら実用的です。Ran Isenbergさんの検証では、実アプリの統合テスト34件を移して、約7秒で全部通ったそうです。ただし、Lambdaのハンドラーはテストのプロセス内で呼んでおり、エミュレータのLambdaやAPI Gatewayは通していないと断り書きがあります。
  • アプリのSDK呼び出し部分:本番がECSでも、アプリをローカルで動かせば、S3やSQS、Secrets Managerを呼ぶコードは検証できます。
  • IaCの事前検証:TerraformやCDKの構文、参照、作成順の確認には十分です。
  • AIコーディングエージェントのサンドボックス:実アカウントの資格情報を渡さずに、作成・検証・修正のループを回させられます。壊しても課金も事故もありません。公式も、AIエージェント向けの aws-emulator スキルを用意しています(AWS CLI 2.35.0以降が必要)。
  • 並列テスト:ポートを変えて複数起動すると、状態が完全に分かれます。9297で作ったバケットは、9298からは見えません。
  • 研修やハンズオン:アカウントの発行も後片付けも要りません。

逆に、IAMやセキュリティの検証、負荷試験、ネットワークの疎通、コンテナの起動、DBエンジン固有の挙動には向きません。

ECS+RDS構成では「三者分担」に落ち着いた

ECS+RDSの構成について、私は「アプリとDBはdocker compose、AWSのマネージドサービスはエミュレータ」という分担に落ち着きました。エミュレータはホストの 127.0.0.1 で待ち受けるので、アプリはホストのネットワークで動かします。

ローカルでの役割分担アプリのコンテナはホストネットワークで動き、DATABASE_URLでdocker composeの本物のPostgreSQL(127.0.0.1:5432)につなぎ、AWS_ENDPOINT_URLでAWS Emulator(127.0.0.1:9297)のS3やSQSを呼ぶ。実RDSを試したいときはSSMのポートフォワードでlocalhost:15432につなぎ、DATABASE_URLだけを差し替える。ローカルでの役割分担開発機 / CIランナー(127.0.0.1)web(アプリ)network_mode: hostPostgreSQL 16docker compose :5432AWS EmulatorS3・SQSなど :9297DATABASE_URLAWS_ENDPOINT_URL実RDS(必要なときだけ)SSMポートフォワード → localhost:15432DB接続はAWS SDKを通らないただのTCP接続。DATABASE_URL だけで向け先を変えられる。
# compose.yaml(エミュレータはホストで起動済みの前提)
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: appdb
    ports:
      - "127.0.0.1:5432:5432"

  web:
    build: .
    network_mode: host
    environment:
      DATABASE_URL: postgres://app:app@127.0.0.1:5432/appdb   # DBは本物のPostgreSQL
      AWS_ENDPOINT_URL: https://127.0.0.1:9297                # S3/SQSなどはエミュレータ
      AWS_SHARED_CREDENTIALS_FILE: /emu/emulator_credentials
      AWS_CONFIG_FILE: /emu/config
      AWS_CA_BUNDLE: /emu/ca.pem
      AWS_REGION: us-east-1
    volumes:
      - ${HOME}/.aws/emulator/9297:/emu:ro
    depends_on:
      - db

DB接続はAWS SDKを通らないただのTCP接続なので、エミュレータ向けの設定とは関係なく向け先を決められます。実RDSにつなぎたいときも、SSMのポートフォワードを張って DATABASE_URL だけ差し替えれば済みます。

# 別ターミナル。実AWSの資格情報で実行し、emu-env.sh は読み込まない
aws ssm start-session --profile dev \
  --target i-0123456789abcdef0 \
  --document-name AWS-StartPortForwardingSessionToRemoteHost \
  --parameters '{"host":["mydb.xxxx.ap-northeast-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["15432"]}'

実AWSとの並行テスト

現実的な運用は、同じテストコードを接続先だけ変えて、CIの2つのジョブで流す形だと考えています。実AWS側を「審判」として残し、両方で通ったテストだけを「エミュレータで回してよいテスト」と認めます。

同じテストを2つの接続先で流す共通のテストコードとTerraformを、プルリクエストごとにエミュレータのジョブで資格情報なしに実行し、aws_onlyのテストはスキップする。mainへのマージ時や夜間には、OIDCで検証用アカウントにつなぐ実AWSのジョブで全テストを実行し、最後にterraform destroyする。片方だけ落ちたテストはaws_onlyに移し、差分はIssueで報告する。同じテストを2つの接続先で流す共通のコードpytest+Terraform接続先は環境変数だけemulator ジョブPRごと・資格情報なし・数十秒TEST_TARGET=emulatoraws_only マーカーはスキップaws ジョブ(審判)mainマージ時・夜間・OIDCで検証用アカウント全テストを実行workspace分離・最後に terraform destroy結果を突き合わせ片方だけ落ちたらaws_only へ移し差分はIssueへ
  1. テストに aws_only マーカーを付けて分類する(RDS、ECS、ALB、IAMなど)
  2. 接続先は環境変数だけで切り替え、コードには書かない
  3. 開始時にアカウントIDを確かめ、取り違えを防ぐ
  4. 片方だけ落ちたテストは aws_only に移し、差分はIssueで報告する
# conftest.py(並行版)
import os
import boto3
import pytest

TARGET = os.environ.get("TEST_TARGET", "emulator")
EXPECTED = {
    "emulator": "000000000000",
    "aws": os.environ.get("AWS_TEST_ACCOUNT_ID"),
}

@pytest.fixture(scope="session", autouse=True)
def guard():
    acct = boto3.client("sts").get_caller_identity()["Account"]
    assert acct == EXPECTED[TARGET], f"接続先が違います: {acct}"

def pytest_collection_modifyitems(config, items):
    if TARGET != "emulator":
        return
    skip = pytest.mark.skip(reason="実AWS専用")
    for item in items:
        if "aws_only" in item.keywords:
            item.add_marker(skip)
@pytest.mark.aws_only
def test_web_via_alb():
    ...

マーカーはpytestの設定にも登録しておくと、綴り間違いを警告してくれます。

# pytest.ini
[pytest]
markers =
    aws_only: 実AWSでしか確かめられないテスト(ECS・ALB・RDS・IAMなど)
# .github/workflows/dual-test.yml
jobs:
  emulator:              # PRごと。資格情報なし
    runs-on: ubuntu-latest
    permissions: { contents: read }
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/start-emulator.sh   # 起動して AWS_* を $GITHUB_ENV へ
      - run: pytest
        env: { TEST_TARGET: emulator }

  aws:                   # mainマージ時や夜間。OIDCで検証用アカウントへ
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions: { id-token: write, contents: read }
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::<ACCOUNT_ID>:role/ci-test
          aws-region: ap-northeast-1
      - run: pytest
        env:
          TEST_TARGET: aws
          AWS_TEST_ACCOUNT_ID: "<ACCOUNT_ID>"

scripts/start-emulator.sh は、前の章のワークフローの「Start aws-emulator」ステップの中身をそのまま切り出したものです。Terraformはコードを共通にして、stateだけを分けます。エミュレータ側は使い捨てのローカルstate、実AWS側は実行ごとのworkspaceです。実AWS側はリソース名に実行IDを付けて衝突を避け、最後に必ず terraform destroy します。

2つのジョブの違い
項目emulator ジョブaws ジョブ
実行タイミングPRごとmainマージ時・夜間
資格情報なし(contents: read のみ)OIDCで検証用ロールを引き受ける
Terraformのstate使い捨てのローカルstate実行ごとのworkspace
対象テストaws_only 以外すべて
後片付け不要(停止で消える)terraform destroy 必須

この章の並行テストの構成は、設計案として書いたものです。ワークフロー全体を実際に動かしての検証はまだしていません。

統合テストには時期尚早、でも夢はある

Terraform+ECS Fargate+RDSのような構成を、エミュレータだけで自動統合テストするのは、現時点では時期尚早です。EC2もECSもALBもRDSも、作ったことになるだけで実体がありません。「動くインフラ」を期待していた私には、正直肩透かしでした。

それでも、このツールには夢があると感じています。理由は3つあります。

  • 伸びる速度が速い:開発も保守もAIエージェントが担い、1日に何度もリリースされています。実AWSとの差はスコアとして公開され、日々埋まっていきます。
  • AIエージェントに「壊していいAWS」を渡せる:資格情報なしで、インフラの構築からテストまで自律的に回させられます。Claude Codeのようなエージェントを日常的に使う人ほど、効いてくるはずです。
  • テストの段の組み方が変わる:これまで実AWSにデプロイしてから回していた統合テストの一部を、デプロイ前に秒単位で回せます。実AWSは最後のE2Eに絞れます。

私の現時点の結論は次のとおりです。

テストの任せ先(2026年11月時点の筆者の結論)
任せる先対象
AWS EmulatorS3、SQS、SNS、DynamoDB、LambdaなどのSDK呼び出し、IaCの構文・参照チェック、AIエージェントの試行錯誤
docker composeアプリのコンテナの起動、PostgreSQLなどのDBエンジン
実AWSECS・RDS・ALB・VPCの実際の動作、IAMとセキュリティ、最後のE2E

半年後に同じ構成で試したら、この表はかなり書き換わっているかもしれません。そのときは、また記事にします。

※2026年11月時点の公開情報に基づきます。AWS Emulatorは1日に何度も更新されるため、記事中の挙動や数値は変わっている可能性があります。他の方の検証から引いた数値は、それぞれの記事の時点のものです。

確認した情報

各情報は2026年11月14日に確認しました。

カテゴリ

AWS

公開日

2026-11-14

💬 無料技術相談のご案内

この記事でご紹介した技術について、導入や活用のご相談を30分間無料で承っております。

  • 「自社でも導入できる?」といった技術的な疑問
  • 既存システムとの連携・移行に関するご相談
  • コスト感や導入スケジュールの目安

30年以上のIT経験をもとに、率直にお答えします。強引なセールスや勧誘は一切ありません。

野口真一 野口真一

お気軽にご相談ください

記事に関するご質問や、AI・IT技術導入のご相談など、お気軽にお問い合わせください。