2026年11月13日
この記事は、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公式(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日に何度もリリースされる) |
スマートフォンなど画面幅が狭い場合は、図と表を左右にスクロールできます。
私が一番驚いたのは、作り方です。公式によると、コードはすべて自律型のAIエージェントが書き、保守しています。エージェントが実AWSで通るテストを書き、同じテストがエミュレータでも通るまで直す、というループを回し続けているそうです。リポジトリはPRを受け付けておらず、不具合はIssueで報告する運用です。
もう一つの特徴が「フィデリティスコア」です。実AWSと同じ結果になるかを確かめるテストの合格数を、サービスごとにリリースのたびに公開しています。v1.0.322のスコアファイルを集計したDevOps Dailyの検証記事によると、全体の合格率は56.5%(42,937件中24,243件)でした。よく使うサービスの数字は次のとおりです。
| サービス | 合格率 | サービス | 合格率 |
|---|---|---|---|
| EventBridge | 71.4% | SQS | 67.8% |
| KMS | 71.3% | S3 | 62.7% |
| SNS | 69.3% | IAM | 58.0% |
| DynamoDB | 68.1% | Lambda | 51.9% |
| Secrets Manager | 68.0% | STS | 47.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 ならエミュレータ
| 環境変数 | 役割 |
|---|---|
AWS_ENDPOINT_URL | 全サービスの接続先をエミュレータにする |
AWS_SHARED_CREDENTIALS_FILE / AWS_CONFIG_FILE | エミュレータが発行した資格情報と設定を読む |
AWS_CA_BUNDLE / NODE_EXTRA_CA_CERTS | 自己署名証明書を信頼させる |
AWS_EC2_METADATA_DISABLED | EC2のメタデータから資格情報を探しに行かせない |
AWS_DISABLE_HOST_PREFIX_INJECTION | SDKがホスト名の先頭に文字列を足すのを止める |
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 はステップをまたがない |
| Terraform | apply後に plan -detailed-exitcode | 差分ありは終了コード2で失敗扱い |
| pytest | aws_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 | 記録だけ。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 で待ち受けるので、アプリはホストのネットワークで動かします。
# 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側を「審判」として残し、両方で通ったテストだけを「エミュレータで回してよいテスト」と認めます。
- テストに
aws_onlyマーカーを付けて分類する(RDS、ECS、ALB、IAMなど) - 接続先は環境変数だけで切り替え、コードには書かない
- 開始時にアカウントIDを確かめ、取り違えを防ぐ
- 片方だけ落ちたテストは
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 します。
| 項目 | 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に絞れます。
私の現時点の結論は次のとおりです。
| 任せる先 | 対象 |
|---|---|
| AWS Emulator | S3、SQS、SNS、DynamoDB、LambdaなどのSDK呼び出し、IaCの構文・参照チェック、AIエージェントの試行錯誤 |
| docker compose | アプリのコンテナの起動、PostgreSQLなどのDBエンジン |
| 実AWS | ECS・RDS・ALB・VPCの実際の動作、IAMとセキュリティ、最後のE2E |
半年後に同じ構成で試したら、この表はかなり書き換わっているかもしれません。そのときは、また記事にします。
※2026年11月時点の公開情報に基づきます。AWS Emulatorは1日に何度も更新されるため、記事中の挙動や数値は変わっている可能性があります。他の方の検証から引いた数値は、それぞれの記事の時点のものです。
確認した情報
- aws/aws-emulator(GitHub) — ライセンス、既定のエンドポイント、PRを受け付けない運用、セキュリティテストに使わないという注意、AIエージェント向けスキル、既知の不具合の一覧、リリース(最新はv1.0.345)。
- AWS Emulator User Guide — インストール、Dockerでの起動、テレメトリ、フィデリティの考え方、HTTP 501の扱い。
- AWS News Blog: Test your applications against AWS locally with AWS Emulator — 2026年11月11日の一般提供、資格情報と証明書のファイル、IAM評価の違い。
- How we built the agents that built AWS Emulator — AIエージェントによる開発と保守、438のAPIモデル。
- DevOps Daily: We tested AWS Emulator — v1.0.322のフィデリティ集計、起動時間、Node.js SDKのSQS、Terraformのproviderによる差とDynamoDBの差分。
- How far can CDK/Terraform stacks run on AWS Emulator — CloudFormationのアラーム、複数起動時の状態の分離。
- Ran Isenberg: AWS Emulator first impressions — 実アプリの統合テスト34件の移行。
- フューチャー技術ブログ(Terraform+GitHub Actions) — TerraformとGitHub Actionsでの利用例。
各情報は2026年11月14日に確認しました。
